ScriptEngine注入
Java SE 6內(nèi)嵌了對(duì)腳本支持,提供了一些接口來定義一個(gè)腳本規(guī)范,也就是JSR223。通過實(shí)現(xiàn)這些接口,Java SE 6可以支持任意的腳本語(yǔ)言(如PHP或Ruby)。
ScriptEngine官方定義
ScriptEngine is the fundamental interface whose methods must be fully functional in every implementation of this specification. These methods provide basic scripting functionality. Applications written to this simple interface are expected to work with minimal modifications in every implementation. It includes methods that execute scripts, and ones that set and get values.
在java1.8以前,java內(nèi)置的javascript解析引擎是基于Rhino。自JDK8開始,使用新一代的javascript解析名為Oracle Nashorn。Nashorn在jdk15中被移除。所以下面的命令執(zhí)行在JDK8-JDK15都是適用的。
ScriptEngineManager,腳本引擎的管理類,用來創(chuàng)建腳本引擎,在類加載的時(shí)候通過spi的方式,掃描classpath中已經(jīng)包含實(shí)現(xiàn)的所有ScriptEngineFactory,載入后用來負(fù)責(zé)生成具體的ScriptEngine。
– 獲取支持的所有js引擎信息
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf( "Name: %s%n" + "Version: %s%n" + "Language name: %s%n" + "Language version: %s%n" + "Extensions: %s%n" + "Mime types: %s%n" + "Names: %s%n", factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
]); } }
Name: Oracle Nashorn
Version: 1.8.0_261
Language name: ECMAScript
Language version: ECMA - 262 Edition 5.1
Extensions: [js]
Mime types: [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
通過結(jié)果中的Names,我們知道了所有的js引擎名稱故getEngineByName的參數(shù)可以填[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],舉個(gè)例子:
String test="function fun(a,b){ return a+b; }; print(fun(1,4));";
ScriptEngineManager manager = new ScriptEngineManager(null);
//根據(jù)name獲取解析引擎,在jdk8環(huán)境下下面輸入的js和nashorn獲取的解析引擎是相同的。
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(test);
//執(zhí)行結(jié)果
//5
上面的代碼很簡(jiǎn)單就是定義了一個(gè)js函數(shù)加法函數(shù)fun,然后執(zhí)行fun(1,4),就會(huì)得到結(jié)果。
腳本攻擊
因?yàn)閟criptEngine的相關(guān)特性,可以執(zhí)行java代碼,所以當(dāng)我們把test替換為如下代碼,就可以命令執(zhí)行了。
String test="var a = mainOutput();
function mainOutput()
{
var x=java.lang.Runtime.getRuntime().exec("calc")
};
至此,我已經(jīng)發(fā)現(xiàn)了這個(gè)比較簡(jiǎn)單的命令執(zhí)行漏洞,然后我寫了報(bào)告,覺得已經(jīng)完事了。但是,事情不是這么發(fā)展的。因?yàn)榻鉀Q這個(gè)問題的根本方法是底層做沙箱,或者上js沙箱。但是底層沙箱和js沙箱都做不到,一個(gè)過于復(fù)雜另外一個(gè)過于影響效率(效率降低了10倍,這是一個(gè)產(chǎn)品不能接受的)。
所以我們就需要找到一個(gè)其他方法了,新的思路就是黑名單或者白名單。為了靈活性(靈活性是安全的最大敵人),為了客戶方便,不可能采取白名單,所以只能使用黑名單了。
java安全處理器
安全管理器 SecurityManager
java.lang.SecurityManager
具體來說, SecurityManager 可以對(duì)JAVA中的諸如文件訪問、命令執(zhí)行、反射的方法的精準(zhǔn)控制。
安全管理器是Java沙箱的基礎(chǔ)組件。我們一般所說的打開沙箱,也就是加-Djava.security.manager選項(xiàng)。
其實(shí)日常的很多API都涉及到安全管理器,它的工作原理一般是:
- 請(qǐng)求Java API
- Java API使用安全管理器判斷許可權(quán)限
- 通過則順序執(zhí)行,否則拋出一個(gè)Exception
文件讀取的控制
以 FileInputStream() 為例進(jìn)行說明:
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
其中Java代碼,
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
就是利用 SecurityManager 對(duì)權(quán)限進(jìn)行校驗(yàn)。
命令執(zhí)行的控制
public Process start() throws IOException {
// Must convert to array first -- a malicious user-supplied
// list might try to circumvent the security check.
String[] cmdarray = command.toArray(new String[command.size()]);
cmdarray = cmdarray.clone();
for (String arg : cmdarray)
if (arg == null)
throw new NullPointerException();
// Throws IndexOutOfBoundsException if command is empty
String prog = cmdarray[0];
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExec(prog);
// ...
// other code
}
可以看到同樣利用 SecurityManager 對(duì)權(quán)限進(jìn)行校驗(yàn)
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExec(prog);
反射的控制
java.lang.Class:getDeclaredMethods()
public Method[] getDeclaredMethods() throws SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
return copyMethods(privateGetDeclaredMethods(false));
}
跟蹤進(jìn)入到 java.lang.Class:checkMemberAccess() :
private void checkMemberAccess(int which, Class<?> caller, boolean checkProxyInterfaces) {
final SecurityManager s = System.getSecurityManager();
if (s != null) {
/* Default policy allows access to all {@link Member#PUBLIC} members,
* as well as access to classes that have the same class loader as the caller.
* In all other cases, it requires RuntimePermission("accessDeclaredMembers")
* permission.
*/
final ClassLoader ccl = ClassLoader.getClassLoader(caller);
final ClassLoader cl = getClassLoader0();
if (which != Member.PUBLIC) {
if (ccl != cl) {
s.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
}
}
this.checkPackageAccess(ccl, checkProxyInterfaces);
}
}
同樣存在 SecurityManager 利用 checkPermission() 對(duì)操作的校驗(yàn)。
自定義 SecurityManager
通過上述的示例演示,可以知道 SecurityManager 在很多關(guān)鍵的位置都進(jìn)行了動(dòng)作的校驗(yàn)。在上一節(jié)中,我們通過策略文件同樣就是對(duì)一些關(guān)鍵操作進(jìn)行了權(quán)限定義。當(dāng)JAVA程序運(yùn)行至該操作時(shí)就會(huì)檢查此權(quán)限。當(dāng)然我們也可以通過自定義 SecurityManager 來實(shí)現(xiàn)對(duì)某些文件的訪問控制、某些操作的訪問控制。
如果我們需要實(shí)現(xiàn)自定義的訪問控制,我們需要繼承 SecurityManager 類,然后在其中實(shí)現(xiàn)自己的權(quán)限控制的方法。在 java.lang.SecurityManager 中定義了很多的權(quán)限檢測(cè)的方法,包括 checkConnect() 、 checkDelete() 、 checkExec() 、 checkListen() 、 checkRead() 、 checkPropertiesAccess() 等等。所有的這些方法都會(huì)最終調(diào)用 checkPermission() 。所以如果我們要實(shí)現(xiàn)自定義的訪問控制,那么我們就可以嘗試重載 checkPermission() 方法。
對(duì)文件的訪問控制
以文件訪問控制為例:
import java.io.FileInputStream;
import java.io.IOException;
public class TestSecurityManager {
public static void main(String args[]) throws IOException {
System.setSecurityManager(new MySecurityManager());
FileInputStream fis = new FileInputStream("./test.txt");
byte[] bs = new byte[1024];
fis.read(bs);
fis.close();
}
}
class MySecurityManager extends SecurityManager {
@Override
public void checkPermission(Permission perm) {
if(perm instanceof FilePermission) {
String action = perm.getActions();
if (action.equals("read")) {
String filename = perm.getName();
if (filename.contains(".txt")) {
throw new SecurityException("No Access" + filename);
}
}
}
}
}
這樣寫的比較的復(fù)雜。因?yàn)樵?SecurityManager 中直接存在 checkRead() 方法用于對(duì)訪問文件的控制,所以我們也可以選擇直接重載 checkRead() 方法。如下:
class MySecurityManager extends SecurityManager {
@Override
public void checkRead(String file) {
if (file.contains(".txt")) {
throw new SecurityException("No Access " + file);
}
}
@Override
public void checkRead(String file, Object context) {
checkRead(file);
}
}
運(yùn)行程序之后就會(huì)拋出 SecurityException 的錯(cuò)誤。
限制命令執(zhí)行
import java.io.File;
import java.io.FileInputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.security.Permission;
public class TestSecurityManager {
public static void main(String args[]) throws IOException {
System.setSecurityManager(new MySecurityManager());
Runtime.getRuntime().exec("calc.exe");
}
}
class MySecurityManager extends SecurityManager {
@Override
public void checkExec(String cmd) {
if (cmd.contains("calc.exe")) {
throw new SecurityException("forbidden execute");
}
}
}
運(yùn)行上述的程序就會(huì)拋出 SecurityException 錯(cuò)誤。
以上就是一個(gè)簡(jiǎn)單的Demo。這個(gè)僅僅只是實(shí)現(xiàn)了對(duì) calc.exe 的禁止,如果要實(shí)現(xiàn)對(duì)其他方法的限制,上述Demo的方式是明顯不行的。下面是相對(duì)來說一個(gè)禁止命令執(zhí)行的通用版本。
public class TestSecurityManager {
public static void main(String args[]) throws IOException {
System.setSecurityManager(new MySecurityManager());
Runtime.getRuntime().exec("calc.exe");
}
}
class MySecurityManager extends SecurityManager {
@Override
public void checkPermission(Permission perm) {
if (perm instanceof FilePermission) {
String action = perm.getActions();
if (action != null && action.contains("execute")) {
throw new SecurityException("forbidden execute");
}
}
}
}
注意需要通過 FilePermission 來對(duì)權(quán)限進(jìn)行控制。因?yàn)樽罱K的命令執(zhí)行其實(shí)最終都會(huì)調(diào)用本地文件來執(zhí)行代碼,所以通過對(duì) FilePermission 的檢測(cè),判斷是否存在 execute 的動(dòng)作,從而禁止命令執(zhí)行。
其他
通過這種防護(hù)是不是就一定萬(wàn)無(wú)一失了呢?如果 setSecurityManager 被攻擊者設(shè)置為null,這樣就導(dǎo)致了我們所有的安全檢查全部失效了,所以我們也需要保護(hù)我們自定義的 SecurityManager 。如下:
class MySecurityManager extends SecurityManager {
@Override
public void checkPermission(Permission perm) {
// 禁止設(shè)置新的SecurityManager,保護(hù)自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}
}
當(dāng)我們?cè)O(shè)置了之后,我們通過檢查 setSecurityManager 方法禁止其他人對(duì) SecurityManager 進(jìn)行設(shè)置。
總結(jié)
總的來說,當(dāng)需要執(zhí)行第三方的未知代碼時(shí),使用 SecurityManager 來設(shè)置一些白名單、黑名單也是一個(gè)非常好的方法。至于如何到底是選擇策略文件還是通過代碼的方式來實(shí)現(xiàn),主要是看自己項(xiàng)目的需求
黑名單
國(guó)內(nèi)一些單那個(gè)如阿里云提供java沙箱:
https://help.aliyun.com/document_detail/27967.html?spm=5176.doc51823.6.647.rt0efa
mport com.google.common.collect.Sets;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
public class KeywordCheckUtils {
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",
// JavaScript 方法
"eval","new function");
private KeywordCheckUtils() {
// 空構(gòu)造方法
}
public static void checkInsecureKeyword(String code) {
// 去除注釋
String removeComment = StringUtils.replacePattern(code, "(?:/\\*(?:[^*]|(?:\\*+[^*/]))*\\*+/)|(?://.*)", "");
// 多個(gè)空格替換為一個(gè)
String finalCode = StringUtils.replacePattern(removeComment, "\\s+", " ");
Set<String> insecure = blacklist.stream().filter(s -> StringUtils.containsIgnoreCase(finalCode, s))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(insecure)) {
throw new Exception("輸入字符串不是安全的");
}
}
}
因?yàn)楹诿麊沃杏幸粋€(gè)new function。為了檢測(cè)new function,所以他多個(gè)空格換成一個(gè)空格。到這里我就突然想到了空格,既然注釋可以繞過,空格是不是也可以繞過呢。然后就繞過了。
String test="var a = mainOutput(); function mainOutput() { var x=java.lang. Runtime.getRuntime().exec(\"calc\");};";
最后的過濾呢,先過濾了注釋,然后在去匹配過濾空格和剩下一個(gè)空格的。
這一步的操作就是為了匹配new function。
// 去除注釋
String removeComment = StringUtils.replacePattern(code, "(?:/\\*(?:[^*]|(?:\\*+[^*/]))*\\*+/)|(?://.*)", " ");
// 去除空格
String removeWhitespace = StringUtils.replacePattern(removeComment, "\\s+", "");
// 多個(gè)空格替換為一個(gè)
String oneWhiteSpace = StringUtils.replacePattern(removeComment, "\\s+", " ");
Set<String> insecure = blacklist.stream().filter(s -> StringUtils.containsIgnoreCase(removeWhitespace, s) ||
StringUtils.containsIgnoreCase(oneWhiteSpace, s)).collect(Collectors.toSet());
一些總結(jié)
為什么要禁用new function呢?這是因?yàn)閖s的特性,可以使用js返回一個(gè)新的對(duì)象,如下面的字符串??梢钥吹竭@種情況就很難通過字符串匹配來過濾了。
var x=new Function('return'+'(new java.'+'lang.ProcessBuilder)')(); x.command("calc"); x.start(); var a = mainOutput(); function mainOutput() {};
黑名單總是存在潛在的風(fēng)險(xiǎn),總會(huì)出現(xiàn)新的繞過思路。而白名單就比黑名單好很多,但是又失去了很多靈活性。
如果沒有禁用eval,會(huì)有什么樣的繞過方式呢?下面的套娃,就可以實(shí)現(xiàn)
var a = mainOutput(); function mainOutput() { new javax.script.ScriptEngineManager().getEngineByName("js").eval("var a = test(); function test() { var x=java.lang."+"Runtime.getRuntime().exec(\"calc\");};"); };
其它繞過
因?yàn)楹诿麊沃幸呀?jīng)禁用了java.lang.ClassLoader和java.lang.Class當(dāng)時(shí)就是想著防止反射調(diào)用和ClassLoader加載。。
這個(gè)繞過還是很有意思的,先通過子類獲取ClassLoader類,然后通過反射執(zhí)行ClassLoader的definClass方法,從字節(jié)碼中加載一個(gè)惡意類。下面的classBytes存儲(chǔ)的就是一個(gè)惡意類,后面通過實(shí)例惡意類完成攻擊
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 = 'yv66vgAAADQAHwoABgASCgATABQIABUKABMAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAJTEV4cGxvaXQ7AQAKRXhjZXB0aW9ucwcAGQEAClNvdXJjZUZpbGUBAAxFeHBsb2l0LmphdmEMAAcACAcAGgwAGwAcAQAEY2FsYwwAHQAeAQAHRXhwbG9pdAEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAQABAAcACAACAAkAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACgAAAA4AAwAAAAQABAAFAA0ABgALAAAADAABAAAADgAMAA0AAAAOAAAABAABAA8AAQAQAAAAAgAR';" +
" 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();";
惡意類的代碼如下。上面的classBytes就是Exploit類的字節(jié)碼
import java.io.IOException;
public class Exploit {
public Exploit() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
從上面的代碼讓我意識(shí)到禁用java.lang.Class是不可能就阻止反射的,于是我開始思考一個(gè)反射poc中的哪些是重要的關(guān)鍵字。反射方法的調(diào)用和實(shí)例化都是關(guān)鍵的一步,他們一定需要執(zhí)行。所以我禁掉了這兩個(gè)關(guān)鍵字。
新的黑名單就這么形成了。文章來源:http://www.zghlxwxcb.cn/news/detail-628900.html
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",
//反射關(guān)鍵字
"invoke","newinstance",
// JavaScript 方法
"eval", "new function",
//引擎特性
"Java.type","importPackage","importClass","JavaImporter"
);
還可以通過nicode方式繞過:文章來源地址http://www.zghlxwxcb.cn/news/detail-628900.html
String test61="var test = mainOutput(); function mainOutput() { var x=java.lang.//\nRuntime.getRuntime().exec(\"calc\");};";
最后的修復(fù)方案
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",
//反射關(guān)鍵字
"invoke","newinstance",
// JavaScript 方法
"eval", "new function",
//引擎特性
"Java.type","importPackage","importClass","JavaImporter"
);
public KeywordCheckUtils7() {
// 空構(gòu)造方法
}
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+", "");
// 多個(gè)空格替換為一個(gè)
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("存在不安全的關(guān)鍵字:"+insecure);
throw new Exception("存在安全問題");
}else{
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(code);
}
}
}```
到了這里,關(guān)于【web安全系列】scriptEngine注入防御的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!