利用Java SecurityManager进行权限控制

说明

上一节对使用策略文件来定义危险函数的用法进行了说明,本文主要是通过代码的方式对权限进行控制。关于SecurityManager的机制可以具体看看java沙箱绕过中的Java Security Manager介绍章节。
具体来说,SecurityManager可以对JAVA中的诸如文件访问、命令执行、反射的方法的精准控制。

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

其中Java代码,

1
2
3
4
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}

就是利用SecurityManager对权限进行校验。

命令执行的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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对权限进行校验

1
2
3
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExec(prog);

反射的控制

java.lang.Class:getDeclaredMethods()

1
2
3
4
public Method[] getDeclaredMethods() throws SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
return copyMethods(privateGetDeclaredMethods(false));
}

跟踪进入到java.lang.Class:checkMemberAccess():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()对操作的校验。

自定义SecurityManager

通过上述的示例演示,可以知道SecurityManager在很多关键的位置都进行了动作的校验。在上一节中,我们通过策略文件同样就是对一些关键操作进行了权限定义。当JAVA程序运行至该操作时就会检查此权限。当然我们也可以通过自定义SecurityManager来实现对某些文件的访问控制、某些操作的访问控制。
如果我们需要实现自定义的访问控制,我们需要继承SecurityManager类,然后在其中实现自己的权限控制的方法。在java.lang.SecurityManager中定义了很多的权限检测的方法,包括checkConnect()checkDelete()checkExec()checkListen()checkRead()checkPropertiesAccess()等等。所有的这些方法都会最终调用checkPermission()。所以如果我们要实现自定义的访问控制,那么我们就可以尝试重载checkPermission()方法。

对文件的访问控制

以文件访问控制为例:

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

这样写的比较的复杂。因为在SecurityManager中直接存在checkRead()方法用于对访问文件的控制,所以我们也可以选择直接重载checkRead()方法。如下:

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

运行程序之后就会抛出SecurityException的错误。

限制命令执行

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

运行上述的程序就会抛出SecurityException错误。

以上就是一个简单的Demo。这个仅仅只是实现了对calc.exe的禁止,如果要实现对其他方法的限制,上述Demo的方式是明显不行的。下面是相对来说一个禁止命令执行的通用版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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来对权限进行控制。因为最终的命令执行其实最终都会调用本地文件来执行代码,所以通过对FilePermission的检测,判断是否存在execute的动作,从而禁止命令执行。

其他

通过这种防护是不是就一定万无一失了呢?如果setSecurityManager被攻击者设置为null,这样就导致了我们所有的安全检查全部失效了,所以我们也需要保护我们自定义的SecurityManager。如下:

1
2
3
4
5
6
7
8
9
10
11
12
class MySecurityManager extends SecurityManager {
@Override
public void checkPermission(Permission perm) {
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}
}

当我们设置了之后,我们通过检查setSecurityManager方法禁止其他人对SecurityManager进行设置。

总结

总的来说,当需要执行第三方的未知代码时,使用SecurityManager来设置一些白名单、黑名单也是一个非常好的方法。至于如何到底是选择策略文件还是通过代码的方式来实现,主要是看自己项目的需求。