beelogin starts with a simple HTML page with five input fields. Diving into the source, there’s almost sixty thousand lines of JavaScript. The vast majority of that ends up being junk that isn’t run. I’ll trim it down to around 30 lines. Then there’s some math to track where each of 64 bytes in the key impact which bytes of the result. Once I have that, I can check for bytes that produce valid JavaScript, and find the key. The result is some obfuscated JavaScript that comes out to be doing the same thing again, on the second half of the key. Once I have both halves, I can get the flag or put the key in and get the page to give it to me.

## Challenge

You’re nearly done champ, just a few more to go. we put all the hard ones at the beginning of the challenge this year so its smooth sailing from this point. Call your friends, tell ‘em you won. They probably don’t care. Flare-On is your only friend now.

$file beelogin.html beelogin.html: HTML document, ASCII text, with very long lines, with CRLF, LF line terminators  The file is quite long, 59,090 lines, 271,350 words, and over three million characters: $ wc beelogin.html


## Running It

Opening it in Firefox shows a background that looks like an advertisement for Bee Movie, with five input fields and a Submit button:

Click for full size image

Pushing the submit button doesn’t seem to do anything regardless of any data guessed in the various inputs.

## RE

### Overview

The top of the file is some static HTML

<!DOCTYPE HTML>
<html>
<style>
body {
...[snip]...
</style>
<body>
<div><input id="submit" type="submit"></div>
</form>
...[snip]...


The rest of the file falls between a <script> tag:

Where the close close for that is at the very end of the file:

In fact, the entire script is one function, Add, which is called by the <form> tag onsubmit and passed the form itself. So in the script, xDyuf5ziRN1SvRgcaYDiFlXE3AwG will be the form.

### Chaff

There’s a ton of stuff in this JavaScript that just isn’t ever used. Tons of functions are defined and never called. There’s also a bunch of lines that look like:

BntQj9FBk=xDyuf5ziRN1SvRgcaYDiFlXE3AwG.LLfYTmPiahzA3WFXKcL5BczcG1s1.value.split(';')
if('rFzmLyTiZ6AHlL1Q4xV7G8pW32'>=BntQj9FBk)eval(BntQj9FBk)
vwXaWQUif35pQPp1HRk=xDyuf5ziRN1SvRgcaYDiFlXE3AwG.LLfYTmPiahzA3WFXKcL5BczcG1s1.value.split(';')


All but one of those are never used. The one that is used is actually based on the forth input field:

qguBomGfcTZ6L4lRxS0TWx1IwG=xDyuf5ziRN1SvRgcaYDiFlXE3AwG.ZJqLM97qEThEw2Tgkd8VM5OWlcFN6hx4y2.value.split(';')


There is a giant Base64 blob on line 4310:

This is the first line that actually runs, and putting a break point here allows me to single step and see the next lines call, which is useful in cleaning out all the garbage.

### Algorithm

By steping through and cleaning up, I end up with the following pseudocode that repsents what the JavaScript is doing:

pyk = "[big b64 blob]"
gjr = "b2JDN2luc2tiYXhLOFZaUWRRWTlSeXdJbk9lVWxLcHlrMXJsRnk5NjJaWkQ4SHdGVjhyOENQeFE5dGxUaEd1dGJ5ZDNOYTEzRmZRN1V1emxkZUJQNTN0Umt6WkxjbDdEaU1KVWF1M29LWURzOGxUWFR2YjJqQW1HUmNEU2RRcXdFSERzM0d3emhOaGVIYlE3dm9aeVJTMHdLY2Vhb3YyVGQ4UnQ2SXUwdm1ZbGlVYjA4YVRES2xESnlXU3NtZENMN0J4MnBYdlZET3RUSmlhY2V6Y3B6eUM2Mm4yOWs=";
l = 64
qgu = ZJInput.split(';')
npx = atob(pyk)
pef = npx.length
euf = atob(gjr)
bnt = 'gflsdgfdjgflkdsfjg4980utjkfdskfglsldfgjJLmSDA49sdfgjlfdsjjqdgjfj'
if(qgu[0].length==l){
bnt=qgu[0]
}
for(i=0; i < euf.length; i++) {
euf[i] = (euf[i] + bnt[i%l]) & 0xff
}
oz9 = npx
for(i=0; i<pef; i++) {
oz9[i] = (oz9[i] - euf[i%euf.length]) & 0xff
}
sej = ""
for(i=0; i < npx.length; i++) {
sej += oz9[i]
}
eval(sej);


There’s two blogs of base64 data, “big blob” (pyk above which decodes to npx), and “little blob” (gjr above which decodes to euf). The input in the forth input field is split on ; and the first result is stored in bnt if it’s exactly 64 characters in length.

Then there’s a loop over the smaller decoded blob, adding the corresponding byte from the input (and looping to the start of the input when it reaches the end). Then it loops over the big blob, subtracting the corresponding byte in the small blob (and looped when it reaches the end).

## Key First Half

### Find Pattern

#### Theory

Let’s say I had two buffers that we 5 and 18 bytes long. In the example above, the key would be 5 (instead of 64), and the small blob decoded would be 18 (instead of 221 bytes). So by messing with the first byte in the key, the small blob would see changes at the green positions:

If that was then applied to a bigger buffer (say 60 bytes), the pattern would look like:

It’s changing every fifth byte up to the length of 18, then it starts over.

Some character is impacted by the first byte in the key if:

$(pos\mod{18})\mod{5} = 0$

The second byte if that is 1, third if 2, etc.

So for example, position 23:

$(23\mod{18})\mod{5} = 5\mod{5} = 0$

It becomes 5 and then 0, which is why it’s green in the image, impacted by key position 0. Looking at 30:

$(30\mod{18})\mod{5} = 12\mod{5} = 2$

So position 30 is impacted by the second byte of this five byte key.

#### Brute Forcing

To see it a different way, I wrote a simple program to look at the impact of changing the key at one position:

#!/usr/bin/env python3

import sys
from base64 import b64decode
from itertools import cycle

try:
pos = int(sys.argv[1])
except:
pos = 0

with open('PyKEvIqAmUkUVL0Anfn9FElFUN2dic3z.base64', 'r') as f:
gjr =  "b2JDN2luc2tiYXhLOFZaUWRRWTlSeXdJbk9lVWxLcHlrMXJsRnk5NjJaWkQ4SHdGVjhyOENQeFE5dGxUaEd1dGJ5ZDNOYTEzRmZRN1V1emxkZUJQNTN0Umt6WkxjbDdEaU1KVWF1M29LWURzOGxUWFR2YjJqQW1HUmNEU2RRcXdFSERzM0d3emhOaGVIYlE3dm9aeVJTMHdLY2Vhb3YyVGQ4UnQ2SXUwdm1ZbGlVYjA4YVRES2xESnlXU3NtZENMN0J4MnBYdlZET3RUSmlhY2V6Y3B6eUM2Mm4yOWs=";

npx = b64decode(pyk)
pef = len(npx)
euf = b64decode(gjr)

key1 = b"a" * 64
key2 = b"a" * pos + b"b" + b"a" * (63-pos)

euf_key1 = [(x+y)&0xff for x,y in zip(euf, cycle(key1))]
euf_key2 = [(x+y)&0xff for x,y in zip(euf, cycle(key2))]

oz9_k1 = [(x-y)&0xff for x,y in zip(npx, cycle(euf_key1))]
oz9_k2 = [(x-y)&0xff for x,y in zip(npx, cycle(euf_key2))]

for i in range(5000):
if oz9_k1[i] != oz9_k2[i]:
print(f'{i}, ', end="")
print()


It will read in the two buffers and decode them, and create two keys. The first is all “a”. The second is all “a” except for one “b”. It then generates the resulting buffers for each, and prints any characters in the first 5000 that are different. So changing the first character changes:

$python get_changes.py 0 0, 64, 128, 192, 221, 285, 349, 413, 442, 506, 570, 634, 663, 727, 791, 855, 884, 948, 1012, 1076, 1105, 1169, 1233, 1297, 1326, 1390, 1454, 1518, 1547, 1611, 1675, 1739, 1768, 1832, 1896, 1960, 1989, 2053, 2117, 2181, 2210, 2274, 2338, 2402, 2431, 2495, 2559, 2623, 2652, 2716, 2780, 2844, 2873, 2937, 3001, 3065, 3094, 3158, 3222, 3286, 3315, 3379, 3443, 3507, 3536, 3600, 3664, 3728, 3757, 3821, 3885, 3949, 3978, 4042, 4106, 4170, 4199, 4263, 4327, 4391, 4420, 4484, 4548, 4612, 4641, 4705, 4769, 4833, 4862, 4926, 4990,  The change impacts every 64 bytes until it reaches 221, where it starts over. This fits the double mod equations shown above. Changing other bytes shows the same behavior: $ python get_changes.py 1
1, 65, 129, 193, 222, 286, 350, 414, 443, 507, 571, 635, 664, 728, 792, 856, 885, 949, 1013, 1077, 1106, 1170, 1234, 1298, 1327, 1391, 1455, 1519, 1548, 1612, 1676, 1740, 1769, 1833, 1897, 1961, 1990, 2054, 2118, 2182, 2211, 2275, 2339, 2403, 2432, 2496, 2560, 2624, 2653, 2717, 2781, 2845, 2874, 2938, 3002, 3066, 3095, 3159, 3223, 3287, 3316, 3380, 3444, 3508, 3537, 3601, 3665, 3729, 3758, 3822, 3886, 3950, 3979, 4043, 4107, 4171, 4200, 4264, 4328, 4392, 4421, 4485, 4549, 4613, 4642, 4706, 4770, 4834, 4863, 4927, 4991,
$python get_changes.py 63 63, 127, 191, 284, 348, 412, 505, 569, 633, 726, 790, 854, 947, 1011, 1075, 1168, 1232, 1296, 1389, 1453, 1517, 1610, 1674, 1738, 1831, 1895, 1959, 2052, 2116, 2180, 2273, 2337, 2401, 2494, 2558, 2622, 2715, 2779, 2843, 2936, 3000, 3064, 3157, 3221, 3285, 3378, 3442, 3506, 3599, 3663, 3727, 3820, 3884, 3948, 4041, 4105, 4169, 4262, 4326, 4390, 4483, 4547, 4611, 4704, 4768, 4832, 4925, 4989,  ### Find First Key #### Find Possible Keys I wrote another Python program to brute force all possible key bytes that produced valid JavaScript characters: #!/usr/bin/env python3 import sys from base64 import b64decode from collections import defaultdict from itertools import cycle from string import printable, ascii_letters, digits with open('PyKEvIqAmUkUVL0Anfn9FElFUN2dic3z.base64', 'r') as f: pyk = f.read() gjr = "b2JDN2luc2tiYXhLOFZaUWRRWTlSeXdJbk9lVWxLcHlrMXJsRnk5NjJaWkQ4SHdGVjhyOENQeFE5dGxUaEd1dGJ5ZDNOYTEzRmZRN1V1emxkZUJQNTN0Umt6WkxjbDdEaU1KVWF1M29LWURzOGxUWFR2YjJqQW1HUmNEU2RRcXdFSERzM0d3emhOaGVIYlE3dm9aeVJTMHdLY2Vhb3YyVGQ4UnQ2SXUwdm1ZbGlVYjA4YVRES2xESnlXU3NtZENMN0J4MnBYdlZET3RUSmlhY2V6Y3B6eUM2Mm4yOWs="; npx = b64decode(pyk) pef = len(npx) euf = b64decode(gjr) leuf = len(euf) shifted = [(x-y) for x,y in zip(npx, cycle(euf))] key = ['a'] * 64 for i in range(64): print(f"\n[{i:02d}] ", end="") res = [shifted[j] for j in range(pef) if (j % leuf) % 64 == i] #for c in printable[:-2]: for c in ascii_letters + digits: if all(chr((x-ord(c)) % 256) in printable[:-2] for x in res): print(c, end="") key[i] = c print(f'\n{"".join(key)}')  For each byte in the key, it calculates and collects the intermediate values for all the characters that are impacted by that byte (that’s what if (j % leuf) % 64) == i selects for). Then it loops over letters and digits looking for any that make all valid output. The result looks like: oxdf@hacky[~/flare/08-beelogin]$ python brute.py

[00] C
[01] cdefgh
[02] V
[03] C
[04] SV
[05] Y
[06] wz
[07] I
[08] 1
[10] U
[11] 9
[12] c
[13] V
[14] g
[15] 1
[16] u
[17] k
[18] B
[19] nq
[20] O
[21] 2
[22] u
[23] 4
[24] RU
...[snip]...
[57] m
[58] K
[59] B
[60] CDEFGH
[61] PQRSTU
[62] RSTUVW
[63] I
ChVCVYzI1dU9cVg1ukBqO2u4UGr9aVCNWHpMUuYDLmDO22cdhXq3oqp8jmKBHUWI


It’s not completely unambiguous, but it’s close enough that I can give it a look. I updated the script to allow me to pass in a key and see the results:

#!/usr/bin/env python3

import sys
from base64 import b64decode
from collections import defaultdict
from itertools import cycle
from string import printable, ascii_letters, digits

with open('PyKEvIqAmUkUVL0Anfn9FElFUN2dic3z.base64', 'r') as f:
gjr = "b2JDN2luc2tiYXhLOFZaUWRRWTlSeXdJbk9lVWxLcHlrMXJsRnk5NjJaWkQ4SHdGVjhyOENQeFE5dGxUaEd1dGJ5ZDNOYTEzRmZRN1V1emxkZUJQNTN0Umt6WkxjbDdEaU1KVWF1M29LWURzOGxUWFR2YjJqQW1HUmNEU2RRcXdFSERzM0d3emhOaGVIYlE3dm9aeVJTMHdLY2Vhb3YyVGQ4UnQ2SXUwdm1ZbGlVYjA4YVRES2xESnlXU3NtZENMN0J4MnBYdlZET3RUSmlhY2V6Y3B6eUM2Mm4yOWs=";

npx = b64decode(pyk)
pef = len(npx)
euf = b64decode(gjr)
leuf = len(euf)

if len(sys.argv) == 1:
shifted = [(x-y) for x,y in zip(npx, cycle(euf))]
key = ['a'] * 64
for i in range(64):
print(f"\n[{i:02d}] ", end="")
res = [shifted[j] for j in range(pef) if (j % leuf) % 64 == i]
#for c in printable[:-2]:
for c in ascii_letters + digits:
if all(chr((x-ord(c)) % 256) in printable[:-2] for x in res):
print(c, end="")
key[i] = c
print(f'\n{"".join(key)}')
else:
key = sys.argv[1].encode()
mod_key = [x+y for x,y in zip(euf, cycle(key))]
res = ''.join([chr((x-y) % 256) for x,y in zip(npx, cycle(mod_key))])
print(res)


#### Find Key

I can start with the key from my script, which happens to be taking the last possible key for each option. The idea was I can change each character to other options in the list above as needed. But it turns out taking the last in each option is the right answer, and the key above is correct: ChVCVYzI1dU9cVg1ukBqO2u4UGr9aVCNWHpMUuYDLmDO22cdhXq3oqp8jmKBHUWI.

The resulting text is starts with a bunch of Bee Movie quotes commented out:

//Yes, but who can deny the heart that is yearning?
//Affirmative!
//Uh-oh!
//This.
//At least you're out in the world. You must meet girls.
//Why is yogurt night so difficult?!
//I feel so fast and free!
//Good idea! You can really see why he's considered one of the best lawyers...
//One's bald, one's in a boat, they're both unconscious!
//You know what a Cinnabon is?
//Just one. I try not to use the competition.
//Whose side are you on?
//Did you ever think, "I'm a kid from The Hive. I can't do this"?
//Can I get help with the Sky Mall magazine? I'd like to order the talking inflatable nose and ear hair trimmer.
//It's a close community.
//Which one?
...[snip]...


Then comes obfuscated JS:

## Key Second Half

### De-JS-fuck

I saved that output to a file, and removed the comment lines. The result is what is called JSFuck. According to it’s own page:

JSFuck is an esoteric and educational programming style based on the atomic parts of JavaScript. It uses only six different characters to write and execute code.

It’s a complete language that is legit JavaScript using only 6 characters: ()[]+!. This is clearly that.

There are a lot of online JS deobfuscators that claim to deobfuscate JSFuck, but most crashed when I gave them such a long program. But de4js handled it beautifuly. I’ll upload the file here, and get the results:

Click for full size image

The resutling JavaScript is:

(function (qguBomGfcTZ6L4lRxS0TWx1IwG) {
SEN5lpjT4o1WcRyenF3c6EmlnjdnW = "N0l2N2l2RTVDYlNUdk5UNGkxR0lCbTExZmI4YnZ4Z0FpeEpia2NGN0xGYUh2N0dubWl2ZFpOWm15c0JMVDFWeHV3ZFpsd2JvdTVSTW1vZndYRGpYdnhrcGJFS0taRnZOMnNJU1haRXlMM2lIWEZtN0RSQThoMG8yYUhjNFZLTGtmOXBDOFR3OUpyT2RwUmFOOUdFck12bXd2dnBzOUVMWVpxRmpnc0ZHTFFtMGV4WW11Wmc1bWRpZWZ6U3FoZUNaOEJiMURCRDJTS1o3SFpNRzcwRndMZ0RCNFFEZWZsSWE4Vg==";
pKxpcv7X8OO7AY4brDHDibSSlZx2F = atob(sInNWkbompb8pOyDG5D).split('');
WLjv1KngPLuN8eezUIIj5tGR1ZZgqUZ = pKxpcv7X8OO7AY4brDHDibSSlZx2F.length;
anFlFCVHqfi4WmTzNxmg = atob(SEN5lpjT4o1WcRyenF3c6EmlnjdnW).split('');
NbgNroelQqxtLGx4xr2FzHuonetRtscR2 = '87gfds8f4h4dsahfdjhkDHKHF83hNNFDHHKFBDSAKFSfsd47lmkbfjghgdfgda34'.split('');
if (qguBomGfcTZ6L4lRxS0TWx1IwG[1].length == 64) NbgNroelQqxtLGx4xr2FzHuonetRtscR2 = qguBomGfcTZ6L4lRxS0TWx1IwG[1].split('');
for (i = 0; i < anFlFCVHqfi4WmTzNxmg.length; i++) {
anFlFCVHqfi4WmTzNxmg[i] = (anFlFCVHqfi4WmTzNxmg[i].charCodeAt(0) + NbgNroelQqxtLGx4xr2FzHuonetRtscR2[i % 64].charCodeAt(0)) & 0xFF;
};
for (i = 0; i < WLjv1KngPLuN8eezUIIj5tGR1ZZgqUZ; i++) {
pKxpcv7X8OO7AY4brDHDibSSlZx2F[i] = (pKxpcv7X8OO7AY4brDHDibSSlZx2F[i].charCodeAt(0) - anFlFCVHqfi4WmTzNxmg[i % anFlFCVHqfi4WmTzNxmg.length]) & 0xFF;
};
pKxpcv7X8OO7AY4brDHDibSSlZx2F = String.fromCharCode.apply(null, pKxpcv7X8OO7AY4brDHDibSSlZx2F);
if ('rFzmLyTiZ6AHlL1Q4xV7G8pW32' >= Oz9nOiwWfRL6yjIwvM4OgaZMIt0B) eval(pKxpcv7X8OO7AY4brDHDibSSlZx2F);
})(qguBomGfcTZ6L4lRxS0TWx1IwG);


### Dejavu

This code is doing the same thing as the first iteration, except this time it is taking the second object from the input field split on ;. I created a copy of brute.py and updated to new buffers, so that it does the same thing again:

$python3 brute2.py [00] RU [01] LMNOPQ [02] 8 [03] y [04] gj [05] klmnopq [06] rstuvw [07] A [08] k [09] o [10] V [11] DG [12] m [13] 7 [14] V [15] AD [16] d [17] h [18] L [19] lo [20] D [21] hk [22] 0 [23] Q ...[snip]... [58] x [59] J [60] P [61] dg [62] H [63] l UQ8yjqwAkoVGm7VDdhLoDk0Q75eKKhTfXXke36UFdtKAi0etRZ3DoHPz7NxJPgHl  With the correct key (UQ8yjqwAkoVGm7VDdhLoDk0Q75eKKhTfXXke36UFdtKAi0etRZ3DoHPz7NxJPgHl), it produced more commented movie quotes, and some more JSFuck: oxdf@jawad:~/flareon/08-beelogin$ python3 brute2.py UQ8yjqwAkoVGm7VDdhLoDk0Q75eKKhTfXXke36UFdtKAi0etRZ3DoHPz7NxJPgHl | head
//He's not bothering anybody.
//Why would you question anything? We're bees.
//But you've never been a police officer, have you?
//Up on a float, surrounded by flowers, crowds cheering.
//According to all known laws of aviation, there is no way a bee should be able to fly.
//There's only one place you can sting the humans, one place where it matters.
//Can I get help with the Sky Mall magazine? I'd like to order the talking inflatable nose and ear hair trimmer.
//Maybe I'll pierce my thorax. Shave my antennae. Shack up with a grasshopper. Get a gold tooth and call everybody "dawg"!
//Did you bring your crazy straw?
...[snip]...


### Flag

Uploading this JSFuck to de4js returned a very simple line of JavaScript:

Click for full size image

That’s all I need to get the flag.

But also, I know have the key to the site. I can put this flag into the forth input box:

ChVCVYzI1dU9cVg1ukBqO2u4UGr9aVCNWHpMUuYDLmDO22cdhXq3oqp8jmKBHUWI;UQ8yjqwAkoVGm7VDdhLoDk0Q75eKKhTfXXke36UFdtKAi0etRZ3DoHPz7NxJPgHl


And on hitting submit:

Flag: I_h4d_v1rtU411y_n0_r3h34rs4l_f0r_th4t@flare-on.com