Protein Cookies
Protein Cookies is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Protein Cookies attack path
Protein Cookies 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
Extracted the provided Flask/Python 2 source bundle...
Found a custom login_info cookie containing...
Confirmed the secret length is fixed at 16 bytes in...
Observed that decoded cookie payloads are parsed with...
Implemented a self-contained SHA-512 length-extension...
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.
- Crypto/Protein-Cookies/writeup.md
- htb-challenge/Crypto/Protein-Cookies/notes.md
- htb-challenge/Crypto/Protein-Cookies/memory-summary.md
- htb-challenge/Crypto/Protein-Cookies/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Crypto__Protein-Cookies__memory-summary.md.59ab7db0dd.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Crypto__Protein-Cookies__notes.md.07bb4267e8.md
Technical Walkthrough
Writeup
Challenge
- Name: Protein-Cookies
- Category: Crypto
- Difficulty: Easy
- Mode: hybrid
Summary
Protein Cookies is a Flask app with a custom authenticated cookie. The cookie MAC is built as sha512(secret + payload), which is vulnerable to SHA-512 length extension. Because the payload parser accepts duplicate keys and keeps the last value, a forged cookie can append a second isLoggedIn=True parameter and access /program.
Artifact Inventory
Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.
- Remote service:
http://<TARGET>:31106. - Local source bundle includes the Flask application, Dockerfile, static assets, and
flag.pdf. - Relevant files:
- application/models.py: custom cookie creation and validation.
- application/util.py: guest cookie setup and login gate.
- application/blueprints/routes.py: /program sends flag.pdf behind verify_login.
Analysis
The app issues a guest cookie when login_info is absent. Its payload is:
username=guest&isLoggedIn=FalseThe signature is:
base64(sha512(secret + payload).hexdigest())The source fixes the secret length at 16 bytes. Since SHA-512 is Merkle-Damgard based, the known digest of secret || payload can be extended to a valid digest for:
secret || payload || sha512_padding || &isLoggedIn=TrueAfter base64-decoding the forged payload, Python 2 urlparse.parse_qs treats the appended key as a second isLoggedIn value. The app keeps v[-1], so the effective login state becomes True.
One implementation detail mattered during exploitation: sending the forged cookie through requests.Session(..., cookies=...) can leave the original guest cookie in the jar. The working solver sends a manually constructed HTTP cookie header containing only the forged value.
Solve
Run:
python3 solve/solve.pyThe script:
- Fetches a fresh guest cookie.
- Performs SHA-512 length extension with the known 16-byte secret length.
- Appends
&isLoggedIn=True. - Sends the forged cookie to
/program. - Saves the returned PDF to
loot/program.pdf. - Extracts the HTB flag from the PDF text.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
hash(secret || message)is not a safe MAC for Merkle-Damgard hashes.- Duplicate query parameters can turn a length-extension primitive into an auth bypass when the parser keeps the last value.
- Cookie jar behavior can invalidate a correct exploit if the original cookie is sent alongside the forged one.
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: Protein-Cookies
- Category: Crypto
- Difficulty: Easy
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-10T09:42:26Z
- 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/a12c7331-260f-4f58-aac8-c0c5933eda11.zip | 2268362 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 33 shown in artifact inventory JSON |
files/extracted/crypto_protein_cookies/Dockerfile | 574 | <hash redacted> | ASCII text | |
files/extracted/crypto_protein_cookies/build_docker.sh | 179 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/__init__.py | 0 | <hash redacted> | empty | |
files/extracted/crypto_protein_cookies/challenge/application/app.py | 636 | <hash redacted> | Python script text executable, ASCII text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/blueprints/__init__.py | 0 | <hash redacted> | empty | |
files/extracted/crypto_protein_cookies/challenge/application/blueprints/routes.py | 844 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/config.py | 209 | <hash redacted> | ASCII text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/flag.pdf | 47147 | <hash redacted> | PDF document, version 1.4, 1 pages | |
files/extracted/crypto_protein_cookies/challenge/application/models.py | 1269 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/static/favicon.png | 105798 | <hash redacted> | PNG image data, 300 x 300, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/card.png | 1748 | <hash redacted> | PNG image data, 48 x 48, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/logo.png | 189644 | <hash redacted> | PNG image data, 900 x 308, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/promo.png | 948421 | <hash redacted> | PNG image data, 1920 x 660, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr1.jpg | 268643 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 490x490, components 3 | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr2.jpg | 258795 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 490x490, components 3 | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr3.png | 446751 | <hash redacted> | PNG image data, 490 x 490, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/js/main.js | 1081 | <hash redacted> | ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/templates/index.html | 6631 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/templates/login.html | 4818 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/templates/register.html | 6044 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/util.py | 768 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/requirements.txt | 5 | <hash redacted> | ASCII text, with no line terminators | |
files/extracted/crypto_protein_cookies/challenge/run.py | 99 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/config/supervisord.conf | 270 | <hash redacted> | ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-10T09:42:26Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-10T09:42:26Z | artifact inventory | analysis/artifact-inventory.json | 25 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-10T09:43:43Z | hypothesis recorded | hypothesis-board.md | Forge login_info with SHA-512 length extension to append a second isLoggedIn=True parameter, then request /program. | Medium | Use a locally tested SHA-512 length-extension implementation against a known secret, then apply it to a live guest cookie. |
| 2026-06-10T09:43:43Z | research record | analysis/research/research-records.md | Research tagged MATCHED | Medium | Validate against current evidence |
| 2026-06-10T09:43:44Z | checkpoint recorded | analysis/checkpoint-hypothesis_ready-20260610T094344016843Z-5cffec02.md | Checkpoint for <secret redacted> | High | Use checkpoint to drive next decision |
| 2026-06-10T09:43:44Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-10T09:43:44Z | evaluator | analysis/evaluator-20260610T094344126612Z-dfb0007e.md | Proceed | High | Implement local length-extension forge, fetch guest cookie, request /program, and extract flag from returned PDF. |
| 2026-06-10T09:43:55Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-10T09:46:18Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-10T09:47:46Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Remote target:
http://<TARGET>:31106. - The application is Python 2 Flask and sets a guest
login_infocookie on first request. models.pyimplements a custom cookie MAC assha512(secret + payload)with a 16-byte random prefix.- The cookie format is
base64(payload).base64(sha512(secret + payload).hexdigest()). - The decoded payload is parsed with
urlparse.parse_qs, and duplicate keys resolve to the last value throughv[-1]. - A SHA-512 length extension can append
&isLoggedIn=Trueto the guest payload while preserving MAC validity. - The forged cookie must be sent in a manually constructed HTTP cookie header; using a
requests.Sessioncookie jar can merge/prefer the original guest cookie. - The forged request to
/programreturns a PDF, and the HTB flag was extracted from that PDF intoloot/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: Crypto
- Challenge: Protein-Cookies
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Extracted the provided Flask/Python 2 source bundle and inventoried the app.
- Found a custom
login_infocookie containingbase64(payload).base64(sha512(secret + payload).hexdigest()). - Confirmed the secret length is fixed at 16 bytes in source.
- Observed that decoded cookie payloads are parsed with
parse_qs, and duplicate keys resolve to the last value. - Implemented a self-contained SHA-512 length-extension forge to append a second logged-in parameter.
- Sent the forged cookie in a manually constructed HTTP cookie header to avoid session-cookie merging.
- Downloaded the protected PDF from
/programand extracted the HTB flag.
Reusable Lessons
- Prefix MACs using Merkle-Damgard hashes are length-extension vulnerable.
- Parser behavior such as duplicate-key handling can be the second half of an auth bypass.
- Raw cookie headers may be necessary when a client library maintains an original session cookie.
- For PDF-backed flags, save the returned PDF under
loot/and extract text withpypdf.
Dead Ends
- Login and registration APIs intentionally return errors and were not needed.
- Using
requests.Sessionwithcookies={...}still sent or preferred the original guest cookie; a manually constructed HTTP cookie header was required.
Tool Quirks
- Local
hashpumpy,hlextend, andpymd5modules were unavailable, so the solver implements SHA-512 compression directly. pypdfwas available and worked for extracting text from the downloaded PDF.
Evidence Paths
files/extracted/crypto_protein_cookies/challenge/application/models.pyfiles/extracted/crypto_protein_cookies/challenge/application/util.pyfiles/extracted/crypto_protein_cookies/challenge/application/blueprints/routes.pyanalysis/source-audit.mdanalysis/forge-debug.txtsolve/solve.pyloot/program.pdfloot/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 | Forge login_info with SHA-512 length extension to append a second isLoggedIn=True parameter, then request /program. | models.py uses sha512(secret + payload), source defines secret: <redacted> and validate_login returns the last parse_qs value for isLoggedIn. | Use a locally tested SHA-512 length-extension implementation against a known secret, then apply it to a live guest cookie. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Crypto
- Challenge: Protein-Cookies
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Extracted the provided Flask/Python 2 source bundle and inventoried the app.
- Found a custom
login_infocookie containingbase64(payload).base64(sha512(secret + payload).hexdigest()). - Confirmed the secret length is fixed at 16 bytes in source.
- Observed that decoded cookie payloads are parsed with
parse_qs, and duplicate keys resolve to the last value. - Implemented a self-contained SHA-512 length-extension forge to append a second logged-in parameter.
- Sent the forged cookie in a manually constructed HTTP cookie header to avoid session-cookie merging.
- Downloaded the protected PDF from
/programand extracted the HTB flag.
Reusable Lessons
- Prefix MACs using Merkle-Damgard hashes are length-extension vulnerable.
- Parser behavior such as duplicate-key handling can be the second half of an auth bypass.
- Raw cookie headers may be necessary when a client library maintains an original session cookie.
- For PDF-backed flags, save the returned PDF under
loot/and extract text withpypdf.
Dead Ends
- Login and registration APIs intentionally return errors and were not needed.
- Using
requests.Sessionwith `cookies= <REDACTED>
Tool Quirks
- Local
hashpumpy,hlextend, andpymd5modules were unavailable, so the solver implements SHA-512 compression directly. pypdfwas available and worked for extracting text from the downloaded PDF.
Evidence Paths
files/extracted/crypto_protein_cookies/challenge/application/models.pyfiles/extracted/crypto_protein_cookies/challenge/application/util.pyfiles/extracted/crypto_protein_cookies/challenge/application/blueprints/routes.pyanalysis/source-audit.mdanalysis/forge-debug.txtsolve/solve.pyloot/program.pdfloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Protein-Cookies
- Category: Crypto
- Difficulty: Easy
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-10T09:42:26Z
- 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/a12c7331-260f-4f58-aac8-c0c5933eda11.zip | 2268362 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 33 shown in artifact inventory JSON |
files/extracted/crypto_protein_cookies/Dockerfile | 574 | <hash redacted> | ASCII text | |
files/extracted/crypto_protein_cookies/build_docker.sh | 179 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/__init__.py | 0 | <hash redacted> | empty | |
files/extracted/crypto_protein_cookies/challenge/application/app.py | 636 | <hash redacted> | Python script text executable, ASCII text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/blueprints/__init__.py | 0 | <hash redacted> | empty | |
files/extracted/crypto_protein_cookies/challenge/application/blueprints/routes.py | 844 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/config.py | 209 | <hash redacted> | ASCII text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/flag.pdf | 47147 | <hash redacted> | PDF document, version 1.4, 1 pages | |
files/extracted/crypto_protein_cookies/challenge/application/models.py | 1269 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/static/favicon.png | 105798 | <hash redacted> | PNG image data, 300 x 300, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/card.png | 1748 | <hash redacted> | PNG image data, 48 x 48, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/logo.png | 189644 | <hash redacted> | PNG image data, 900 x 308, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/promo.png | 948421 | <hash redacted> | PNG image data, 1920 x 660, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr1.jpg | 268643 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 490x490, components 3 | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr2.jpg | 258795 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 490x490, components 3 | |
files/extracted/crypto_protein_cookies/challenge/application/static/images/tr3.png | 446751 | <hash redacted> | PNG image data, 490 x 490, 8-bit/color RGBA, non-interlaced | |
files/extracted/crypto_protein_cookies/challenge/application/static/js/main.js | 1081 | <hash redacted> | ASCII text | |
files/extracted/crypto_protein_cookies/challenge/application/templates/index.html | 6631 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/templates/login.html | 4818 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/templates/register.html | 6044 | <hash redacted> | HTML document text, Unicode text, UTF-8 text, with CRLF line terminators | |
files/extracted/crypto_protein_cookies/challenge/application/util.py | 768 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/challenge/requirements.txt | 5 | <hash redacted> | ASCII text, with no line terminators | |
files/extracted/crypto_protein_cookies/challenge/run.py | 99 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/crypto_protein_cookies/config/supervisord.conf | 270 | <hash redacted> | ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-10T09:42:26Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-10T09:42:26Z | artifact inventory | analysis/artifact-inventory.json | 25 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-10T09: <REDACTED>, then request /program. | Medium | Use a locally tested SHA-512 length-extension implementation against a known secret, then apply it to a live guest cookie. | |||
| 2026-06-10T09:43:43Z | research record | analysis/research/research-records.md | Research tagged MATCHED | Medium | Validate against current evidence |
| 2026-06-10T09:43:44Z | checkpoint recorded | analysis/checkpoint-hypothesis_ready-20260610T094344016843Z-5cffec02.md | Checkpoint for <secret redacted> | High | Use checkpoint to drive next decision |
| 2026-06-10T09:43:44Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-10T09: <REDACTED>, fetch guest cookie, request /program, and extract flag from returned PDF. | |||||
| 2026-06-10T09:43:55Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-10T09: <REDACTED> | |||||
| 2026-06-10T09:47:46Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Remote target:
http://<TARGET>:31106. - The application is Python 2 Flask and sets a guest
login_infocookie on first request. models.pyimplements a custom cookie MAC assha512(secret + payload)with a 16-byte random prefix.- The cookie format is
base64(payload).base64(sha512(secret + payload).hexdigest()). - The decoded payload is parsed with
urlparse.parse_qs, and duplicate keys resolve to the last value throughv[-1]. - A SHA-512 length extension can append
&isLoggedIn=Trueto the guest payload while preserving MAC validity. - The forged cookie must be sent in a manually constructed HTTP cookie header; using a
requests.Sessioncookie jar can merge/prefer the original guest cookie. - The forged request to
/programreturns a PDF, and the HTB flag was extracted from that PDF intoloot/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 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 Protein Cookies, 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.