前言

本人并不是很喜欢写工具使用教程,但是每次用都得搜语法,再加上之前找实习的时候几乎只要牵涉到Java安全,就会问有没有使用过自动化代码审计工具,所以干脆写一篇语法总结

反序列化的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
起点:
readObject
java.lang.reflect.InvocationHandler.invoke
readResolve
readExternal

终点:
java.lang.reflect.invoke
Class.forName()
loadClass()
defineClass()
lookup()

常用链:
HashMap.readObject->HashMap.hash()->*.hashCode()
Hashtable.readObject->AbstractMap.equals->*.get()


tabby使用

TABBY是一款针对Java语言的静态代码分析工具。
它使用静态分析框架 Soot 作为语义提取工具,将JAR/WAR/CLASS文件转化为代码属性图。 并使用 Neo4j 图数据库来存储生成的代码属性图CPG。

使用前的配置

怎么安装就不写了,这里只写用法
tabby配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tabby.build.target                        = cases/hessian-onlyJdk.jar
# 有些情况需要某些依赖,但是又不想分析这些依赖,此时可以放到lib目录下
tabby.build.libraries = libs
tabby.build.mode = gadget # 分析类型 web 或 gadget,web模式会剔除常见jar包的全量分析,gadget模式会对target目录下的文件进行全量分析,如果是挖web层面的漏洞可以设置mode为web
tabby.output.directory = ./output/dev

# debug
tabby.debug.details = true

# jdk settings
tabby.build.isJDKProcess = true # 分析过程是否加入基础的2个jdk依赖,这里我们主要挖掘target 目录文件的利用链所以设置isJDKProcess为true
tabby.build.withAllJDK = false # 分析过程是否加入全量的jdk依赖
tabby.build.excludeJDK = false
tabby.build.isJDKOnly = false# 分析过程仅分析jdk依赖,不会去分析target目录下的文件,用于挖掘 jdk 里面的利用链

neo4j配置看官方语雀https://www.yuque.com/wh1t3p1g/tp0c1t/wx6fiha89p0wu6s5#

多个jar包分析

如果是多个jar包可以先合成一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@echo off
setlocal enabledelayedexpansion

set folder=E:\code\tabby\cases\weblogic
rem 设置目标目录
set targetDir=E:\code\tabby\cases\weblogic\directory

rem 创建目标目录,如果它不存在
if not exist "%targetDir%" mkdir "%targetDir%"

rem 切换到目标目录
cd /d "%targetDir%"

rem 遍历当前目录下所有 .jar 文件
for %%F in (%folder%\*.jar) do (
echo 正在解压 %%F...
rem 使用 jar 解压
"E:\Download\java\bin\jar.exe" xf "%%F"
)

echo 解压完成!
pause

jar cf merged.jar -C directory_path .将directory_path下的jar包合成一个merged.jar

运行命令

1
java -Xmx7g -jar tabby.jar #允许命令,7g是分配的内存,不能设置太高了,不然内存爆了还得重新来

基本用法

Cypher 是 Neo4j 的图查询语言,被设计用于查询图数据

漏洞挖掘找可能存在的链子

这里以hessian反序列化为例,演示的内容为0CTF/TCTF 2022 hessian-onlyJdk的一个非预期
我们先找readobject->putval这个已经公认的链子,起始点肯定是com.caucho.hessian.io.Hessian2Input#readObject

1
2
3
4
match (source:Method) where source.NAME in ["readObject"] and source.CLASSNAME="com.caucho.hessian.io.Hessian2Input"
match (sink:Method {IS_SINK:true, NAME:"invoke"})<-[r:CALL]-(m1:Method) where r.REAL_CALL_TYPE in ["java.lang.reflect.Method"]
CALL apoc.algo.allSimplePaths(m1, source ,"<CALL|ALIAS", 9) yield path
return * limit 20

QQ20240921111-19218552
我们再找sink点,这里以任意方法调用为目标(为方便展示,这里只返回一条路径,实践这样的化容易误判)

1
2
3
4
match (source:Method) where source.NAME in ["putVal"] and source.CLASSNAME="java.util.HashMap"
match (sink:Method {IS_SINK:true, NAME:"invoke"})<-[r:CALL]-(m1:Method) where r.REAL_CALL_TYPE in ["java.lang.reflect.Method"]
CALL apoc.algo.allSimplePaths(m1, source ,"<CALL|ALIAS", 9) yield path
return * limit 1

QQ20241001-043900

ctf比赛找黑名单绕过

1
2
3
4
match (source:Method) where source.NAME in ["readObject"] and source.CLASSNAME="javax.activation.URLDataSource"
match (sink:Method {NAME:"toString"})<-[r:CALL]-(m1:Method)
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 6) yield path where none(n in nodes(path) where (n.CLASSNAME =~ "javax.management.*" or n.CLASSNAME =~ "com.alibaba.fastjson.*" ))
return * limit 10

web层次找可能的未授权

1
2
3
4
MATCH (source:Method) WHERE source.NAME in ["doGet","doPost"]
MATCH (sink:Method{NAME:"doUploadBrithdayCard"})<-[:CALL]-(m1:Method)
CALL apoc.algo.allSimplePaths(m1, source ,"<CALL|ALIAS", 8) yield path
RETURN * limit 20

总结

它这个工具找到其实挺全的,但是容易跑偏,毕竟没有考虑参数传递,只考虑了方法调用,所以我们最好一段一段去找,当成一个思路扩展和日常学习的辅助工具还是不错的

codeQL使用

CodeQL支持进行漏洞挖掘的语言包括:C/C++、C#、Go、Java、JavaScript、Python、Ruby。其针对这些不同的语言有着不同的规则编写规范,所以相比tabby,CodeQL使用起来也更加复杂

使用前的准备

创建codeql数据库

在使用 CodeQL 分析代码之前,需要创建一个 CodeQL 数据库

这条命令需要在需要审计的源代码目录中使用,如果要审计jar包,需要把jar包解压成项目文件

1
2
3
4
5
6
7
8
codeql database create ./xxx_db --language=xxx
#java需要lib库的,要加command参数运行一下mvn安装依赖
CodeQL database create quartz_db --language="java" --command="mvn clean install --file pom.xml -Dmaven.test.skip=true"


#对与单个jar/war文件使用CodeQLpy生成数据库
python3 main.py -t xxx.jar -c
CodeQL database create quartz_db --language="java" --command="run.cmd" --overwrite

使用vscode插件分析数据库

vscode打开ql\java\ql\examples\snippets目录,vscode插件选中生成数据库并选择java语言
随后编写脚本,点击 运行,就可以查看查找结果了

此外还支持命令行的形式运行
codeql database analyze ./quartz_db –format=csv –output=result.csv
codeql database analyze ./quartz_db ./CodeQL/ql/java/ql/examples/demo –format=csv –output=result.csv

基本使用

基本语法

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
类型
RefType Class或Interface

hasQualifiedName("javax.naming", "Context") 类的名字叫javax.naming.Context
getACallable() 获取所有可以调用方法(其中包括构造方法)
getAMember() 获取所有成员,其中包括调用方法,字段和内部类
getAField() 获取所有字段
getAMethod() 获取所有方法
getASupertype() 获取父类
getAnAncestor() 获取所有的父类相当于递归的getASupertype*()


method

this.hasName("parse") 名字叫parse
getNumberOfParameters() 参数数量
getParameter(0) 获取第一个形参
fromSource() 来自源代码,而不是从其他地方(如库或编译生成的代码)
getDeclaringType() 获取的是当前方法所属class的名称

简单地说, Callable是可以被调用的东西, Call是调用Callable的东西。他俩都属于method
Callable 可以调用方法
polyCalls(Callable target) 一个Callable 是否调用了另外的Callable,这里面包含了类似虚函数的调用
hasName(name) 可以对方法名进行限制

Call
getCallee() 返回函数声明的位置,及转换成method
getCaller() 返回调用这个函数的函数位置

实例

收集了一些QL

寻找特定类或方法

寻找public构造方法且该构造方法只有一个String类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java //导入codeql的java依赖

//where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数;这个函数,就叫谓词
predicate isparam(Method method) {
method.getNumberOfParameters() = 1
and
method.getParameter(0).getType().getName() = "String"
}//第一个参数类型是String

from Constructor c //c是一个构造方法
//获取声明该构造方法的类类型, 检查这个构造方法是否是公共的
where c.getDeclaringType().getAMethod().isPublic()
and isparam(c)
and c.fromSource()
and c.getName() != ""

select c //输出查找结果
寻找Lookup调用getter,setter方法,及证明jndi
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
import java

class LookupMethod extends Call {
LookupMethod() {
this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
this.getCallee().hasName("lookup")
}
}

class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
hasNoParameters() and
getName().length() > 3
or
getName().matches("set%") and
getNumberOfParameters() = 1
}
}

query predicate edges(Callable a, Callable b) { a.polyCalls(b) }

from LookupMethod endcall, GetterCallable entryPoint, Callable endCallAble
where
endcall.getCallee() = endCallAble and
edges+(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Geter jndi"
spring框架中MyBatis SQL注入
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
import java
import DataFlow2::PathGraph
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking2

class AllControllerMethod extends Method{ //获取所有Controller中的方法
AllControllerMethod(){
exists(
RefType rt |
rt.getName().indexOf("Controller")>0 and //万一不叫control就寄了
this = rt.getACallable()
)
}
}

class MyBatisMapperXmlFile extends XmlFile { //获取MyBatis MapperXML 文件
MyBatisMapperXmlFile(){
count( XmlElement e|e = this.getAChild()) = 1 and
this.getAChild().getName()="mapper"//mapper的子标签
}
}

class MyBatisMapperXmlElement extends XmlElement {//获取到标签中的值
MyBatisMapperXmlElement(){
this.getFile() instanceof MyBatisMapperXmlFile
}
string getId() {
result = this.getAttribute("id").getValue() //获取id属性的值,代表的是方法名
}
string getValue() {
result = this.getAChild*().allCharactersString().trim() //加个*是递归的意思,递归获取元素中的值(大量重复),父标签会包含子标签的值
}
}

class MyBatisMapperVulMethod extends Method{ //寻找 ${}对应的方法
MyBatisMapperVulMethod(){
exists(MyBatisMapperXmlElement mmxe|
mmxe.getValue().indexOf("${")>0 and //找到${}的标签
this.hasName(mmxe.getId()) and //方法名相同
this.getDeclaringType().getASubtype*().getQualifiedName() = //递归获取父类名 是否和xml中namespace相等,则证明类名相同
mmxe.getFile().getAChild().getAttribute("namespace").getValue()
)
}
}

//数据流分析

class MyTaintTrackingConfiguration extends TaintTracking2::Configuration{
MyTaintTrackingConfiguration(){
this = "MyTaintTrackingConfiguration"
}
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource or
exists(Method m |
m instanceof AllControllerMethod and
source.asParameter() = m.getAParameter() //类名,参数都相等,将Controller的方法参数当作source
)
}

override predicate isSink(DataFlow::Node sink) { //sink点为使用了 ${...} 的XML文件对应的方法的方法调用中的任意一个参数
exists(MethodCall call, MyBatisMapperXmlElement mmxe, string
unsafeExpression |
call.getMethod() instanceof MyBatisMapperVulMethod and
unsafeExpression = mmxe.getValue().regexpFind("(\\$)\\{[^\\}]*\\}",//sink处方法调用的参数一定得是使用了${...}的
_, _) and
(sink.asExpr().toString() = unsafeExpression.substring(2,
unsafeExpression.length()-1) or
//形如criterion.condition这种参数,应以"."作为分隔符,分别判断
sink.asExpr().toString() = unsafeExpression.substring(2,
unsafeExpression.length()-1).splitAt(".")) and
sink.asExpr() = call.getAnArgument()
)
}
}

from MyTaintTrackingConfiguration config, DataFlow2::PathNode source,DataFlow2::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(),source, sink, "sqlInject!"

官方的sql注入查询的ql,codeql-main\java\ql\src\experimental\Security\CWE\CWE-089\MyBatisMapperXmlSqlInjection.ql
可以看官方文档,https://codeql.github.com/codeql-query-help/go/

总结

codeql在找java链子方面功能要比tabby强大,tabby只能按照开发者设置规则来找链子,但是codeql可以自己设计规则,如找固定参数类型的方法,或者找实现某个接口的类,找符合某种自定义规则的方法