DEFCON Quals 2021 - threefactooorx

An attachment was given as an Chrome extension. Unzip the extension and import it to Chrome, I got this.

In the folder I discovered a javascript file named content_script.js. After beautifully printed it using Chrome, I got the code easier to read slightly and could find keyword FLAG in it. Besides, many constant seems to be converted to function(num, num, num) just like document[_0x39523f(-0x19a, -0x187, -0x194, -0x17d)]. After pasting the functions definition it needed to console, we can finally know _0x39523f(-0x19a, -0x187, -0x194, -0x17d) refers to the string “body”. By this way, we can convert and extract the critical code. After reading the code carefully, wo got the code as follow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
function FetchTestConstructor() {
return !document["constructor"]["constructor"]('^([^ ]+( +[^\x20]+)+)+[^ ]}')['test'](FetchTestConstructor);
}

FetchTestConstructor();

/// ============= this area is code that I haven't figured out what it's doing =============
// function (_0x3f763e, _0xe07374) {
// if (_0xe07374) {
// const _0x11ee12 = _0xe07374["apply"](_0x3f763e, 'arguments');
// return _0xe07374 = null, _0x11ee12;
// }
// return _0x4bed5c = false, _0x22c111;
// }
/// ============= this area is code that I haven't figured out what it's doing =============

function ConsoleOuputRemove() {
const messages = {};
messages["getflag"] = 'true';
chrome['runtime']["sendMessage"](messages, function (e) {
console["log"]('flag:' + e["flag"]);
nodesadded === 5 && nodesdeleted === 3 && attrcharsadded === 23 && domvalue === 2188 && (document["getElementById"]('div')["value"] = e['flag']);
const divArray = document["createElement"]('div');
divArray["setAttribute"]('id', 'processed');
document['body']["appendChild"](divArray);
});
const _console = window["console"] = window["console"] || {};
let consoleMethods = ['log', 'warn', 'info', 'error', 'exception', 'table', 'trace'];
let consoleMethodBind;
let bindChoice;
for (let consoleMethod = 0; consoleMethod < consoleMethods['length']; consoleMethod++) {
const selfBind = ConsoleOuputRemove["constructor"]["prototype"]["bind"](ConsoleOuputRemove);
consoleMethodBind = consoleMethods[consoleMethod];
bindChoice = _console[consoleMethodBind] || selfBind;
selfBind["__proto__"] = ConsoleOuputRemove["bind"](ConsoleOuputRemove);
selfBind["toString"] = bindChoice["toString"]["bind"](bindChoice);
_console[consoleMethodBind] = selfBind;
}
}

ConsoleOuputRemove();

console['log']('Hello from OOO');
let FLAG = '';
domvalue = '';
document['addEventListener']('DOMContentLoaded', () => {
document["body"]["style"]['backgroundColor'] = 'green';
domvalue = check_dom();
console["log"]('Final\x20DOM\x20' + 'value:\x20' + domvalue);
}
);

function getDepth(node) {
depth = -1;
while (node != null) {
depth++;
node = node["parentNode"];
}
return depth;
}

function check_dom() {
var _3fa_elements = document["getElementById"]('3fa');
let chilen = _3fa_elements["querySelectorAll"]('*')["length"];
let maxdepth = 0;
let total_attributes = _3fa_elements["attributes"]["length"];
let _depth;
for (let element of _3fa_elements["querySelectorAll"]('*')) {
_depth = getDepth(element);
if (_depth > maxdepth)
maxdepth = _depth;
if (element['attributes'])
total_attributes += element["attributes"]["length"];
}
let specificid = 0;
document["querySelector"]([tost = "1"]) != null && (specificid = 1);
let token = 0;
_3fa_elements["innerHTML"]["length"] = chilen + maxdepth + total_attributes + totalchars + specificid + token;
let totalchars = _3fa_elements["innerHTML"]["length"];
return totalchars;
}

let nodesadded = 0;
let nodesdeleted = 0;
let attrcharsadded = 0;
const config = {};
config['attributes'] = true;
config['childList'] = true;
config['subtree'] = true;
const callback = function (elements) {
for (const element of elements) {
document["getElementById"]('3fa');
if (element['type'] === 'childList') {
nodesadded += element["addedNodes"]["length"];
nodesdeleted += element["removedNodes"]["length"];
} else if (element["type"] === 'attributes') {
attrcharsadded += element["attributeName"]["length"];
}
}
}
console['log']('Nodes\x20added:' + nodesadded);
console["log"]('Nodes\x20deleted:' + nodesdeleted);
console["log"]('Attribute chars added:' + attrcharsadded);

observer = new MutationObserver(callback);
observer['observe'](document, config);
console['log']('The\x20observer is observing.');
setTimeout(function () {
const messages = {};
messages["getflag"] = true;
chrome["runtime"]["sendMessage"](messages, function (e) {
FLAG = e["flag"];
console['log']('flag:' + e["flag"]);
nodesadded === 5 && nodesdeleted === 3 && attrcharsadded === 23 && domvalue === 2188 && (document['getElementById']('thirdfactooor')['value'] = e["flag"]);
const divArray = document["createElement"]('div');
divArray["setAttribute"]('id', 'processed');
document["body"]["appendChild"](divArray);
});
}, 500);

From the code I know that all we need is to fulfill the conditions nodesadded === 5 && nodesdeleted === 3 && attrcharsadded === 23 && domvalue === 2188. I can figure out that nodesadded increases when creating an div element, nodesdeleted increases when removing an div element. domvalue is the chars total in div with id 3fa. attrcharsadded is the total length of attribute name in the context. However, domvalue is not only the length of document["getElementById"]('3fa').innerHTML.length. After debugging in the browser, I found document["getElementById"]('3fa').innerHTML.length should be 2131, which actually declined 8 chars in total I expected it should be before.

After fulfilled all the conditions above, the script will try to set the value of a element whose id is thirdfactooor as flag. So I just create a textarea for the flag. Besides, use an input can also reach the answer, but domvalue should be recalculate to fit 2188. Also, a element which is not div to be create in the 3fa context is also allowed only if the domvalue is correct.

Therefore, we can create a HTML page like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<html>
<head>
<script>
ExecutePayload = function(){
let context = document.getElementById("3fa");
let div1 = document.createElement("div");
let div2 = document.createElement("div");
let div3 = document.createElement("div");
let div4 = document.createElement("div");
let div5 = document.createElement("div");

// nodesadded
context.append(div1);
context.append(div2);
context.append(div3);
context.append(div4);
context.append(div5);

// nodesdeleted
context.removeChild(div1);
context.removeChild(div2);
context.removeChild(div3);

// attrcharsadded & domvalue
div4.setAttribute("S".repeat(23), "D".repeat(2131));
}
</script>
</head>
<body>
<textarea id="thirdfactooor" rows="2" cols="100"></textarea>
<div id="3fa"></div>
<script>ExecutePayload();</script>
</body>
</html>

Also, a payload like this is also allowed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<html>
<head>
<script>
ExecutePayload = function(){
let context = document.getElementById("3fa");
let div1 = document.createElement("a");
let div2 = document.createElement("a");
let div3 = document.createElement("a");
let div4 = document.createElement("a");
let div5 = document.createElement("a");

// nodesadded
context.append(div1);
context.append(div2);
context.append(div3);
context.append(div4);
context.append(div5);

// nodesdeleted
context.removeChild(div1);
context.removeChild(div2);
context.removeChild(div3);

// attrcharsadded & domvalue
div4.setAttribute("S".repeat(23), "D".repeat(802));
}
</script>
</head>
<body>
<input id="thirdfactooor" style="width: 500px;height: 25px;"></input>
<div id="3fa"></div>
<script>ExecutePayload();</script>
</body>
</html>

1
OOO{themorefactorsthebetter_butyouneedatleastthree}