Challenge / Crypto

POPO

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

MediumPublished 2024-04-21Sanitized local writeup

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.

POPO sanitized attack graph

Walkthrough flow

01

Challenge parameters

02

Weak assumption

03

Recovered secret state

04

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.

95% 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.

  • 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:

text
(n + 1)^m mod n^2 = 1 + m*n mod n^2

So if gm can be isolated, the plaintext is simply:

text
FLAG = ((gm - 1) // n) mod n

The 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:

  1. Run encrypt(1) once to set optim = 1.
  2. Run encrypt(1) again to recover randomizer = r^n mod n^2.
  3. Run encrypt(0) to recover masked_gm = gm * randomizer mod n^2.
  4. Compute gm = masked_gm * inverse(randomizer, n^2) mod n^2.
  5. Decode long_to_bytes(((gm - 1) // n) % n).

Local algebra validation:

bash
python3 Crypto/POPO/solve/solve.py --self-test

Remote solve was run through the harness wrapper:

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

The 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 + 1 makes plaintext recovery trivial if g^m is

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

FileSizeSHA256TypeNotes
files/a12c739b-3be6-4916-9a16-f366811566b2.zip1414<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 2 shown in artifact inventory JSON
files/extracted/crypto_popo/server.py3158<hash redacted>Python script text executable, Unicode text, UTF-8 text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-12T15:18:50Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-12T15:18:50Zartifact inventoryanalysis/artifact-inventory.json2 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-12T15:22:45Zhypothesis recordedhypothesis-board.mdRecover 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.HighRun solve.py --self-test, then run gated solve.py against remote.
2026-06-12T15:22:45Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-12T15:22:45Zresearch recordanalysis/research/research-records.mdResearch tagged MATCHEDMediumValidate against current evidence
2026-06-12T15:23:06ZRAG queryanalysis/rag/rag-query-20260612T152255812110Z-94cdcdf1.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-12T15:23:06Zcheckpoint recordedanalysis/checkpoint-analysis-20260612T152306417742Z-be73f410.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-12T15:23:40Zinstrumentation plananalysis/instrumentation-plan.mdRecover flag with exactly three encryptions and local algebra, no factoring or brute force.HighStop after one failed candidate format or non-invertible randomizer; reconnect once only if randomizer is non-invertible.
2026-06-12T15:23:52Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-12T15:24:18ZRAG recordanalysis/rag-records.mdRetrieved memory tagged MISSINGMediumValidate or reject with live evidence
2026-06-12T15:24:27Zevaluatoranalysis/evaluator-20260612T152427455451Z-cff31be7.mdProceedHighRun solve.py against remote through challenge_exec.
2026-06-12T15:25:02Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-12T15:25:45Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

-

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

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Recover 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.HighActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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.