POPO
POPO is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
POPO attack path
POPO 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 Crypto evidence, validation, and reusable operator lessons.
Walkthrough flow
Challenge parameters
Weak assumption
Recovered secret state
Proof captured
Source coverage
High source coverage
Status: complete. This article is generated from 4 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.
- Crypto/POPO/writeup.md
- htb-challenge/Crypto/POPO/notes.md
- htb-challenge/Crypto/POPO/memory-summary.md
- htb-challenge/Crypto/POPO/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: POPO
- Category: Crypto
- Difficulty: Medium
- Mode: hybrid
Summary
POPO is a Paillier-style service with two state bugs. The constructor computes
gm = (n + 1)^FLAG mod n^2 before replacing g with a random value, and the
encryption oracle reuses one fixed randomizer unless a caller provides another
one. The optimizer state also makes the second positive non-flag encryption use
the raw message as the local value. Querying encrypt(1), encrypt(1), and
encrypt(0) exposes r^n and gm * r^n, which is enough to recover gm and
decode the flag without factoring n.
Artifact Inventory
files/a12c739b-3be6-4916-9a16-f366811566b2.zip: original challenge archive.files/extracted/crypto_popo/server.py: challenge service source.- Remote service:
<TARGET>:31338. - Inventory details are recorded in
analysis/artifact-inventory.json.
Analysis
The key source behavior is documented in analysis/source-audit.md.
The constructor starts with g = n + 1, computes gm = pow(g, FLAG, n2), and
only afterward enters the validation loop that randomizes g. For Paillier's
special generator n + 1, the binomial expansion gives:
(n + 1)^m mod n^2 = 1 + m*n mod n^2So if gm can be isolated, the plaintext is simply:
FLAG = ((gm - 1) // n) mod nThe oracle normally masks the local value with r^n mod n^2. Because self.r
is fixed and reused, the same mask appears across default encryptions. The
optimizer bug makes the second positive non-flag encryption return m * r^n
instead of g^m * r^n; using m = 1 leaks the mask directly. Then m = 0
takes the flag branch and returns gm * r^n.
The private RAG query did not contain a POPO-specific memory match and was
recorded as missing in analysis/rag-records.md; the proceed decision is based
on direct source review, the local self-test, and the live remote transcript.
Solve
solve/solve.py performs the full recovery:
- Run
encrypt(1)once to setoptim = 1. - Run
encrypt(1)again to recoverrandomizer = r^n mod n^2. - Run
encrypt(0)to recovermasked_gm = gm * randomizer mod n^2. - Compute
gm = masked_gm * inverse(randomizer, n^2) mod n^2. - Decode
long_to_bytes(((gm - 1) // n) % n).
Local algebra validation:
python3 Crypto/POPO/solve/solve.py --self-testRemote solve was run through the harness wrapper:
python3 scripts/challenge_exec.py Crypto/POPO -- python3 Crypto/POPO/solve/solve.py \
--host <TARGET> \
--port 31338 \
--output Crypto/POPO/analysis/flag-candidate.txt \
--transcript Crypto/POPO/analysis/remote-solve-transcript.txtThe flag candidate was captured with challenge_harness.py capture-flag and the
temporary analysis candidate was removed.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- In Paillier, using
g = n + 1makes plaintext recovery trivial ifg^mis
exposed in isolation.
- Precomputing a flag-dependent value before finalizing parameters can leak the
protected plaintext even if later parameters look randomized.
- Stateful optimizer shortcuts are dangerous in cryptographic code, especially
when combined with reused randomness.
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: POPO
- Category: Crypto
- Difficulty: Medium
- Mode: hybrid
- Remote instance: <TARGET>:31338
- Start time: 2026-06-12T15:18:50Z
- 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/a12c739b-3be6-4916-9a16-f366811566b2.zip | 1414 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 2 shown in artifact inventory JSON |
files/extracted/crypto_popo/server.py | 3158 | <hash redacted> | Python script text executable, Unicode text, UTF-8 text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-12T15:18:50Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-12T15:18:50Z | artifact inventory | analysis/artifact-inventory.json | 2 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-12T15:22:45Z | hypothesis recorded | hypothesis-board.md | Recover fixed Paillier randomizer via optimizer state, strip it from encryption of 0 to recover gm=(n+1)^FLAG mod n^2, then decode FLAG=(gm-1)//n. | High | Run solve.py --self-test, then run gated solve.py against remote. |
| 2026-06-12T15:22:45Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-12T15:22:45Z | research record | analysis/research/research-records.md | Research tagged MATCHED | Medium | Validate against current evidence |
| 2026-06-12T15:23:06Z | RAG query | analysis/rag/rag-query-20260612T152255812110Z-94cdcdf1.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-12T15:23:06Z | checkpoint recorded | analysis/checkpoint-analysis-20260612T152306417742Z-be73f410.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-12T15:23:40Z | instrumentation plan | analysis/instrumentation-plan.md | Recover flag with exactly three encryptions and local algebra, no factoring or brute force. | High | Stop after one failed candidate format or non-invertible randomizer; reconnect once only if randomizer is non-invertible. |
| 2026-06-12T15:23:52Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-12T15:24:18Z | RAG record | analysis/rag-records.md | Retrieved memory tagged MISSING | Medium | Validate or reject with live evidence |
| 2026-06-12T15:24:27Z | evaluator | analysis/evaluator-20260612T152427455451Z-cff31be7.md | Proceed | High | Run solve.py against remote through challenge_exec. |
| 2026-06-12T15:25:02Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-12T15:25:45Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
-
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: Crypto
- Challenge: POPO
- 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.
Reusable Lessons
-
Dead Ends
-
Tool Quirks
-
Evidence Paths
-
Ingestion Decision
- Proposed for LightRAG: yes/no
- 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 | Recover fixed Paillier randomizer via optimizer state, strip it from encryption of 0 to recover gm=(n+1)^FLAG mod n^2, then decode FLAG=(gm-1)//n. | server.py computes gm before g randomization while g=n+1; anonymize reuses fixed r; second encryption of positive non-flag message returns m*r^n when optim=1; m=0 uses gm branch. | Need local self-test and one bounded remote solve. | Run solve.py --self-test, then run gated solve.py against remote. | High | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Technical analogy
How to remember this solve
Think of the challenge like a locked box where the lock is mathematical but slightly flawed. The goal is not to smash the box; it is to notice which part of the lock repeats, leaks, or trusts the wrong assumption.
For POPO, 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.