java自动化代码审计

java自动化审计

codeql

CodeQL是一个免费开源的代码语义分析引擎(GitHub购买之后的开源项目),其利用QL语言对代码、执行流程等进行“查询”,以此实现对代码的安全性白盒审计,进行漏洞挖掘。

靶场

在网上找到的一个简单的靶场,spring的项目,使用的jdk1.8。

codeql安装

到目前为止,codeql已经更新到v2.7.3版本,

  1. 下载codeql:下载地址:Releases · github/codeql-cli-binaries · GitHub

  2. 放入环境变量:export PATH=/Home/CodeQL/codeql:$PATHsource /etc/profile,方便我们后期使用vscode插件。

  3. 下载codeql sdk:git clone https://github.com/Semmle/ql

  4. 下载codeql vsc插件:

只需要几步下载就可以完成codeql环境的部署

生成codeql数据库

codeql并不提供语法树解析,只是提供对外查询的接口,所以我们需要给他生成database帮助其进行查询(这里检测的是我在网上clone下的靶场)。

codeql database create databasePath  --language="java"  --command="mvn clean install --file pom.xml" --source-root=sourcePath

databasePath:数据库路径(要保存到哪)

sourcePath:源文件代码存放路径

执行完之后就会生成对应的数据库,这里需要注意,mvn的路径问题。

测试语句

  1. 首先用vscode打开codeql sdk,在ql/java/ql/src下创建一个test.ql来编写测试脚本。
  2. 在codeql选项卡中打开之前创建的数据库
  3. 在test.ql中编写 select "hello word",右击选择执行ql,选择测试的数据库

如果执行完毕之后出现如下洁面,则说明环境搭建完毕

到此为止就可以在test.ql下编写检测脚本了。

codeql语法

和sql语句很类似,可以通过AST视图查看到当前数据库中的内容:

这里简单介绍基本的使用方法

from [datatype] var
where condition(var = something)
select var

对应下面的

from int i
where i = 1
select i

第一行指定的变量,第二行条件判断,第三行输出内容。

类库可以通过查看AST中的内容进行确定

名称 解释
Method 方法类,Method method表示获取当前项目中所有的方法
MethodAccess 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用
Parameter 参数类,Parameter表示获取当前项目当中所有的参数

这只是举例说明,例如下面这段代码:

import java
 
from Method method
select method

可以查询出当前项目的所有方法。

下面这段是存在过滤的代码:

import java
 
from Method method
where method.hasName("getStudent")
select method.getName(), method.getDeclaringType()

查找名称为getStudent的方法和对应的类

并且codeql提供了一种谓词的方法帮助来分割复杂的逻辑代码:

import java
 
predicate isStudent(Method method) {exists(|method.hasName("getStudent"))}
 
from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType()

详细的语法内容可以参考:Basic query for Java and Kotlin code — CodeQL

污点分析

污点分析可以抽象成一个三元组〈sources, sinks, sanitizers〉的形式, 其中, source即污点源, 代表直接引入不受信任的数据或者机密数据到系统中; sink即污点汇聚点, 代表直接产生安全敏感操作 (违反数据完整性) 或者泄露隐私数据到外界 (违反数据保密性); sanitizer即无害处理, 代表通过数据加密或者移除危害操作等手段使数据传播不再对软件系统的信息安全产生危害.污点分析就是分析程序中由污点源引入的数据是否能够不经无害处理, 而直接传播到污点汇聚点.如果不能, 说明系统是信息流安全的; 否则, 说明系统产生了隐私数据泄露或危险数据操作等安全问题.

简单的理解是,source参数输入的位置,sink危险函数执行的位置,sanitizers过滤函数

通过定义上述三点内容可以定位出一条参数传递链,当然sanitizers可以不存在。

设置source

通过override predicate isSource(DataFlow::Node src) {}设置source,这是大多常用的source入口,包括spring的也在其中。

设置sink

override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
  method.hasName("query")
  and
  call.getMethod() = method and
  sink.asExpr() = call.getArgument(0)
)
}

上述代码是一个谓语,查询方法名为query的方法。

当然,目前检测的是SQL注入,所以当jdbc执行query方法,则为进入了sink。

Flow数据流

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

通过config.hasFlowPath(source, sink)设置source和sink,这样codeql就可以帮助自动检测漏洞,检索调用链。

测试

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph


class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

  override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodAccess call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }
}


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

上述是拷贝的他人写好的,代码也是上面所讲,运行之后就可以看到所有进入query方法的链路了。

可以点击对应的source和sink查看位置,可以看到,codeql已经帮我们整理出了5条调用链

一步步跟踪第一条调用链,可以得出完整的调用链路:

indexLogic.getStudent(username);
indexDb.getStudent(username);
String sql = "select * from students where username like '%" + username + "%'";
jdbcTemplate.query(sql, ROW_MAPPER);

至此,简单的SQL注入就能查询得到。

codeql缺点

虽然codeql的优点非常的多,但是也是存在一个比较明显的缺点:不能直接通过打包好的程序进行代码审计,当你得到的只是一个jar包,那么将会比较棘手,因为现在市面上的反编译程序反编译的结构都是不太满意,在我的使用体验中其实个人感觉idea是算是最完美的了,但是还是可能存在一些反编译出现问题一点语法错误的地方,最终可能导致编译过程中codeql解析的不准确。

Wker_java_audit

从公司回学校又接近三个周的时间了,虽然白天是在上班,但是晚上的时间相对比较宽裕一些,包括现在已经是凌晨了,我还可以在这里敲键盘,主要是工作是在学校进行,我在外面租了一个出租屋,保证自己的工作不会被舍友打扰和打扰到舍友。

在前几天log4j这个洞爆出之前(听说log4j这个在2020年就有人用codeql扫到了),有看到过一篇通过ASM追踪堆栈查看反序列化漏洞的文章,虽然没有完全看完,但是感觉这种自动化代码审计应该是可行的。

所以在回到学校这接近三个周,晚上我都会抽出一点时间实现一下这个自动化代码审计的工具,目前已经简单实现出了一个初代版本,如果反响比较好的话呢我会继续更新。

为了弥补codeql不能够直接检索打包好的程序,所以我在开发了Wker_java_audit,当然名字其实没有想好,暂时先这样子叫吧。

思路

我的思路是这样子的:

  1. 解析jar包,因为打包好的要不是war要不是jar
  2. 解析class文件结构
  3. 反编译class文件,得到方法的语法树,当然其他类似注解,属性之类的都是需要获取的
  4. 优化语法树执行流,其实就是将一些比较笨重的操作进行优化,类似于编译器做的一些优化
  5. 生成java代码,方便进行审计
  6. 编写相应脚本,根据语法树查询危险的数据流向
  7. 脚本中增加过滤,根据过滤函数进行剪枝

大体可以分为上述的7步,也就是将反编译工具和codeql结合起来做到可以实现自动化审计闭源代码的效果。

在这里我还是拿上面的的靶场进行演示。

首先需要准备:

  1. 靶场
  2. Wker_java_audit

使用方法

java -jar DecompileDialog.jar运行起来,运行起来之后将会让你选择项目jar包,我们选择靶场的jar包,程序就会打开。

spring的项目在BOOT-INF\classes中能找到对应的class文件。

首先我们可以先对比一下反编译的效果。

其实效果上的话呢还是比较相似的。

当然反编译这一块的内容我还是做的比较仔细的,就不给大家完全展开看了,最重要的还是要分享对于分析的思路。

codeql是通过类似于sql语句的方式进行检索的,而我还是沿用之前的cheetah,进行更有逻辑的结构分析。

我给大家带了一个例子,是针对于sql注入的,这里详细给大家介绍一些具体是怎样,这里先给大家看一下执行的结果,选择cheetahLangue标签,挑选script下的sqlI.cheetah然后执行。

可以看到,最终也打印出了所有的流程,并且也进行了颜色区分和信息处理。

sqlI.cheetah:

#define filter1=String.valueOf(.*?)
#define filter2=Integer.valueOf(.*?)
function filter(sentence){
	a = StrRe(sentence,filter1);
	if(GetArrayNum(a) != 0){return 0;}
	a = StrRe(sentence,filter2);
	if(GetArrayNum(a) != 0){return 0;}
	return 1;
}
function track(className,methodName){
	array allNode;
	allNode = TrackVarIntoFun(className,methodName,0,"org/springframework/jdbc/core/JdbcTemplate","query",0);
	size = GetArrayNum(allNode);
	if(StrFindStr(GetJavaSentence(allNode[ToInt(size-1)]),".query(",0) != "-1"){
		i = 0;
		print(methodName."参数流动:");
		cc = 7;
		cs = 1;
		while(i < size){
			sentence = GetJavaSentence(allNode[i]);
			if(filter(sentence) == 0){cc = 5;cs = 5;printcolor("想办法绕过此类:",4);}
			if(i == ToInt((size-1))){
				if(cc != 5){cs = 2;cc = 3;}
			}else{}
			if(cc == 5){printcolor("[-]",6);}else{printcolor("[+]",1);}
			printcolor(GetClassName(GetNodeClassName(allNode[i]))."   ",cc);
			printcolor(sentence.StrRN(),cs);
			i = ToInt(i+1);
		}
	}
	return 0;
}
function main(args){
	className = "com/l4yn3/microserviceseclab/controller/IndexController";
	methods = GetAllMethodName(className);
	size = GetArrayNum(methods);
	i = 0;
	while(i < size){
		if(methods[i] != "<init>"){track(className,methods[i]);
}
		i = ToInt(i+1);
	}
}

脚本编写

我来解释一下具体是做了一些什么事情,对于语法不懂的可以看我之前的cheetah渗透测试脚本语言的内容。

首先在main函数中指定我们要检索的class名称,通过支持库中的GetAllMethodName得到所有的方法名称,当然<init><cinit>这两个是构造方法和静态代码块的内容,我们就不看了,如果不是这两个函数,就调用track函数进行分析,track中调用支持库提供的TrackVarIntoFun

TrackVarIntoFun:

参数1:起始类
参数2:起始方法
参数3:起始方法参数下标
参数4:目标方法的类
参数5:目标方法:参数6:目标方法的参数下标
返回值:执行流node数组

通过调用这个函数得到执行流,返回的执行流中包含着class名称和对应的Node,这个Node并不是一个字符串而是一个AST,这个AST通过GetJavaSentence方法可以得到对应的java语句,通过GetNodeClassName可以得到类名称。

下面就是挨个输出,只是输出的时候进行了剪枝,当然这种剪枝并不是完全正确的,但是我咋是没有提供更好的剪枝函数。

这里的剪枝是通过正则匹配Integer.value进行剪枝,当然后期可以自行添加。

如果在路径中存在一处sanitizers,则被阻断,这样就用红色的进行输出。

如果整个路径都没有过滤函数,那最后的sink就用绿色打印出来,这样子会比较直观。

整个脚本没有什么难点,底层的复杂逻辑我已经实现了,但是肯定还是有一些不足的。

我们还可以测试一下里面的XXE漏洞。

当然,脚本内容大相径庭,无非就是起始类和目标类有所更改。

TrackVarIntoFun(className,methodName,0,"javax/xml/parsers/DocumentBuilder","parse",0);

当然,如果有兴趣,你也可以看spring的代码:

目前存在的一些缺陷,如果有反响的话呢之后会修复的。

  1. or 拼接问题,目前看起来很丑
  2. new int{1,2,3} 类似语句解析成分块的问题
  3. 目前只支持追踪参数流向,无法满足很多情况

工作顺利~

5 个赞