Challenge / Pwn

Funkynator

Funkynator is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator

MediumPublished 2025-03-23Sanitized local writeup

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.

Funkynator sanitized attack graph

Walkthrough flow

01

Reverse the menu model: saved heap pointers can be...

02

Arrange adjacent heap chunks so a previous message...

03

Reuse the adjacency pattern on a small freed chunk to...

04

Use a two-entry tcache bin before poisoning....

05

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.

100% coverage
Evidence verdict

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:

bash
cd <local workspace>
. .venv311/bin/activate
python solve/solve.py --host <TARGET> --port 30596 --output loot/flag.txt

Then capture through the harness:

bash
cd <local workspace>
python3 scripts/challenge_harness.py capture-flag Pwn/Funkynator --from loot/flag.txt

The 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

FileSizeSHA256TypeNotes
files/a16ef757-0bb9-4c0c-a9ce-cb67b6335148.zip1009939<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 8 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-10T20:57:30Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-10T20:57:42Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-10T20:59:45Zhypothesis recordedhypothesis-board.mdMenu-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.MediumRun local/static ELF triage, collect remote interaction transcript, then reproduce menu states before exploit gate.
2026-06-10T21:01:11ZRAG queryanalysis/rag/rag-query-20260610T210049907663Z-b581858b.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-10T21:01:11Zcheckpoint recordedanalysis/checkpoint-triage-20260610T210111150513Z-e4bb1e0e.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-10T21:02:06ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-10T21:02:43Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-10T21:06:35Zlocal memory searchanalysis/research/local-memory-search-20260610T210635752635Z-b86bb733.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-10T21:06:56Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-10T21:07:47Zevaluatoranalysis/evaluator-20260610T210747181789Z-d1133e0a.mdProceedHighBuild 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:45Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-10T22:04:14Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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_hook was 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:

bash
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.

  1. 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.

  1. Arrange adjacent heap chunks so a previous message can print into a

freed large chunk and leak the unsorted-bin pointer for libc base.

  1. Reuse the adjacency pattern on a small freed chunk to leak the tcache

safe-linking key.

  1. 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.

  1. Poison the freed chunk's encoded next pointer so a later allocation

overlaps _IO_2_1_stderr_.

  1. 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_hook remains 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_hook overwrite: 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.txt
  • analysis/static-map.txt
  • analysis/source-audit.md
  • analysis/fsop-attempt-notes.md
  • analysis/solve-local-run.err
  • analysis/solve-remote-run.err
  • solve/solve.py
  • loot/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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Menu-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.HighConfirmed

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition
__free_hook overwriteBundled libc symbol/disassembly and local exploit attemptsWrites landed, but free did not dispatch through the hookModern bundled glibc does not use the classic hook pathOnly revisit with a different libc that still calls the hook
One-entry tcache poisoningLocal allocator validation during solver developmentForged target was not returned after the real chunk was poppedPer-bin tcache count reached zero, so the forged pointer was ignoredOnly 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.

  1. 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.

  1. Arrange adjacent heap chunks so a previous message can print into a

freed large chunk and leak the unsorted-bin pointer for libc base.

  1. Reuse the adjacency pattern on a small freed chunk to leak the tcache

safe-linking key.

  1. 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.

  1. Poison the freed chunk's encoded next pointer so a later allocation

overlaps _IO_2_1_stderr_.

  1. 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_hook remains 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_hook overwrite: 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.txt
  • analysis/static-map.txt
  • analysis/source-audit.md
  • analysis/fsop-attempt-notes.md
  • analysis/solve-local-run.err
  • analysis/solve-remote-run.err
  • solve/solve.py
  • loot/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

FileSizeSHA256TypeNotes
files/a16ef757-0bb9-4c0c-a9ce-cb67b6335148.zip1009939<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 8 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-10T20:57:30Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-10T20:57:42Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-10T20:59:45Zhypothesis recordedhypothesis-board.mdMenu-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.MediumRun local/static ELF triage, collect remote interaction transcript, then reproduce menu states before exploit gate.
2026-06-10T21:01:11ZRAG queryanalysis/rag/rag-query-20260610T210049907663Z-b581858b.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-10T21:01:11Zcheckpoint recordedanalysis/checkpoint-triage-20260610T210111150513Z-e4bb1e0e.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-10T21:02:06ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-10T21:02:43Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-10T21:06:35Zlocal memory searchanalysis/research/local-memory-search-20260610T210635752635Z-b86bb733.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-10T21:06:56Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-10T21:07:47Zevaluatoranalysis/evaluator-20260610T210747181789Z-d1133e0a.mdProceedHighBuild 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:14Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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_hook was 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:

bash
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.