cd /news/ai-crawlers/solving-anubis-s-sha-256-proof-of-wo… · home › topics › ai-crawlers › article
[ARTICLE · art-148238] src=gist.github.com ↗ pub= topic=ai-crawlers verified=true sentiment=· neutral

Solving Anubis's SHA-256 Proof-of-Work Challenge

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.

by read3 min views2 publishedOct 9, 2026

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.

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

HTTP 200
Content-Type: text/html

<!doctype html>
<html lang="en">
  <head>
    <script id="anubis_version" type="application/json">"v1.26.4"</script>
    <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>
    <script id="anubis_base_prefix" type="application/json">""</script>
    <script id="anubis_public_url" type="application/json">""</script>
    ...
  </head>
  ...
</html>

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

The #anubis_challenge element's text content, parsed as JSON:

{
  "rules": { "algorithm": "fast", "difficulty": 6 },
  "challenge": {
    "id": "<uuid>",
    "randomData": "<hex string, ~128 chars>",
    "difficulty": 6
  }
}

The task: find the smallest non-negative integer nonce such that

SHA256(randomData_as_utf8_bytes + nonce.to_string().as_bytes())

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

A plain GET (not a POST — the browser does window.location.replace(...), so query params alone are enough) to a fixed path:

GET {origin}/.within.website/x/cmd/anubis/api/pass-challenge
    ?id=<challenge.id>
    &response=<hex_hash>
    &nonce=<nonce>
    &redir=<original request URL, url-encoded>
    &elapsedTime=<ms spent solving>

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

use sha2::{Digest, Sha256};

/// Returns (nonce, hex_hash).
fn solve_anubis_pow(random_data: &str, difficulty: usize) -> (u64, String) {
    let zero_bytes = difficulty / 2;
    let odd_nibble = difficulty % 2 != 0;
    let mut nonce: u64 = 0;
    loop {
        let mut hasher = Sha256::new();
        hasher.update(random_data.as_bytes());
        hasher.update(nonce.to_string().as_bytes());
        let digest = hasher.finalize();

        let leading_zero_bytes_ok = digest[..zero_bytes].iter().all(|&b| b == 0);
        let next_nibble_ok = !odd_nibble || (digest[zero_bytes] >> 4) == 0;

        if leading_zero_bytes_ok && next_nibble_ok {
            let hex_hash = digest.iter().map(|b| format!("{b:02x}")).collect::<String>();
            return (nonce, hex_hash);
        }
        nonce += 1;
    }
}

Submission, with any HTTP client carrying a cookie jar (reqwest/ rquest with .cookie_store(true)):

let (nonce, response_hash) = solve_anubis_pow(&challenge.random_data, rules.difficulty);

let pass_response = client
    .get("https://TARGET/.within.website/x/cmd/anubis/api/pass-challenge")
    .query(&[
        ("id", challenge.id.as_str()),
        ("response", response_hash.as_str()),
        ("nonce", &nonce.to_string()),
        ("redir", original_request_url.as_str()),
        ("elapsedTime", &elapsed_ms.to_string()),
    ])
    .send()
    .await?;

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.

── more in #ai-crawlers 4 stories · sorted by recency
── more on @anubis 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/solving-anubis-s-sha…] indexed:0 read:3min 2026-10-09 · —