Can you pass the path traversal exam? π There might be some requirements tho for your specieβ¦
https://path-traversal-101.knping.pl
ec96a2a4ee45dd97dfa37319f21468a3.zip
Weβre given a zip file containing the source files for the website. Letβs take a look.
Firstly, robot.js:
export default (req, res, next) => { const userAgent = req.get("User-Agent"); if (userAgent == "robot") { next(); } else { res.render("robot", { error: "π€π€π€π€π€" }); }};Seems like we need to change our User-Agent to robot. This can easily be done with the Google Chrome extensions Requestly or with BurpSuite. I chose to do it with Requestly.
To do it with Requestly, create a new rule to modify HTTP Request Headers, and modify User-Agent to robot. Reloading the page now gets us in!
Now take a look at index.js:
import express from "express";import { randomUUID } from "crypto";import withCatch from "./withCatch.js";import { task1, task2, task3, tasks } from "./tasks.js";import cookieParser from "cookie-parser";
import rl from "./ratelimit.js";import robot from "./robot.js";
const { FLAG } = process.env;
const app = express();const port = 3000;
const sessions = [];
const createSession = () => { const id = randomUUID(); return { id, task1: false, task2: false, task3: false, };};
app.set("view engine", "ejs");app.use(express.static("static"));app.use(express.urlencoded({ extended: true }));app.use(express.json());app.use(cookieParser());
app.get("/", (req, res) => { res.render("robot", { error: "", });});
app.get("/robots.txt", (req, res) => { res.type("text/plain"); res.send("User-agent: *\nDisallow: /π€");});
app.get("/%F0%9F%A4%96", robot, (req, res) => { const token = req.cookies.token; if (!token) { const session = createSession(); sessions.push(session); res.cookie("token", session.id); res.render("exam", { task: tasks[0], }); } else { const session = sessions.find((session) => session.id === token); if (!session) { const session = createSession(); sessions.push(session); res.cookie("token", session.id); res.render("exam", { task: tasks[0], }); } else { if (!session.task1) { res.render("exam", { task: tasks[0], }); } else if (!session.task2) { res.render("exam", { task: tasks[1], }); } else if (!session.task3) { res.render("exam", { task: tasks[2], }); } else { res.render("exam", { task: FLAG, }); } } }});
app.get("/*", (req, res) => { res.render("robot", { error: "", });});
app.post( "/%F0%9F%A4%96", rl, robot, withCatch(async (req, res) => { const token = req.cookies.token; if (!token) { throw new Error("Unauthorized"); } const session = sessions.find((session) => session.id === token); if (!session) { throw new Error("Unauthorized"); } const { solution } = req.body; if (!session.task1) { const result = task1(solution); if (result) { session.task1 = true; res.render("exam", { task: tasks[1], }); } else { res.render("exam", { task: tasks[0], error: "Try again!", }); } } else if (!session.task2) { const result = task2(solution); if (result) { session.task2 = true; res.render("exam", { task: tasks[2], }); } else { res.render("exam", { task: tasks[1], error: "Try again!", }); } } else if (!session.task3) { const result = task3(solution); if (result) { session.task3 = true; res.render("exam", { task: FLAG, }); } else { res.render("exam", { task: tasks[2], error: "Try again!", }); } } }));
app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`);});Immediately, we can notice that at /robots.txt, /π€ is disallowed from web crawling. Visiting that site presents us with our first task of tasks.js. Hereβs tasks.js for reference:
import path from "path";
const preTask = (solution) => { if (typeof solution !== "string") { throw new Error("Solution must be a string"); } if (solution.length > 512) { throw new Error("Solution must be less than 512 characters"); } if (solution === "flag") { throw new Error("Your solution can't be 'flag'"); } if (solution === "./flag") { throw new Error("Your solution can't be './flag'"); }};
export const task1 = (solution) => { preTask(solution); if (!solution.startsWith("/robot") || solution.endsWith("/flag")) { throw new Error( "You cannot access the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};
export const task2 = (solution) => { preTask(solution); solution = solution.replaceAll("../", ""); if (solution === "/flag") { throw new Error( "You cannot ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};
export const task3 = (solution) => { preTask(solution); if (solution.includes("../") || solution === "/flag") { throw new Error( "You CANNOT ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};
export const tasks = [ `if (!solution.startsWith("/robot") || solution.endsWith("/flag")) { throw new Error( "You cannot access the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" );}
const solutionPath = path.join("/", solution);return solutionPath === "/flag";`, `solution = solution.replaceAll("../", ""); if (solution === "/flag") { throw new Error( "You cannot ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";`, `if (solution.includes("../") || solution === "/flag") { throw new Error( "You CANNOT ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" );}
const solutionPath = path.join("/", solution);return solutionPath === "/flag";`,];Letβs take a look at task 1:
export const task1 = (solution) => { preTask(solution); if (!solution.startsWith("/robot") || solution.endsWith("/flag")) { throw new Error( "You cannot access the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};Firstly, to locally test our solution, itβs easy to use a Node.js compiler like this and run each task there. Just note that importing the path module requires the use of this statement: var path = require('path');, not the one they provide in tasks.js.
Now letβs develop our solution. The first one requires that our path start with /robot and not end with /flag. This can be pretty easily done with .., which allows us to traverse backwards. Thus, we can construct the following string:
/robot/../flag/a/..
The idea here is that /robot/.. travels into and then out of the robot directory, while /a/.. travels into and then out of the a directory, leaving us with just /flag, our desired outcome!
Hereβs task 2:
export const task2 = (solution) => { preTask(solution); solution = solution.replaceAll("../", ""); if (solution === "/flag") { throw new Error( "You cannot ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};Task two seems to just be removing all ../ and replacing them with black spaces. Once the removal is done it simply checks if the result of the replacement is /flag. After solving this and moving onto task 3, I realized that the intended solution was probably the following payload:
/flag/a/....//
The reason this would work is because the replace operation would actually result in the following:
/flag/a/../
Which would evaluate as a path to the flag. However, I actually found a different solution:
//flag
The first forward slash doesnβt actually do anything to change the path, so this works just fine to pass task 2!
Hereβs task 3:
export const task3 = (solution) => { preTask(solution); if (solution.includes("../") || solution === "/flag") { throw new Error( "You CANNOT ACCESS the flag!!! You are UNAUTHORIZED!!! π€π€π€π€π€" ); }
const solutionPath = path.join("/", solution); return solutionPath === "/flag";};Conveniently, because I found the unintended solution to task 2, my solution there worked completely fine here as well.
//flag
Once I completed the final task, the website presented me with the flag!
ping{p4th_tr4V3Rs4L_06c22f693acd46015891c98cb72f45e3}