Funkynator
Funkynator is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Funkynator attack path
Funkynator is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Objective
Challenge walkthrough focused on Pwn evidence, validation, and reusable operator lessons.
Walkthrough flow
Reverse the menu model: saved heap pointers can be...
Arrange adjacent heap chunks so a previous message...
Reuse the adjacency pattern on a small freed chunk to...
Use a two-entry tcache bin before poisoning....
Poison the freed chunk's encoded next pointer so a...
Source coverage
High source coverage
Status: complete. This article is generated from 6 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
High confidence: the page is reconstructed from a primary walkthrough plus multiple supporting notes or evidence sources. Treat the chain as source-backed, while still checking the listed source files for sensitive values.
- Pwn/Funkynator/writeup.md
- htb-challenge/Pwn/Funkynator/notes.md
- htb-challenge/Pwn/Funkynator/memory-summary.md
- htb-challenge/Pwn/Funkynator/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Pwn__Funkynator__memory-summary.md.8cabd49cef.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Pwn__Funkynator__notes.md.04c3a19de9.md
Technical Walkthrough
Writeup
Challenge
- Name: Funkynator
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
Summary
Funkynator is a menu-driven heap challenge built around saving,
deleting, viewing, and continuing heap-backed messages. The useful bug
is an unchecked one-byte write in the continue-processing submenu:
the program accepts an arbitrary unsigned offset and writes one byte
relative to the current message buffer.
The final exploit uses that primitive to turn adjacent heap metadata
and freed-chunk forward pointers into a libc leak, a heap safe-linking
key leak, and then a modern-glibc FILE-structure hijack. The remote flag
was captured with the reproducible solver in solve/solve.py; the raw
flag is intentionally stored only in loot/flag.txt.
Artifact Inventory
Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.
challenge/funkynator: stripped x86-64 PIE ELF.challenge/glibc/libc.so.6: bundled libc used for reliable offsets.challenge/glibc/ld-linux-x86-64.so.2: bundled loader.- Remote instance:
<TARGET>:30596. - Mitigations from
analysis/checksec.txt: Full RELRO, stack canary,
NX, and PIE.
Analysis
Static and dynamic triage showed the challenge keeps up to ten saved
heap message pointers. A saved pointer can be deleted, viewed, or
continued. Continuing a saved message removes it from the saved slot and
returns to the message submenu.
The vulnerable submenu operation is byte overwrite. It asks for an
offset and a byte, then writes that byte without checking whether the
offset is within the message allocation. Because adjacent chunks can be
arranged predictably, the primitive is enough to temporarily overwrite
terminators and chunk metadata, then restore metadata after leaking.
The libc leak comes from freeing a large adjacent chunk into the
unsorted bin and extending the previous message print path until it
reaches the freed chunk's unsorted-bin pointer. The tcache safe-linking
key comes from the same adjacency pattern against a small freed chunk.
The first obvious branch, overwriting __free_hook, was closed. The
bundled glibc still exposes the symbol, but the allocator path does not
call it. The second correction was that one-entry tcache poisoning is
insufficient on this libc because the per-bin count reaches zero before
the forged pointer is returned. The working path uses a two-entry tcache
bin, poisons the freed chunk's encoded next pointer, and allocates a
chunk overlapping _IO_2_1_stderr_.
The final control-flow step is a lock-backed House-of-Apple2 style
stderr payload. The solver writes the FILE payload, repairs bytes that
were transformed by the challenge's funkify routine, exits the program,
and uses the resulting command execution to read flag.txt.
Solve
Run the solver from the challenge workspace:
cd <local workspace>
. .venv311/bin/activate
python solve/solve.py --host <TARGET> --port 30596 --output loot/flag.txtThen capture through the harness:
cd <local workspace>
python3 scripts/challenge_harness.py capture-flag Pwn/Funkynator --from loot/flag.txtThe remote run evidence is stored in analysis/solve-remote-run.err
and analysis/solve-remote-run.txt. The exploit script is
solve/solve.py.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Validate allocator assumptions against the bundled libc rather than
relying on older hook-based exploit patterns.
- Modern tcache poisoning must account for the per-bin count as well as
safe-linking pointer encoding.
- When the target transforms input bytes before storing them, the final
exploit should either choose transform-safe values or repair changed
bytes with the available write primitive.
- For this challenge, public/RAG context was only generic. The decisive
evidence came from local reverse engineering, local Docker validation,
and constrained remote reproduction.
Source-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Challenge: Funkynator
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
- Remote instance: <TARGET>:30596
- Start time: 2026-06-10T20:57:30Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a16ef757-0bb9-4c0c-a9ce-cb67b6335148.zip | 1009939 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 8 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-10T20:57:30Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-10T20:57:42Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-10T20:59:45Z | hypothesis recorded | hypothesis-board.md | Menu-driven heap/memory-slot exploitation: use save/view/delete/continue plus byte overwrite primitive to leak and corrupt allocator/libc state, then gain code execution. | Medium | Run local/static ELF triage, collect remote interaction transcript, then reproduce menu states before exploit gate. |
| 2026-06-10T21:01:11Z | RAG query | analysis/rag/rag-query-20260610T210049907663Z-b581858b.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-10T21:01:11Z | checkpoint recorded | analysis/checkpoint-triage-20260610T210111150513Z-e4bb1e0e.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-10T21:02:06Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-10T21:02:43Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-10T21:06:35Z | local memory search | analysis/research/local-memory-search-20260610T210635752635Z-b86bb733.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-10T21:06:56Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-10T21:07:47Z | evaluator | analysis/evaluator-20260610T210747181789Z-d1133e0a.md | Proceed | High | Build a solver that first validates heap/libc leak behavior, then performs tcache poisoning to __free_hook only if the expected leak and allocator conditions are observed. |
| 2026-06-10T22:00:45Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-10T22:04:14Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The binary is a stripped PIE x86-64 heap challenge with bundled glibc
and strong mitigations: Full RELRO, stack canary, NX, and PIE.
- The core primitive is the continue-processing submenu byte overwrite:
arbitrary offset, one byte, no bounds check against the current heap
message allocation.
- The exploit path used an unsorted-bin leak for libc base, a tcache
safe-linking key leak, two-entry tcache poisoning, and an overlapping
write onto _IO_2_1_stderr_.
__free_hookwas rejected as a dead branch for this bundled glibc:
the symbol exists, but the active allocator path does not call it.
- One-entry tcache poisoning was rejected as incomplete because the
per-bin tcache count reached zero before the forged pointer could be
returned.
- The final solver writes a modern FILE payload, repairs bytes affected
by the funkify transformation, exits to trigger the payload, and reads
flag.txt.
- Remote flag capture succeeded; raw flag remains only in
loot/flag.txt.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Pwn
- Challenge: Funkynator
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Reverse the menu model: saved heap pointers can be deleted, viewed,
or continued; continuing a saved message exposes a submenu with an
unchecked arbitrary-offset one-byte overwrite.
- Arrange adjacent heap chunks so a previous message can print into a
freed large chunk and leak the unsorted-bin pointer for libc base.
- Reuse the adjacency pattern on a small freed chunk to leak the tcache
safe-linking key.
- Use a two-entry tcache bin before poisoning. One-entry poisoning is
insufficient on this libc because the tcache count reaches zero before
the forged pointer is returned.
- Poison the freed chunk's encoded
nextpointer so a later allocation
overlaps _IO_2_1_stderr_.
- Write a lock-backed House-of-Apple2 style FILE payload, repair bytes
mutated by the program's case-changing routine, trigger on program
exit, and read the challenge flag.
Reusable Lessons
- Do not assume
__free_hookremains exploitable just because the
symbol exists. Validate the active libc free path.
- For modern tcache, track both safe-linking encoding and per-bin counts.
- Input transformations can be handled by post-write repair if the bug
provides repeated byte writes.
- Treat RAG/public notes as a checklist only; the reliable path here came
from local reversing and local reproduction.
Dead Ends
__free_hookoverwrite: closed because free does not dispatch through
it in the bundled glibc.
- Single-entry tcache poisoning: closed because count handling prevented
the forged pointer from being returned.
- Early FILE payloads: closed until stderr lock and wide-vtable layout
were corrected.
Tool Quirks
- Docker Desktop had to be running before local Linux validation could
start.
- The challenge's funkify routine changes alphabetic bytes by index, so
exploit payloads need transform-safe values or repair writes.
Evidence Paths
analysis/checksec.txtanalysis/static-map.txtanalysis/source-audit.mdanalysis/fsop-attempt-notes.mdanalysis/solve-local-run.erranalysis/solve-remote-run.errsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Hypothesis Board
Keep no more than 3 active hypotheses on Easy/Medium and 5 on Hard unless the user explicitly asks for breadth.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Menu-driven heap/memory-slot exploitation: use save/view/delete/continue plus byte overwrite primitive to leak and corrupt allocator/libc state, then gain code execution. | Binary strings expose memory slots, save/delete/view/continue actions, byte overwrite, malloc/free, and bundled libc/loader. Local and remote validation confirmed libc leak, heap safe-link key leak, two-entry tcache poisoning, and stderr FILE overwrite. | None for solved path. | Re-run solve/solve.py against the remote instance if reproduction is needed. | High | Confirmed |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|---|---|---|---|
__free_hook overwrite | Bundled libc symbol/disassembly and local exploit attempts | Writes landed, but free did not dispatch through the hook | Modern bundled glibc does not use the classic hook path | Only revisit with a different libc that still calls the hook |
| One-entry tcache poisoning | Local allocator validation during solver development | Forged target was not returned after the real chunk was popped | Per-bin tcache count reached zero, so the forged pointer was ignored | Only revisit with a libc/configuration where count behavior differs |
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Pwn
- Challenge: Funkynator
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Reverse the menu model: saved heap pointers can be deleted, viewed,
or continued; continuing a saved message exposes a submenu with an
unchecked arbitrary-offset one-byte overwrite.
- Arrange adjacent heap chunks so a previous message can print into a
freed large chunk and leak the unsorted-bin pointer for libc base.
- Reuse the adjacency pattern on a small freed chunk to leak the tcache
safe-linking key.
- Use a two-entry tcache bin before poisoning. One-entry poisoning is
insufficient on this libc because the tcache count reaches zero before
the forged pointer is returned.
- Poison the freed chunk's encoded
nextpointer so a later allocation
overlaps _IO_2_1_stderr_.
- Write a lock-backed House-of-Apple2 style FILE payload, repair bytes
mutated by the program's case-changing routine, trigger on program
exit, and read the challenge flag.
Reusable Lessons
- Do not assume
__free_hookremains exploitable just because the
symbol exists. Validate the active libc free path.
- For modern tcache, track both safe-linking encoding and per-bin counts.
- Input transformations can be handled by post-write repair if the bug
provides repeated byte writes.
- Treat RAG/public notes as a checklist only; the reliable path here came
from local reversing and local reproduction.
Dead Ends
__free_hookoverwrite: closed because free does not dispatch through
it in the bundled glibc.
- Single-entry tcache poisoning: closed because count handling prevented
the forged pointer from being returned.
- Early FILE payloads: closed until stderr lock and wide-vtable layout
were corrected.
Tool Quirks
- Docker Desktop had to be running before local Linux validation could
start.
- The challenge's funkify routine changes alphabetic bytes by index, so
exploit payloads need transform-safe values or repair writes.
Evidence Paths
analysis/checksec.txtanalysis/static-map.txtanalysis/source-audit.mdanalysis/fsop-attempt-notes.mdanalysis/solve-local-run.erranalysis/solve-remote-run.errsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Funkynator
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
- Remote instance: <TARGET>:30596
- Start time: 2026-06-10T20:57:30Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a16ef757-0bb9-4c0c-a9ce-cb67b6335148.zip | 1009939 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 8 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-10T20:57:30Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-10T20:57:42Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-10T20:59:45Z | hypothesis recorded | hypothesis-board.md | Menu-driven heap/memory-slot exploitation: use save/view/delete/continue plus byte overwrite primitive to leak and corrupt allocator/libc state, then gain code execution. | Medium | Run local/static ELF triage, collect remote interaction transcript, then reproduce menu states before exploit gate. |
| 2026-06-10T21:01:11Z | RAG query | analysis/rag/rag-query-20260610T210049907663Z-b581858b.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-10T21:01:11Z | checkpoint recorded | analysis/checkpoint-triage-20260610T210111150513Z-e4bb1e0e.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-10T21:02:06Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-10T21:02:43Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-10T21:06:35Z | local memory search | analysis/research/local-memory-search-20260610T210635752635Z-b86bb733.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-10T21:06:56Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-10T21:07:47Z | evaluator | analysis/evaluator-20260610T210747181789Z-d1133e0a.md | Proceed | High | Build a solver that first validates heap/libc leak behavior, then performs tcache poisoning to __free_hook only if the expected leak and allocator conditions are observed. |
| 2026-06-10T22: <REDACTED> | |||||
| 2026-06-10T22:04:14Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The binary is a stripped PIE x86-64 heap challenge with bundled glibc
and strong mitigations: Full RELRO, stack canary, NX, and PIE.
- The core primitive is the continue-processing submenu byte overwrite:
arbitrary offset, one byte, no bounds check against the current heap
message allocation.
- The exploit path used an unsorted-bin leak for libc base, a tcache
safe-linking key leak, two-entry tcache poisoning, and an overlapping
write onto _IO_2_1_stderr_.
__free_hookwas rejected as a dead branch for this bundled glibc:
the symbol exists, but the active allocator path does not call it.
- One-entry tcache poisoning was rejected as incomplete because the
per-bin tcache count reached zero before the forged pointer could be
returned.
- The final solver writes a modern FILE payload, repairs bytes affected
by the funkify transformation, exits to trigger the payload, and reads
flag.txt.
- Remote flag capture succeeded; raw flag remains only in
loot/flag.txt.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Technical analogy
How to remember this solve
Think of the challenge as a small system with one rule that matters more than the rest. The solve is finding that rule, validating it, and using it carefully enough to reach the final proof.
For Funkynator, keep the mental model simple: identify the trusted assumption, prove it with the smallest safe test, then automate or repeat only the part that directly leads to the flag.