Overview

This task is a service which is running on team’s vulnbox (server). It can be seen in traffic that check system frequently connects to service and performs some actions but this traffic is almost unreadable (actually encrypted as we will see later). While performing initial overview of the service we noticed that it is working as systemd network socket and each connection is spawning a new instance of /srv/diagon_alley/diagon_alley which receives client socket as its stdin and stdout (inetd-style server).

There are two binaries inside /srv/diagon_alley/ folder: main binary named diagon_alley and another one named shops. diagon_alley is statically linked library without symbols acting as a frontend, which uses dynamically linked binary shops as a backend to database. By analyzing disassembly code we can figure out that both binaries are “sandboxed” using seccomp which explains why such architecture has been chosen instead of performing database query from the main binary. diagon_alley uses pipe-fork-exec routine to start shops as a child process and communicate with it later on. We can communicate only with diagon_alley binary, there is no direct access to shops backend.

Service supports following actions:

  • registration
  • login
  • creating, entering and listing shops
  • creating, adding, buying and listing items

To login and enter the shop you need to provide a password. Flags are stored inside items so you need to enter the corresponding shop and list items in it in order to get flags.

Communication protocol reverse engineering

No client is provided to communicate with server so let’s open diagon_alley binary in disassembler and dig our way to complete understanding of used communication protocol.

One of the first functions called in main is a constructor of IO_wrapper class. This class provides two methods, IO_read and IO_puts as can be understood from their inner logic. We need to note that these functions not only perform data sending and receiving but are also doing some crypto stuff with that data. They heavily use IO_wrapper object variables so let’s see what are these variables initialized with.

In IO_wrapper constructor (0x402750) two function pointers are set to IO_read and IO_puts functions, input and output FILE* variables are set to stdin and stdout respectively. One DWORD value is set to zero and another is set to rand(). Additionally current time is saved for unknown purpose, it is updated in IO_read so we called it last_op_time.

After constructing IO object it is immediately used in function at address 0x402890. A lot of things are going on there so we start to randomly inspect called functions. In function 0x408580 we can see calls that look like asserts:

...
    sub_406290("in != NULL", "src/pk/rsa/rsa_import.c", 32LL); 
...
    sub_406290("key != NULL", "src/pk/rsa/rsa_import.c", 33LL);
...

Using these messages we can quickly find used crypto library: libtomcrypt. We can identify function at 0x402450 by assert that it contains: perror("fread", a2, v7);. Using this approach we can find most standard function names and obtain the following “beautified” form of function at address 0x402890:

_BOOL8 __fastcall set_check_rsa(IO_wrapper *IO)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v13 = __readfsqword(0x28u);
  sub_402A00();
  stdin = IO->stdin;
  packet_size = 0LL;
  fread(&packet_size, 1LL, 8uLL, stdin);
  v2 = fread(&packet, 1LL, packet_size, IO->stdin);
  user_rsa_key = (rsa_key *)calloc(1LL, 72LL);
  if ( !user_rsa_key )
    goto LABEL_12;
  user_rsa_key& = user_rsa_key;
  if ( (unsigned int)rsa_import(&packet, v2, user_rsa_key) )
    return 1LL;
  IO->user_rsa_key = user_rsa_key&;
  server_rsa_key = (rsa_key *)calloc(1LL, 72LL);
  server_rsa_key& = server_rsa_key;
  if ( server_rsa_key )
  {
    LOBYTE(v9) = rsa_generate(server_rsa_key, 2048);
    if ( v9 )
      return 1LL;
    IO->server_rsa_key = server_rsa_key&;
    a3 = 2048LL;
    if ( rsa_export((unsigned __int8 *)&packet, &a3, 0, server_rsa_key&) )
      return 1LL;
    fwrite(&a3, 1LL, 8uLL, IO->stdout);
    fwrite(&packet, 1LL, a3, IO->stdout);
    result = sub_402810(IO);
  }
  else
  {
LABEL_12:
    perror("calloc", 72LL, v4);
    result = 1LL;
  }
  return result;
}

We identified two additional variables in IO_wrapper object used in this function: user_rsa_key and server_rsa_key. Full IO_wrapper structure:

struct __attribute__((aligned(16))) IO_wrapper
{
    QWORD init_random;
    QWORD cnt;
    FILE *stdin;
    FILE *stdout;
    rsa_key *user_rsa_key;
    rsa_key *server_rsa_key;
    QWORD (__cdecl *write)(IO_wrapper *, char *);
    QWORD (__cdecl *read)(IO_wrapper *, _BYTE *);
    time_t last_op_time;
};

Now we look back at IO_puts and see that it uses IO->user_rsa_key. Similarly, IO_read uses IO->server_rsa_key. Looks like we need to generate our RSA key, send it to server, receive server’s RSA key and after that our messages to server are encrypted using server’s key and incoming messages are encrypted with ours key.

After that we reverse engineer regular packet structure from both IO_puts and IO_read functions. It looks like this:

struct __attribute__((aligned(8))) in_buffer
{
    QWORD chunk_length;   // including itself
    QWORD init_random;    // as determined in IO_wrapper::ctr
    QWORD cnt;            // increments in IO_puts
    QWORD data_length;    // size of data array
    BYTE data[];          // actual data
};

At first cnt is zero. You may think that we don’t know server’s random but after exchanging RSA keys servers sends us main menu and since it is encoded in this format, it contains this random number. Overall, here is the process of establishing and maintaining connection:

  1. Generate and send our RSA key
  2. Receive server’s RSA key
  3. Receive first encrypted packet with main menu, extract init_random from it
  4. Send and receive data encoded as in_buffer structure, do not forget to update local cnt copy on each server’s IO_puts (i.e., our send).

Side note: an interesting question is how exactly to use RSA keys. What encryption&decryption scheme is used? Of course you can reverse engineer diagon_alley binary further or google what scheme libtomcrypt is using by default but who needs these smart approaches in 2018? In our case we imported ours and server’s keys as Crypto.PublicKey.RSA objects and wondered why their methods encrypt and decrypt are raising NotImplemented exception so we googled it and first answer suggested to use PKCS1_OAEP scheme with these keys (good_key = PKCS1_OAEP.new(our_key)). It worked. After the end of the game player from some other team in IRC said that in their version of library raw RSA key’s encrypt and decrypt methods were implemented but not as in PKCS1_OAEP scheme so they were stuck with fully understood but not working protocol. Bad luck.

Script that handles connection and makes encryption layer transparent for pwntools can be found here.

Exploitation

After busting out our static analysis tools (strings from binutils) we can find vulnerability in shops binary. Queries that are meant to check password during login and shop entering use LIKE operator instead of normal comparison. That means, by simply entering %% as a password you can enter any shop from the list and get the items. As you see importing server-generated private keys and establishing communication between client and the server is way harder than further exploitation.

Full exploit can be found here.

It is also possible to achieve code execution at this service which we didn’t manage to do due to the lack of time, but it can still be your homework. It is possible to enter item name large enough to trigger the stack overflow in add_item method. It happenes due to the “typo” which copies 0x20 bytes of the item name to the buffer with 20 bytes size.

IRC logs

Here is what orgs said about this challenge after the end of CTF (saved for history):

23:07 <+mightymo> so in the frontend there was a simple buffer overflow, when you create an item
23:08 <+mightymo> it would check that the item name is less than 50 by using strlen but then copy using the length of the message which is not based on c-strings
23:14 <+mightymo> to get code execution in the backend then, you could trigger a SIGUSR, which would lead to a uaf or craft a malicious createShopReq that would lead to an overflow