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}