Content-Type: text/plain

(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}