FLAreCAPTCHA
- Category: rev
“Hello and welcome to the FLARE-On 13. This first stage challenge is designed to be easy to solve by a human or a robot, assuming the robot is as smart as a human. Are you as smart as a human? How many AI tokens are you going to burn to find out the answer to that question?”
There is no binary. The challenge is flarecaptcha.html, a fake reCAPTCHA v2 checkbox, and the flag is built in the page’s own JavaScript. Every input to the decryption key comes from the page itself: which mouse button you press, how long you hold it, the page’s charset, and the length of one function’s source text.
TL;DR
- Open challenge through python server (python3 -m http.server)
- Right-click and hold on the click box
- Success banner will appears

Solution
1. Left-clicking is the trap
The checkbox’s mousedown handler has two branches. A left click (e.button === 0) spins for a second and then calls triggerFailure(), which shows “Robot, or unskilled reverse engineer detected.” The other branch is the one that prints the flag:
// Ensure click occured in captcha box with 5,0 and 1,1 layout grids
if (e.button === (Math.pow(5, 0) + Math.pow(1, 1))) {
5^0 + 1^1 = 2, which is the right mouse button. The context menu is suppressed on the widget, and a mouseup with button === 2 or a mouseleave before the 2000 ms setTimeout fires calls cancelHold(), which fails the check. So the human answer is to right-click the checkbox and hold it for 2 seconds.
2. How the flag is built
The ciphertext is a 30-byte array, read through a Proxy that XORs every indexed read with the length of the page’s charset:
const recaptchaLogoBitmap = [123, 5, 102, 4, 100, 22, 102, 19, 106, 28, 110, 7, 103, 24, 109, 30, 110, 55, 105, 27, 110, 5, 106, 90, 96, 25, 33, 20, 96, 26];
get(target, prop) {
if (typeof prop === 'string' && !isNaN(prop)) {
const envShift = document.characterSet.length; // "UTF-8" -> 5
return target[prop] ^ envShift;
}
When the 2-second timer fires, it derives a 16-bit key and XORs even-indexed bytes with its high byte and odd-indexed bytes with its low byte:
const displayPaddingPx = Math.round((errorLogTime - errorLogStartTime) / 1000); // hold time in s
const widgetLength = resetWidget.toString().length;
const widgetUpperBoundsPx = (displayPaddingPx * (widgetLength + 1115)) & 0xFFFF;
const wupperLeft = (widgetUpperBoundsPx >> 8) & 0xFF;
const wupperRight = widgetUpperBoundsPx & 0xFF;
... b ^ (i % 2 === 0 ? wupperLeft : wupperRight)
The key depends on the environment. resetWidget.toString() returns the arrow function’s source text with its whitespace, so a reformatted or minified copy of the page gives a different key. A page decoded as anything other than UTF-8 changes the Proxy shift. In the original file resetWidget is 222 characters, so a 2-second hold gives 2 × (222 + 1115) = 2 × 1337 = 2674 = 0x0A72.
3. Brute-force the key instead of trusting the environment
The key is only 16 bits and Flare-On flags always end in @flare-on.com, so there’s no need to model the timing. solve.py applies the charset XOR, tries all 65,536 keys, and keeps the one whose plaintext has the known suffix:
bitmap = [b ^ len("UTF-8") for b in bitmap]
for key in range(0x10000):
hi, lo = key >> 8, key & 0xFF
flag = "".join(chr(b ^ (hi if i % 2 == 0 else lo)) for i, b in enumerate(bitmap))
if flag.endswith("@flare-on.com"):
print(hex(key), flag)
$ python3 solve.py
0xa72 triskaidekaphobia@flare-on.com
Only one key matches, and it is 0x0A72, the value derived from a 2-second hold in step 2. Holding a right-click on the checkbox in a browser for 2 seconds shows the same flag.
Flag: triskaidekaphobia@flare-on.com