AoCyber: Side Quest 2 Part 2

TL;DR: NoSQL inject an unprotected login path until you get the admin index.php.

AoCyber: Side Quest 2 Part 2

See this years' other rooms as well:

SQ 1: Day 0
SQ 2: Day 6
SQ 3: Day 11
SQ 4: Day 20

Index of this challenge:



There wasn't much to start with, just a prompt to look around and dive into a digital snowstorm. At the time, the second hint of "stay stealthy" hadn't yet been released, but it would have saved me some time! I believe the apps were a bit unstable to aggressive scanning and would cause them to fall over. We'll learn a bit more about the reason for this as we go through the article.

I like to go through the OSI model when thinking about what am I looking for. Ports are a great way of seeing which paths I might have forward as they are the most likely to be exploitable. Then I follow with looking for clues/exploits for session handlers like Apache, followed by application level debugging.


└─$ sudo nmap -sC -sV -p- -vv --min-rate 1500 >> /dev/null      

└─$ cat nmapout.article 
Starting Nmap 7.94 ( ) at 2023-12-10 12:17 PST
Nmap scan report for
Host is up, received echo-reply ttl 61 (0.19s latency).
Scanned at 2023-12-10 12:17:57 PST for 94s
Not shown: 65531 closed tcp ports (reset)
22/tcp    open  ssh        syn-ack ttl 61 OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
23/tcp    open  tcpwrapped syn-ack ttl 60
8080/tcp  open  http       syn-ack ttl 60 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 ttl 60
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.0 302 Redirect
|     Server: Webs
|     Location: http://NC-227WF-HD-720P:50628/default.asp
Nmap done: 1 IP address (1 host up) scanned in 97.72 seconds
           Raw packets sent: 71507 (3.146MB) | Rcvd: 70485 (2.819MB)

3 services, and I'll break down each in their own sections in order.

23 Telnet???:

└─$ telnet 23  
Connected to
Escape character is '^]'.
Connection closed by foreign host.

As the connection was allowed but then closed, it signifies that it got past all the boundaries and went to an app that immediately closed the session. It could be a custom app, it could be something subject to overflows, it could be a clue: But I didn't figure it out until the end of the puzzle. I'll leave the mystery alive until we get to the end.

8080 Apache:

This was my first whole day spent. There's not much here, and there's no interesting source code in the page. However the headers do give me some initial clues as to what I'm working on:

HTTP/1.1 403 Forbidden
Date: Sun, 10 Dec 2023 20:33:28 GMT
Server: Apache/2.4.57 (Debian)
Last-Modified: Tue, 05 Dec 2023 18:54:54 GMT
ETag: "3a5-60bc7c52a95e8"
Accept-Ranges: bytes
Content-Length: 933
Connection: close
Content-Type: text/html

Apache on Debian. It's not much but it's a start!

Time to enumerate some more. Gotta go deeper!


└─$ nikto -host                       
- Nikto v2.5.0
+ 0 host(s) tested
- Nikto v2.5.0
+ Target IP:
+ Target Hostname:
+ Target Port:        8080
+ Start Time:         2023-12-06 14:16:11 (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:
+ /index.php/123: Retrieved x-powered-by header: PHP/8.1.26.
+ /index.php/123: Cookie PHPSESSID created without the httponly flag. See:
+ /.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:
+ 26613 requests: 0 error(s) and 5 item(s) reported on remote host
+ End Time:           2023-12-06 15:47:24 (GMT-8) (5473 seconds)
+ 1 host(s) tested

Shows some interesting data like how to access index.php without a 403, and the existence of .DS_Store.


└─$ cat gobuster-task2-7.txt
/index.php/           (Status: 200) [Size: 3513]
/login.php/           (Status: 302) [Size: 2342] [--> /]
/demo                 (Status: 200) [Size: 41]
/vendor               (Status: 301) [Size: 318] [-->]
/server-status        (Status: 403) [Size: 933]
└─$ gobuster dir -u -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -x txt,js,php/,html -t 40 --timeout=3s -o gobuster-task2.txt --retry --timeout=1s

There was a lot of 403 errors, so to filter these naughty list elves out for now. And with this some more definitive paths.

View in a picture:

Let's fill in some blanks

This friggin' elf. But let's check DS_Store. A quick google how to reveals a number of methods, this one's probably the simplest: (

└─$ python ../../.DS_Store
Count:  4
└─$ python ../../.DS_Store4
Count:  20

Now we know we are working with a mongoDB backend and a few other projects that some quick googling tells us that we're looking at a PHP app. We're really cranking now, but we still need to bypass those elves.

Taking a closer look at the enumeration output, we can see that the successful calls came when appended with /. One reason for this could be that Apache is doing a location based filter that has a deny <something> that we haven't quite gone past, and the php/ ending does not match rules such as *.php.

Trying these pages with appending the / results in 302 redirects back to /login.php which itself returns to our elf buddy. But that also lets us know that we are getting requests through to the PHP application, we just have to skirt around Apache.

Let's use some burp suite to really fine tune our calls:

By throwing the request into the Proxy Browser and sending them to the Reapeater, we can really dial in our calls and responses. Here's an example of the login GET page, this is a really juicy looking one.

GET /login.php/ HTTP/1.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

And knowing, from our library calls and that there are exceptions for "users" on the index page, we have a new picture to work with and a new area we can really dig into.

We'll go ahead and rule out further investigation of /vendor/ as we already got our mongo dependency and the other dependencies are for code hygiene, and we'll ignore .DS_Store as it already gave us the vendor folder. Other enumerations of the file were empty.

Testing the login prompt to see what it looks like natively we get a few new snippets of information:

We've got a PHPSESSID to track our authentication, we have an invalid username or password message, and we have further validation that we have a way around .php filtering from Apache.

Attempting to brute force this with hydra yielded nothing but wasted CPU cycles.

Throwing some payloads at it with various types of text yields some interesting errors that confirms some suspicions and gives us one more piece of data: The text is passed into mongo without any pre-parsers!

Now looking into PHP<>MongoDB SQL injection techniques, I found a great article explaining how PHP connectors work with Mongo when left to their own devices.

TL;DR: The parameters can be turned into an associative array by adding some extra characters to the parameters like so:

And in fact, this gives us a couple more pieces of data on how the data is injected.

commandException: unknown operator: $foo 
executeQuery('admin.users', Object(MongoDB\Driver\Query), Array)

Now we know that the code that calls the database is passing the query directly in, expands these fields into the operators section of the query, and it's calling the admin DB with the users collection meaning these should be some juicy results!

Mongo docs give us a lot of context as to what operators we might have available and how they might work for us if we can decode it into a DB. I spent a little time starting a MongoDB container and built a users table with some sample users in it to play with these results to test my hypothesis' on how it might work without alerting the "authorities" by blindly testing them here, but maybe that was excessive.

regex & ne look like really useful way to get in, and googling common NoSQL injections show me that these are indeed very common tools.

Note that there's no invalid password message anymore, and it now redirects somewhere...


But if we take a look back at our diagram, and the URL we end up at, we remember the filters that are applied and can bypass them by going directly to index.php/ that was showing errors before...

BOY HOWDY! We're in.

PHPSESSID is an authenticated token for Frostbite, which should be the first user in the DB since we were okay with "anything not asdf with any password not asdf". Lets pivot to regex and see if we can get a bit more targeted. I'll link my absolute most referenced stack overflow for this next part.

Alright! User#2 by doing a negative look-around regex. This will always be my favorite SO, I wish I could send that user a beer.

Lets expand this to include Snowballer and start enumerating the users... Might take a bit.

And with the following body that only a mother can love (Note that I left out the one-by-one enumeration to help write this article faster)


We have our second flag.

As of the time of this writing, I'm still determined to figure out what /demo/ holds...

Editors note: I have finished all SQs at this point and still haven't gotten into /demo/. I can't wait to read some more write ups so I can get some ideas on how to breach that path.

Side Quest 2 Part 3: Extra points