hsctf – accessible rich internet applications
(Part of a series of writeups from HSCTF 2019.)
The challenge text reads:
A very considerate fellow, Rob believes that accessibility is very important!
NOTE: The flag for this challenge starts with flag{ instead of hsctf{
The challenge links to a download of a file index.html , a "Magic Number Generator" powered by an obfuscated piece of JavaScript.
When unobfuscated (but otherwise unchanged), it looks like this.
var lookup = ['\x0b=\x22epduzqf!...snip...{f1=0ejw?=0ejw?', 'charCodeAt', 'write'];
(function(_lookup, counter) {
var fn = function(c) {
while (--c) {
_lookup['push'](_lookup['shift']());
}
};
fn(++counter);
}(lookup, 0xbd));
var get = function(hex) {
hex = hex - 0x0;
var value = lookup[hex];
return value;
};
var s = get('0x0');
m = '';
for (i = 0x0; i < s['length']; i++)
m += String['fromCharCode'](s['charCodeAt'](i) - 0x1);
document.write(m)
Standard obfuscation, but easily "defeated" by inspecting the rendered source in something as accessible as devtools.
At the bottom of the page is a set of 1040 binary bits, in an unknown order, each with its position in the set attached to it.
<div id="list" role="listbox">
<div role="option" aria-posinset="525" aria-setsize="1040">1</div>
<div role="option" aria-posinset="642" aria-setsize="1040">1</div>
<div role="option" aria-posinset="291" aria-setsize="1040">0</div>
<div role="option" aria-posinset="317" aria-setsize="1040">0</div>
<div role="option" aria-posinset="792" aria-setsize="1040">0</div>
<div role="option" aria-posinset="107" aria-setsize="1040">1</div>
<div role="option" aria-posinset="698" aria-setsize="1040">1</div>
<div role="option" aria-posinset="420" aria-setsize="1040">0</div>
<!-- ... -->
</div>
I made the assumption that this was an ASCII bitstream, and the calculation 1040/8 equating to a whole number gave me even more reason to believe it.
Since document.write is synchronous, any code afterwards can now read the
new
DOM
. Each bit needs to be read out and put at the right position in the set. The length is
read only once, because efficiency.
const list = document.querySelector('#list')
const set = {}
let len = null
for (const el of list.children) {
set[el.getAttribute('aria-posinset')] = +el.innerText
if (len === null) {
len = el.getAttribute('aria-setsize')
}
}
The following code loops through the bits in groups of 8, uses bit shifting to create a byte, then converts it to an ASCII character.
let flag = ''
for (let i = 0; i < len; i += 8) {
let char = 0;
for (let j = 8 - 1; j >= 0; --j) {
char = char | (set[i + j] << (8 - j - 1))
}
flag += String.fromCharCode(char)
}
console.log(flag)
Running this shows the message containing the flag.
im gonna add some filler text here so the page is a bit longer. lorem ipsum... here's the flag btw, flag{accessibility_is_crucial}