Exception
Challenge Lab (Medium) - by Vikram Pawar
The following post by 0xb0b is licensed under CC BY 4.0
Scenario
Objective / Scope
As part of an internal penetration test, you have discovered a server handling sensitive corporate communications. Compromising this high-value target is key to demonstrating tangible risk to the client. Conduct a full-scope penetration test against the target IP to identify, exploit, and report on any existing vulnerabilities.
Initial Access
You have been provided with VPN access to their internal environment, but no other information.
Summary
Summary
In Exception we begin with only VPN access to an internal server and no prior credentials. Enumeration reveals SSH on port 22 and two web services on ports 80 and 3000. While port 80 hosts only a static page, port 3000 exposes a Rocket.Chat 3.12.1 instance. After registering a low-privileged user, we enumerate the application and identify the exact version via /api/info. Research confirms that this version is vulnerable to CVE-2021-22911, a NoSQL injection that can be chained into full account takeover and remote code execution.
Instead of blindly running the public exploit, we first analyze and understand it. The vulnerability chain abuses unauthenticated and authenticated NoSQL injection to leak password reset tokens and the administrator’s TOTP secret via $regex and $where operators, resets the administrator password, and finally leverages Rocket.Chat’s webhook integration feature to execute arbitrary system commands. We adapt the exploit to our environment: replacing email-based authentication with username-based login, parameterizing credentials, and integrating a Base64-encoded reverse shell payload to avoid escaping issues.
This reinforces an important lesson: you must read and understand exploits to know what they actually do, what they might break, and to ensure they behave as expected in your target environment.
Executing the modified exploit grants a reverse shell as the rocketchat user inside a container. Within the container, we discover Backup_db.txt, containing database credentials reused on the host. Using these credentials, we authenticate via SSH as user Ron and obtain the user flag. Further enumeration reveals that Ron can execute /opt/log_inspector/check_log --clean as root via sudo. The script opens a temporary file in nano, which allows shell escape. We spawn a root shell and retrieve the final flag from /root/root.txt.
Recon
We use rustscan -b 500 -a 10.1.49.196 -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.
A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

In addition to SSH on port 22, we identify two further web services on ports 80 and 3000.

WEB 80
On port 80, we only have a static page. The directory scan yielded no results, and the chat bot we have in front of us is implemented in JavaScript and does not generate any requests to the server.

WEB 3000
Things get a little more interesting on port 3000. Here we have rocket.chat, which is an open-source team communication platform that provides chat, video conferencing, and collaboration tools similar to Slack or Microsoft Teams.

We can also register low-privileged users on this service. We do that to enumerate the instance from within.

We find a general chat room where we see the user localh0ste. This user also reveals their email address. This could be useful later on.

We note that down.
Perhaps we can find out more about the version. There may be known vulnerabilities. After a quick search, we discover that we can retrieve the version information via /api/info. We are looking at version 3.12.1.

Using searchspliot, we look for possible exploits or POCs and find what we are looking for. Version 3.12.1 is vulnerable to unauthenticated NoSQL injection. There are two exploits, one of which even includes a possibility for unauthenticated RCE. At first glance, this seems very promising.
After further research, we discover that the RCE is not the actual vulnerability, but rather results from the fact that we can compromise admin accounts using NoSQL injection CVE-2021-22911.

Exploit Analysis
We download the exploit onto our machine as follows:

First, we take a closer look at the exploit to understand what it does and thus understand CVE-2021-22911.
The exploit we found targets abuses CVE-2021-22911, a NoSQL injection vulnerability that can lead to a full remote code execution by compromising a administrator account in the exploit chain.
The script chains multiple weaknesses together: an unauthenticated blind NoSQL injection to extract password reset tokens, an authenticated injection that leaks sensitive values through JavaScript exceptions, and laslty it abuse a webhook feature that allows administrators to execute arbitrary system commands.
The script relies on two different techniques for token leakage, whereby the exception leakage method is entirely sufficient and significantly more efficient, but requires a session of a low privileged user.
The script first takes over a low-privileged account without authentication, then uses that account to extract the administrator’s password reset token and two-factor secret, resets the administrator password, and finally abuses Rocket.Chat’s incoming webhook integration feature to execute system commands.
forgotpassword(email, url)
The function forgotpassword(emai, url) triggers Rocket.Chat’s password reset process for a specified email address. It sends a crafted JSON payload to the sendForgotPasswordEmail API method to generate a reset token for the target user.
resettoken(url)
This resettoken(url) function performs the mentioned blind NoSQL injection against the getPasswordPolicy endpoint. It brute-forces the password reset token one character at a time using a $regex prefix match and detects valid guesses by observing differences in server responses. This is a very time consuming approach. Its used to retrieve the reset token of a user to perform an account take-over.
Blind NoSQL Injection (Reset Token Brute Force)
What this does
This injects a $regex operator into the token field. Instead of sending a real reset token, the script sends a regular expression like:
The ^ means 'starts with'. If the guess is correct, the server responds differently. That response difference acts as a boolean oracle, allowing to reconstruct the full reset token one character at a time.
changingpassword(url, token)
This function submits the extracted reset token along with a new password to the resetPassword API endpoint.
twofactor(url, email)
The twofactoer(url, email) function authenticates as the compromised low-privileged user using the email and set password and exploits a $where injection in the users.list endpoint. It forces the server to throw an exception containing the administrator’s TOTP secret, which is extracted from the error message. It assumes that the username of the administrator is admin.
admin_token(url, email)
This function logs in as the low-privileged user and performs another $where injection. Instead of leaking the TOTP secret, it forces the application to throw the administrator’s password reset token via an exception.
changingadminpassword(url, token, code)
This function resets the administrator’s password using the leaked reset token and a valid TOTP code generated from the stolen secret.
rce(url, code, cmd)
This rce(url, code, cmd) logs in as administrator using the new password and valid two-factor code. It then creates a malicious incoming webhook integration containing JavaScript that loads child_process and executes the supplied system command when triggered.
You can find out how to do this manually at hacktricks:
Adaption of the exploit
The script we found has a few flaws. For one thing, it assumes that we do not yet have any users under our control. Since we were able to simply create a user, the blind-based boolean injection step to leak the reset token after the password reset is no longer necessary. This was used to set the password of a low-privilege account to take it over. We can easily comment out these steps in the last few lines.
After running the script for the first time - after commenting out the steps -, we receive an error message stating that authentication failed. After taking a closer look at a login request using Burp Suite, we see that in our version, the username is used instead of the email address.
We had to replace email parameters with username parameters in each payload.
Furthermore, it was not possible to authenticate with the username admin.
The script therefore had to be supplemented with additional parameters that are requested for execution. These included the username of the low-privileged and privileged accounts and the password of the low-privileged user. These adjustments were then also incorporated into the payloads.
After the privileged account was taken over, the pseudo shell proved to be vulnerable to escaping errors, which caused reverse shell payloads to fail. In the script, the pseudo shell was then removed and replaced with a custom reverse shell payload that does not trigger an escape error. The IP and port for the reverse shell can be specified in the parameters.
Down below is each change and the final script:
Added New CLI Arguments (Username, Password, Reverse Shell Support)
Lines ~22–33
Stored New Argument Variables
Lines ~38–44
Modified twofactor() to Use Username + Password
twofactor() to Use Username + PasswordLines ~85–106
Switched from email login → username login
Password is now dynamic (not hardcoded)
Target admin username is dynamic
Modified admin_token() to Use Username + Password
admin_token() to Use Username + PasswordLines ~108–129
Username-based login
Dynamic password
Dynamic admin username target
Modified rce() to Use Chosen Username
rce() to Use Chosen UsernameLines ~142–171
RCE now authenticates using provided admin username
Added Automated Base64 Reverse Shell
Lines ~202–205
Generates reverse shell
Encodes in Base64
Executes via bash
Final Script
Shell as rocketchat
Now that we have customized the script, we can move on to execution. We set up a listener to capture our reverse shell. We use Penelope for this.
Next, we run the modified script.

After execution we get a shell. We are the user rocketchat inside a container.

Shell as Ron
In the root directory of the container we find a Backup_db.txt. This file contains beside the host and database name a db user and the corresponding password.

Maybe those credentials were reused. We try to authenticate as Ron via SSH and are successful. We find the user flag in the users home directory.

Shell as root
While enumerating, we find that the user can execute /opt/log_inspector/check_log --clean as root using sudo.

After execution, we see...

... its just nano opening /tmp/log_clean_temp.

We try to spawn a shell inside nano. Since we are executing it as root, we should receive a root shell. On how to spawn a shell can be taken from GTFOBins.
^R^X translates to CTRL+R CTRL+X
We try to spawn a shell...

... and we have a root shell. We find the final flag at /root/root.txt.

Last updated
Was this helpful?
