{"slug": "solving-anubis-s-sha-256-proof-of-work-challenge", "title": "Solving Anubis's SHA-256 Proof-of-Work Challenge", "summary": "A developer documented how to solve the SHA-256 proof-of-work challenge used by Anubis, the open-source reverse-proxy anti-bot gate deployed by sites including Startpage, entirely server-side without a headless browser. The writeup derives the algorithm from Anubis's public source, showing that at difficulty 6 the winning nonce typically lands under 1000 and the search completes in low single-digit milliseconds, and notes the deployment-specific verification cookie must be captured and replayed on the pass-challenge request.", "body_md": "Anubis (`TecharoHQ/anubis`) is an open-source reverse-proxy anti-bot gate an increasing number of sites put in front of themselves (Startpage among them). When it decides to challenge a client, it serves a page embedding a PoW puzzle that the browser's JS solves in a Web Worker, then replays the answer as a query string against a fixed API path. Replicating that server-side unlocks the real page instead of the \"Just a moment...\" wait screen — no headless browser needed.\n\nEverything below is derived directly from Anubis's own public source (`lib/challenge/proofofwork/proofofwork.go` for the server-side validator, `web/js/worker/sha256.ts` and `wasm/pow/sha256/src/lib.rs` for the two reference client implementations, which agree exactly) — not reverse-engineered from obfuscated code.\n\n```\nHTTP 200\nContent-Type: text/html\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script id=\"anubis_version\" type=\"application/json\">\"v1.26.4\"</script>\n    <script id=\"anubis_challenge\" type=\"application/json\">{\"rules\":{\"algorithm\":\"fast\",\"difficulty\":6},\"challenge\":{\"issuedAt\":\"2026-10-09T10:58:18.167975774Z\",\"metadata\":{\"User-Agent\":\"...\",\"X-Real-Ip\":\"...\"},\"id\":\"01a12050-55b7-7ed8-ac01-8ca29fcefe62\",\"method\":\"fast\",\"randomData\":\"da5eb566735ccae8db7d506e96d18dbfb6790c4bcde8600cf0cd21e9e5c71d7ccd74745509dbfb71c97a74d33f5e4bab60ee58dd704ce318322516afbe9d7272\",\"policyRuleHash\":\"ac980f49c4d35fab\",\"difficulty\":6,\"spent\":false}}</script>\n    <script id=\"anubis_base_prefix\" type=\"application/json\">\"\"</script>\n    <script id=\"anubis_public_url\" type=\"application/json\">\"\"</script>\n    ...\n  </head>\n  ...\n</html>\n```\n\nThe response also sets a `Set-Cookie` header — a verification cookie whose name is deployment-specific (Anubis's own default differs from what a site may configure; Startpage uses `spchal-cookie-verification=<uuid>`). **This cookie must be captured and replayed on the solve request**, or the server rejects it outright.\n\nThe `#anubis_challenge` element's text content, parsed as JSON:\n\n```\n{\n  \"rules\": { \"algorithm\": \"fast\", \"difficulty\": 6 },\n  \"challenge\": {\n    \"id\": \"<uuid>\",\n    \"randomData\": \"<hex string, ~128 chars>\",\n    \"difficulty\": 6\n  }\n}\n```\n\nThe task: find the smallest non-negative integer `nonce` such that\n\n```\nSHA256(randomData_as_utf8_bytes + nonce.to_string().as_bytes())\n```\n\n— hex-encoded — has `difficulty` leading `'0'` hex digits. That's `difficulty / 2` leading zero bytes, plus one more leading-zero nibble if `difficulty` is odd (checking raw digest bytes rather than the hex string avoids re-encoding on every attempt). At `difficulty = 6`, the winning `nonce` typically lands under 1000 — the whole search takes low single-digit milliseconds.\n\nA plain `GET` (not a POST — the browser does `window.location.replace(...)`, so query params alone are enough) to a fixed path:\n\n```\nGET {origin}/.within.website/x/cmd/anubis/api/pass-challenge\n    ?id=<challenge.id>\n    &response=<hex_hash>\n    &nonce=<nonce>\n    &redir=<original request URL, url-encoded>\n    &elapsedTime=<ms spent solving>\n```\n\n— with the challenge page's `Set-Cookie` attached. The response is either the real page directly or a redirect to it; `base_prefix`/` public_url` are empty in practice, so `redir` is just the exact original URL and the path above is used unprefixed off the origin's root.\n\n```\nuse sha2::{Digest, Sha256};\n\n/// Returns (nonce, hex_hash).\nfn solve_anubis_pow(random_data: &str, difficulty: usize) -> (u64, String) {\n    let zero_bytes = difficulty / 2;\n    let odd_nibble = difficulty % 2 != 0;\n    let mut nonce: u64 = 0;\n    loop {\n        let mut hasher = Sha256::new();\n        hasher.update(random_data.as_bytes());\n        hasher.update(nonce.to_string().as_bytes());\n        let digest = hasher.finalize();\n\n        let leading_zero_bytes_ok = digest[..zero_bytes].iter().all(|&b| b == 0);\n        let next_nibble_ok = !odd_nibble || (digest[zero_bytes] >> 4) == 0;\n\n        if leading_zero_bytes_ok && next_nibble_ok {\n            let hex_hash = digest.iter().map(|b| format!(\"{b:02x}\")).collect::<String>();\n            return (nonce, hex_hash);\n        }\n        nonce += 1;\n    }\n}\n```\n\nSubmission, with any HTTP client carrying a cookie jar (`reqwest`/` rquest` with `.cookie_store(true)`):\n\n```\nlet (nonce, response_hash) = solve_anubis_pow(&challenge.random_data, rules.difficulty);\n\nlet pass_response = client\n    .get(\"https://TARGET/.within.website/x/cmd/anubis/api/pass-challenge\")\n    .query(&[\n        (\"id\", challenge.id.as_str()),\n        (\"response\", response_hash.as_str()),\n        (\"nonce\", &nonce.to_string()),\n        (\"redir\", original_request_url.as_str()),\n        (\"elapsedTime\", &elapsed_ms.to_string()),\n    ])\n    .send()\n    .await?;\n```\n\n**Gotcha**: if this runs inside a shared async codebase, don't hold an HTML-parser `Document`/` Selection` in scope across the `.await` on the solve-submit request. Some HTML parsers (e.g. `nipper`, built on `tendril`) aren't `Send` — a value merely left in scope at an `.await` point, even unused, poisons the whole generated future's `Send`-ness. Scope the parsing step in a block that ends before the await.", "url": "https://wpnews.pro/news/solving-anubis-s-sha-256-proof-of-work-challenge", "canonical_source": "https://gist.github.com/RomySaputraSihananda/3a2be0ba02b37c98c58efc9fcdd62f4c", "published_at": "2026-10-09 11:47:34+00:00", "updated_at": "2026-10-09 11:52:29.162358+00:00", "lang": "en", "topics": ["ai-crawlers", "developer-tools", "ai-infrastructure"], "entities": ["Anubis", "TecharoHQ", "Startpage"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/solving-anubis-s-sha-256-proof-of-work-challenge", "markdown": "https://wpnews.pro/news/solving-anubis-s-sha-256-proof-of-work-challenge.md", "text": "https://wpnews.pro/news/solving-anubis-s-sha-256-proof-of-work-challenge.txt", "jsonld": "https://wpnews.pro/news/solving-anubis-s-sha-256-proof-of-work-challenge.jsonld"}}