java中使用javax.script 執(zhí)行js的安全防御
0x01 背景
在某次滲透測(cè)試中,發(fā)現(xiàn)系統(tǒng)后臺(tái)有一處服務(wù)器接收瀏覽器的js代碼,使用javax.script這個(gè)組件去執(zhí)行js代碼。在這里可以使用以下poc去執(zhí)行系統(tǒng)命令
function getResult(sqlArr) {
var r = "";
java.io.BufferedReader
b = java.lang.Runtime.getRuntime().exec('whoami').getInputStream();
r = String.fromCharCode(b.read());
while ((i = b.read()) != -1)
r = r + String.fromCharCode(i);
return r;
}
問(wèn)了一下RD,這里本來(lái)是系統(tǒng)底層命令,在此處使用js去更靈活地完成數(shù)據(jù)篩選任務(wù)。在這里,javax.script使用Nashorn引擎去執(zhí)行js代碼,如果不加以限制,則會(huì)通過(guò)調(diào)用runtime類(lèi)等方法去執(zhí)行系統(tǒng)命令。開(kāi)發(fā)那邊也不可能改,所以只要想一下限制的方法了。谷歌搜索了一下,發(fā)現(xiàn)starkoverflow和openjdk都給出了解決方案,這里說(shuō)一下。
0x02 解決
1.openjdk的解決方案
在這里openjdk解釋?zhuān)琷avax執(zhí)行js代碼,默認(rèn)是沒(méi)有安全限制的??梢栽L(fǎng)問(wèn)任意的java對(duì)象。但是我們可以使用java.security.policy編寫(xiě)policy文件,去限制相應(yīng)java相應(yīng)的行為,例如不能執(zhí)行系統(tǒng)命令,不能訪(fǎng)問(wèn)系統(tǒng)某些文件。實(shí)例代碼如下:
import java.io.*;
import java.nio.file.*;
import javax.script.*;
import jdk.nashorn.api.scripting.*;
public class Main {
public static void main(String[] args) throws Exception {
ScriptEngineManager m = new ScriptEngineManager();
ScriptEngine e = m.getEngineByName("nashorn");
if (args.length == 0) {
System.err.println("Usage: java Main <script_file>");
return;
}
// args[0] is script file to which permissions are granted
// in security policy
File file = new File(args[0]);
// read the file content and pass a String to 'eval'
// The script is untrusted as nashorn does not know the origin!
try {
e.eval(new String(Files.readAllBytes(file.toPath())));
} catch (SecurityException se) {
System.out.println(se);
}
// create a Reader over the file and pass to 'eval'
// The script is untrusted as nashorn does not know the origin!
try {
e.eval(new FileReader(file));
} catch (SecurityException se) {
System.out.println(se);
}
// pass a URLReader on file - script will get permissions
// configured in security policy!
e.eval(new URLReader(file.toURL()));
}
}
在代碼中我們首先創(chuàng)建了一個(gè)nashorn的js引擎,讀取文件,并通過(guò)eval執(zhí)行文件中的代碼。可以看出,如果我們不限制的話(huà),則造成了任意代碼執(zhí)行漏洞,這里假設(shè)js文件的內(nèi)容如下
test.js
var File = java.io.File;
// list contents of the current directory!
for each (var f in new File(".").list())
print(f)
意思是通過(guò)java的File對(duì)象,獲取當(dāng)前文件夾的所有文件夾和文件并打印出來(lái)。下面我們可以通過(guò)編寫(xiě)policy文件去限制這個(gè)行為
test.policy
/ give AllPermission for Main class (or any class in that directory!)
grant codeBase "file:///d:/test" {
permission java.security.AllPermission;
};
// give AllPermission to test.js script
grant codeBase "file:///d:/test/test.js" {
permission java.security.AllPermission;
};
使用如下命令加載并運(yùn)行
java -Djava.security.manager -Djava.security.policy=./test.policy Main test.js
這時(shí)如果再執(zhí)行的話(huà),因?yàn)榘踩呗缘南拗?,則會(huì)報(bào)錯(cuò)
java.security.AccessControlException: access denied ("java.io.FilePermission" "." "read")
java.security.AccessControlException: access denied ("java.io.FilePermission" "." "read")
Main.class
Main.java
test.js
test.policy
在這里安全策略的權(quán)限是指允許代碼執(zhí)行的操作。包含三部分:權(quán)限類(lèi)型、權(quán)限名和允許的操作。權(quán)限類(lèi)型是實(shí)現(xiàn)了權(quán)限的Java類(lèi)名,是必需的。權(quán)限名一般就是對(duì)哪類(lèi)資源進(jìn)行操作的資源定位(比如一個(gè)文件名或者通配符、網(wǎng)絡(luò)主機(jī)等),一般基于權(quán)限類(lèi)型來(lái)設(shè)置,有的比如java.security.AllPermission不需要權(quán)限名。允許的操作也和權(quán)限類(lèi)型對(duì)應(yīng),指定了對(duì)目標(biāo)可以執(zhí)行的操作行為,比如讀、寫(xiě)等。如下面的例子:
| 類(lèi)型 | 權(quán)限名 | 操作 | 例子 | |
|---|---|---|---|---|
| 文件權(quán)限 | java.io.FilePermission | 文件名(平臺(tái)依賴(lài)) | 讀、寫(xiě)、刪除、執(zhí)行 | 允許所有問(wèn)價(jià)的讀寫(xiě)刪除執(zhí)行:permission java.io.FilePermission "<< ALL FILES>>", "read,write,delete,execute";。允許對(duì)用戶(hù)主目錄的讀:permission java.io.FilePermission "${user.home}/-", "read";。 |
| 套接字權(quán)限 | java.net.SocketPermission | 主機(jī)名:端口 | 接收、監(jiān)聽(tīng)、連接、解析 | 允許實(shí)現(xiàn)所有套接字操作:permission java.net.SocketPermission "<em>:1-", "accept,listen,connect,resolve";。允許建立到特定網(wǎng)站的連接:permission java.net.SocketPermission "</em>.abc.com:1-", "connect,resolve";。 |
| 屬性權(quán)限 | java.util.PropertyPermission | 需要訪(fǎng)問(wèn)的jvm屬性名 | 讀、寫(xiě) | 讀標(biāo)準(zhǔn)Java屬性:permission java.util.PropertyPermission "java.<em>", "read";。在sdo包中創(chuàng)建屬性:permission java.util.PropertyPermission "sdo.</em>", "read,write";。 |
| 運(yùn)行時(shí)權(quán)限 | java.lang.RuntimePermission | 多種權(quán)限名[見(jiàn)附錄A] | 無(wú) | 允許代碼初始化打印任務(wù):permission java.lang.RuntimePermission "queuePrintJob" |
| AWT權(quán)限 | java.awt.AWTPermission | 6種權(quán)限名[見(jiàn)附錄B] | 無(wú) | 允許代碼充分使用robot類(lèi):permission java.awt.AWTPermission "createRobot"; permission java.awt.AWTPermission "readDisplayPixels";。 |
| 網(wǎng)絡(luò)權(quán)限 | java.net.NetPermission | 3種權(quán)限名[見(jiàn)附錄C] | 無(wú) | 允許安裝流處理器:permission java.net.NetPermission "specifyStreamHandler";。 |
| 安全權(quán)限 | java.security.SecurityPermission | 多種權(quán)限名[見(jiàn)附錄D] | 無(wú) | |
| 序列化權(quán)限 | java.io.SerializablePermission | 2種權(quán)限名[見(jiàn)附錄E] | 無(wú) | |
| 反射權(quán)限 | java.lang.reflect.ReflectPermission | uppressAccessChecks(允許利用反射檢查任意類(lèi)的私有變量) | 無(wú) | |
| 完全權(quán)限 | java.security.AllPermission | 無(wú)(擁有執(zhí)行任何操作的權(quán)限) | 無(wú) |
</tbody></table>
策略文件的編寫(xiě)
策略文件是控制沙箱的管理要素,一個(gè)策略文件包含一個(gè)或多個(gè)保護(hù)域的項(xiàng)。策略文件完成了代碼權(quán)限的指定任務(wù),策略文件包括全局和用戶(hù)專(zhuān)屬兩種。
為了管理沙箱,策略文件我認(rèn)為是最重要的內(nèi)容。JVM可以使用多個(gè)策略文件,不過(guò)一般兩個(gè)最常用。一個(gè)是全局的:$JREHOME/lib/security/java.policy,作用于JVM的所有實(shí)例。另一個(gè)是用戶(hù)自己的,可以存儲(chǔ)到用戶(hù)的主目錄下。策略文件可以使用jdk自帶的policytool工具編輯。
2. stackoverflow給出的解決方案
在jdk 1.8u40中,可以使用ClassFilter去限制js引擎可以訪(fǎng)問(wèn)的類(lèi)。代碼如下:
import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
public class MyClassFilterTest {
class MyCF implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
if (s.compareTo("java.io.File") == 0) return false;
return true;
}
}
public void testClassFilter() {
final String script =
"print(java.lang.System.getProperty(\"java.home\"));" +
"print(\"Create file variable\");" +
"var File = Java.type(\"java.io.File\");";
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(
new MyClassFilterTest.MyCF());
try {
engine.eval(script);
} catch (Exception e) {
System.out.println("Exception caught: " + e.toString());
}
}
public static void main(String[] args) {
MyClassFilterTest myApp = new MyClassFilterTest();
myApp.testClassFilter();
}
}
執(zhí)行的話(huà),則會(huì)報(bào)錯(cuò),如下所示
C:\Java\jre8
Create file variable
Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
java.io.File
3.nashornsandbox沙箱方案
A secure sandbox for executing JavaScript in Java apps using the Nashorn engine.
通過(guò)下面的代碼,即可控制nashorn引擎可以訪(fǎng)問(wèn)/拒絕某些類(lèi)了,簡(jiǎn)單易用
NashornSandbox sandbox = NashornSandboxes.create();
sandbox.allow(File.class);
sandbox.eval("var File = Java.type('java.io.File'); File;")
并且這個(gè)沙箱的作用很大,不光可以限制java類(lèi)的訪(fǎng)問(wèn),還可以限制nashorn引擎的資源使用情況,如下
NashornSandbox sandbox = NashornSandboxes.create();
sandbox.setMaxCPUTime(100);
sandbox.setMaxMemory(50*1024);
sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30); // because preparing scripts for execution is expensive
sandbox.setExecutor(Executors.newSingleThreadExecutor());
sandbox.eval("var o={}, i=0; while (true) {o[i++]='abc';};");