跳到主要内容

代码审计:SyntaxFlow Java 实战之值的搜索与筛选(一)

· 阅读需 19 分钟
Yak Project
网络安全垂直语言团队

SyntaxFlow作为Yaklang的重要功能,可以适配多种应用场景,广受用户青睐

同时也有不少用户经常问我们:

“SyntaxFlow具体是如何在实际应用场景中使用的?”

“复杂的代码段该如何准确地使用SyntaxFlow工具?”

那么接下来的几篇文章,或许能带大家玩转SyntaxFlow代码审计

SyntaxFlow代码分析是由Yaklang.io团队推出的静态代码分析工具,其核心技术在于将高级语言代码转换为静态单赋值(SSA)形式以实现精准分析。简单概括,SyntaxFlow代码分析的工作流程分为两步:

数据存储:将源代码编译为SSA形式并存入数据库。

数据查询:通过SyntaxFlow语法(类似SQL)从数据库中提取目标代码特征,并完成流量分析等操作。

这种“先存后取”的技术思路,使SyntaxFlow能够用一套规则适配多种语言的代码审计。然而,不同编程语言存在显著差异:

· 语言特性差异:例如Java支持注解,而Go不支持;Java是有类语言,而Go不是等。

· 审计场景依赖的扩展信息:例如审计Java XSS漏洞需关联ISP/FreeMarker模版文件;检查配置规范需解析.properties 或 .yaml 文件。

这些差异意味着,除了源代码的SSA形式,还需将语言特性、框架约定、配置文件等上下文信息纳入分析体系。如何通过SyntaxFlow高效检索这些信息,成为实现跨语言审计的关键。

因此本教程将围绕Java展开,通过以下实战场景帮助读者掌握SyntaxFlow在编写适合Java的漏洞审计、代码规范、安全配置等规则。

教程将提供真实代码样本与逐步操作指南,带领读者从零编写SyntaxFlow规则,完成静态代码审计的入门到进阶。

如需更深入了解SyntaxFlow工作原理,欢迎移步官方网站查看《静态代码分析教程》和《SyntaxFlow手册》。

(须知:Java项目一般文件繁杂,但是SyntaxFlow对编译项目并不要求其能够“跑”起来。只要保证其语法是正确的,那么就能够进行编译。因此本教程练习给出的代码对将会是相对完整语法正确的,但不一定能够运行。)

搜索

在正式开始编写规则之前,我们需要先了解一下SyntaxFlow常见的搜索语法。搜索语法很基础也很重要,后续更高阶的数据流分析也都依赖于搜出来的数据。

变量的搜索

我们从最简单的赋值语句入手,以下就是Java中最简单的赋值样例。

public class SearchVariable {
public static void main(String[] args) {
//赋值
String myStr /*左值*/= "Hello World"/*右值*/;
}
}

SyntaxFlow输入名称就能精确通过左值搜索右值。同时也支持使用Glob进行匹配。因此,你可以使用下面的规则进行匹配。

// 全名称匹配
myStr
// Glob匹配
my*
*Str

如下图所示,我们匹配到了结果。但是单纯搜索没有其它用处。我们一般会使用as作为关键字,将搜索到的内容作为变量,方便后续分析。

下面这个规则就是as关键的典型用法。

myStr as $a;

除了Glob语法以外,还支持正则匹配,正则语句需要使用斜杠给包裹起来。比如:

/(?i)str/ as $a

简单方法搜索

变量的搜索很简单,但是在实战中几乎没有用(因为左值都是人为命名的,每个人命名的名字各种各样,因此写出来的规则没有普适性)。实战中更常见的做法是搜索右值。其中方法的搜索将会经常用到。

以下面代码为例:

public class SearchVariable {
public static void main(String[] args) {
//简单方法
exec("ls","-al");
}
}

使用的规则进行搜索:

exec as $method
同时,我们也可以获取这个方法执行的参数。*代表所有,即获取exec方法下的所有参数。
exec(* as $param);
方法参数搜索

如果只是想获取某一项参数呢?那么可以使用逗号分割符。

exec(* as param1,) // 获取第一个参数exec(,* as param2) // 获取第二个参数

如果遇到三个参数,那么就需要使用这种方式:

method(,,*as $param) // 获取第三个参数

这种选择参数的方式很清晰明了,但是在一个函数包含特别多参数的时候就很繁琐,需要写很多的逗号。后续我们还会使用**native call**选择参数,在这里先不介绍。

普通链式调用搜索

在Java代码中,很少见到使用普通方法的。往往使用的是member call,即链式调用一个方法。比如我们常见的命令执行:

public class SearchVariable {
public static void main(String[] args) {
Runtime.getRuntime().exec("ls -al");
}
}

这种情况下,我们可以直接进行搜索:

Runtime.getRuntime().exec as $a

也能使用下面这种方式进行搜索。下面这种方式会将所有以exec结尾的链式调用方法都搜索出来。

.exec as $a

链式调用的参数则与简单的方法调用的参数有些不同。链式调用的方法会将前面的调用者作为第一参数(这也是为什么链式调用的数据流能够连同的原因,关于这部分的内容后面也会进行介绍)。

比如以下规则:

Runtime.getRuntime().exec(* as $params)

它将会获得两个结果,一个是我们在代码中看到的ls -al,另外一个就是前面调用者。

我们点击Undefined-Runtime-getRuntime,就可以看到它的数据流图如下。

深度链式调用搜索

在一些情况下,我们想并不知道链式调用的顺序,因此不能通过全名搜索;想搜索的内容也不是链式调用的最后一个,因此也不能通过.method()的方式进行搜索。

例如,下面这个样例代码是SpringSecurity的链式调用配置:

@Configuration
@EnableWebSecurity
public class InsecureSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/actuator/**","/test").permitAll() // 不安全:放行所有 Actuator 端点
.anyRequest().authenticated() // 其他请求需要认证
.and()
.httpBasic(); // 使用 HTTP Basic 认证
}
}

我们想搜索antMatchers里面的参数,应该怎么搜索呢?答案是使用使用深度链式调用搜索,搜索的语法是**...**。

比如上面的例子就可以通过下面的规则拿到参数。规则http开始,然后递归检测是否匹配到了antMatchers。深度链式递归调用在实战中面对链式调用十分有用。

http...antMatchers(,* as $param);

接下来介绍的就是类的搜索。我们的样例代码如下,SearchClass类中有字段num和方法foo

package lesson1.search;

public class SearchClass {

public int num;

public void foo(){};

public static void main(String[] args) {
SearchClass searchClass = new SearchClass();
System.out.println(searchClass.num);
searchClass.foo();
}
}

一个类即有实例也有声明,我们可以根据规则的需要确定自己要找的是类的实例还是声明。

类的实例

我们可以通过以下方式找到这个类的实例:

SearchClass() as $a

而找这个实例所拥有的字段与方法的规则如下:

SearchClass().num as $num
SearchClass().foo as $foo

类的声明

找类声明的方式只要在类后面加上_declare就可以,如下:

SearchClass_declare as $a

我们还能通过下面的方式找到他们的字段和方法的声明。

SearchClass_declare.num as $num
SearchClass_declare.foo as $foo

注解的搜索

Java的注解(Annotation)是一种元数据形式,用于为Java代码提供额外的信息。它是Java的一大元编程特性,允许开发者在不改变业务逻辑的情况下,对代码进行标记和增强。注解广泛应用于框架开发中,因此我们很有必要学习如何搜索注解。

以下是一段简单的包含注解的SpringBoot项目控制层的代码:

package lesson1.search;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SearchAnnotation {
@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
@PostMapping(value = "/submit", headers = "Content-Type=application/json")
public String handleJsonRequest() {
return "JSON request handled!";
}
}

直接搜索注解

我们想搜索**@GetMapping@PostMapping**怎么办呢?我们可以直接通过名称就能搜索得到注解。

GetMapping as $get;
PostMapping as $post;

不过我们在前文提到,SyntaxFlow的搜索是支持Glob的,因此我们还能这样搜索:

*Mapping as $mapping;

注解参数的搜索

那么我们想要获得**@PostMapping注解中的参数怎么办呢?比如想获取headers**。我们可以使用下面这种方式:

*Mapping as $mapping;
$mapping.headers as $header;

可以看到直接找到了Content-Type=application/json。

与注解相关联的方法搜索

在这个例子中,注解用来标注方法,因此注解就需要和方法产生关联,那么他们是如何关联的呢?答案是使用关键字ref。使用以下规则就能够通过注解找到他们相关联的方法了。

*Mapping as $mapping;
$mapping.__ref__ as $methods;

同样的,我们也希望能够通过方法找到注解。这个时候我们就可以使用关键字annotation,比如我们使用以下规则也能通过handleJsonRequest方法找到headers

handleJsonRequest.annotation.* as $annotation$annotation.headers as $headers

在实战中,往往通过注解找方法比较常用。但是理解了如何通过方法找注解,才能更为深入理解SSA对注解处理的底层原理。实际上注解与方法就是双向索引的,注解可以找到方法,方法也可以找到注解。其实现的原理是通过SSA的OpMake(关于Make的信息可以查看官方理论知识文档)作为容器,使用一个容器存储注解相关的信息,然后通过一个索引到索引到存储方法的容器上面上面。而方法也是通过一个容器存储相关信息并提供一个字段索引到存储注解的容器上面。

分析值的筛选

现在,我们已经初步掌握了如何搜索到我们想要审计的内容,并将其作为SyntaxFlow的变量。接着我们需要对搜索出来的内容进行审计,也可以叫做过滤。我们将这个过程叫做分析值的筛选

分析值筛选的基础语法

分析值筛选基础语法如下,?{condition}为需要满足的条件,当$a满足大括号里面的条件的时候,那么就会将$a的值赋值给**$b**。数据流是从$a到$b,从左往右一条之间像一条河流一样流过去。所以现在是不是对SyntaxFlow(语法流动)这个名字有了更清楚的认识呢^_^。

$a?{condition} as $b;

其实这个语法可以简单理解成if条件判断,当**$a满足...条件时,就赋值给$b**,它的伪代码如下:

if (condition){    $b = $a;}

可以看到,分析值筛选是没有else的,如果需要实现else的逻辑,那么就需要对条件取反。即:

$a?{!condition} as $b;

分析值筛选里面的condition可以填各种形式,其中以字符串判断最为常见。字符串判断有两个关键字,分别为haveany

我们可以看一下下面这个例子

public class VariableFilter {
public static void main(String[] args) {
System.out.println("Hello Yak");
System.out.println("Hello World");
}
}

如果我们想要找到参数中包含hello字符串的内容,规则可以这样写:

.println(*?{have:'Hello'} as $hello)  //同时搜索到"Hello Yak"和"Hello World"

那如果我们想要搜索到既包含Hello并且包含Yak的内容呢?我们可以这样:

.println(*?{have:'Hello',"Yak"} as $hello)

那如果想要搜索字符串包含Yak或者包含World呢?那么可以这样:

.println(*?{any:'Hello',"Yak"} as $hello)

聪明的你肯定想到了,have关键字和any关键字对应的就是处理字符串判断的与和或,我们可以使用下面的伪代码来进行说明。

// $a?{have:"hello","world"} as $b
if ( strings.Contain($a,"hello") && strings.Contain($b,"world") ){
$b=$a
}

// $a?{any:"world","yak"} as $b
if ( strings.Contain($a,"yak") || strings.Contain($b,"world") ){
$b=$a
}

与搜索一样,字符串判断后面跟着的条件也可以是Glob或者正则表达式。比如?{have:a*}或者?{have:'[0-9]+$'}

分析值筛选的嵌套_

分析值筛选可以嵌套使用。为了理解这个嵌套使用,我们还是看刚才这个的例子,这次我们要筛选的是参数包含**"Yak"**字符串的println方法。和刚才不一样的是,这次我们筛选的不是参数,而是方法。规则如下:

.println?{<getActualParams()>?{have:"Yak"}} as $yakMethod;

其中**<getActualParams()>** 为nativecall,这里作用是用来获取println方法的所有实参。关于nativecall,后续还会讲到,可以理解成是SyntaxFlow封装好的"内置函数",可以直接进行调用。

这个规则一共有两层分析值筛选,第一层内容为**?{<getActualParams()>?{have:"Yak"}}** ,用于筛选方法的实参是否包含Yak,第二层为**?{have:"Yak"}**,筛选是否包含"Yak"字符串。

在后续实战中,经常会遇到分析值筛选的嵌套,这十分的有用。

Opcode判断_

Opcode是SSA系统的一个概念。具体内容可以参考官网的SyntaxFlow手册与静态代码分析教程。为了零基础的同学更好上手,这里做一个简单的介绍。在源代码转化成SSA的形式的时候,SSA会通过Opcode将转化不同信息如常量、方法、函数调用等加以区分。因此可以简单理解成,Opcode就是SSA的类型。

我们以实际Java例子做进行讲解:

package lesson1.filter;

public class FilterOpcode {
public static void main(String[] args) {
Password password1 = new Password("123456");
Password password2 = new Password(utils.getPasswd());
}
}

其中password1的参数是写死的密码,也叫硬编码。而password2则是通过一个方法调用获得的密码。通过Opcode,我们就可以实现拿到所有为硬编码的密码了。

Password(*?{opcode:const} as $hardCode)
const代表常量,说明拿到的密码是一个常量。

当然,也可以拿到密码为函数调用的:

Password(*?{opcode:call} as $safePwd)

小试牛刀

在学习完基础搜索和分析值的筛选之后,我们就可以尝试写出一些有意义的规则啦。

规则1: 不安全哈希算法检测

第一个规则用于检测Java代码中是否存在不安全的哈希算法。不安全的哈希算法是指那些已经被证明存在漏洞或弱点,容易受到攻击(如碰撞攻击、预像攻击等)的加密哈希函数。这些算法可能在特定条件下无法保证数据完整性或安全性,因此在现代密码学和安全系统中已被弃用或强烈不推荐使用。

例如,我们常见的哈希算法md5就是不安全的哈希算法。MD5 算法的主要问题是其抗碰撞性弱和安全性不足,这些问题使其不再适合用于任何需要高安全性的场景。尽管 MD5 仍可用于非安全性要求高的场合(如简单的文件校验),但在密码存储、数字签名或数据完整性验证等场景中,还是推荐使用更安全的算法。

除了MD5算法以外,常见的不安全哈希算法还包括:SHA-0、SHA-1、MD4、MD2等

以下例子为常见不安全哈希算法的例子:

package lesson1.weakhash;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;

public class UnsafeHashExample {
public static String md4(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD4");
byte[] messageDigest = md.digest(input.getBytes());
Formatter formatter = new Formatter();
for (byte b : messageDigest) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
public static String md5(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());

Formatter formatter = new Formatter();
for (byte b : messageDigest) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
public static String sha0(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-0");
byte[] messageDigest = md.digest(input.getBytes());

Formatter formatter = new Formatter();
for (byte b : messageDigest) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
public static String sha1(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] messageDigest = md.digest(input.getBytes());

Formatter formatter = new Formatter();
for (byte b : messageDigest) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
}

观察代码发现,我们只要判断MessageDigest.getInstance方法里面的参数是一个const类型的Opcode,并且其字符串是一个弱算法,我们就能够确定其是不安全的哈希算法。

点击Yakit的代码审计->规则管理->新建创建一个新的规则,规则名称叫做检测Java不安全的哈希算法

其中desc用来描述规则,我们选择risk为risky-crypt(不安全加密算法),type为security(安全规范类型规则),level为low(风险等级为低)。

security规则的类型旨在加强代码安全建设,规则所扫描出来的内容需要根据实际业务代码以判断是否是安全的。

然后编写如下规则并保存:
desc(
risk: 'risky-crypt',
type: security,
level:low,
)

MessageDigest.getInstance(*?{opcode:const && any:'MD4','MD5','SHA-0','SHA-1'}) as $weak;
alert $weak for{
message:"检测到Java中不安全的哈希算法。"
}

好了,我们现在成功编写了自己第一条SyntaxFlow规则,除了我们前面介绍的搜索语法和分析值筛选语法以外,这里还多了个alert语法,也就是如果我们筛选到分析值**$weak**的话,那么我们就会执行alert,并将发现到漏洞的提示信息回显到yakit界面上。

现在我们找个项目试一下吧!

以RuoYi项目为例。

git clone https://github.com/yangzongzhuan/RuoYi.git

选择代码审计->项目管理,将RuoYi项目进行编译。

然后选择刚才的规则进行执行:

可以看到成功检测到了弱哈希算法。

值得注意的是,官方内置的检测弱哈希算法的规则比上面写的更为复杂,主要涉及到了数据流分析。比如MessageDigest.getInstance()方法的参数可能是一个函数传进来的参数。关于数据流分析我们后面将会进行讲解。

规则2: SpringBoot 可控输入参数检测

第二个规则专注于检测 SpringBoot 中的可控输入参数,这一过程具有重要意义。许多注入型漏洞的根源在于用户可控的输入参数未得到妥善处理。例如,常见的安全威胁如 SQL 注入、命令注入以及 XXE(XML 外部实体注入)等攻击,往往利用了未经严格校验的用户输入。

下面是SpringBoot项目控制层的代码:

package lesson1.springBootParam;
import com.example.demo.model.User;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/users")
public class SpringBootParamDetect {
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElse(null);
}

@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getId().equals(id)) {
users.set(i, updatedUser);
return updatedUser;
}
}
return null;
}

@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
users.removeIf(user -> user.getId().equals(id));
}
}

控制层一般会有注解*Mapping进行标记,因此我们实现规则的思路从注解入手,并找到和注解相关联的方法,然后找到该方法的参数作为可控输入参数。规则如下:

desc(
type: audit,
level:info,
)

*Mapping.__ref__?{opcode: function} as $start;
$start(*?{opcode: param && !have: this} as $formalParams);

alert $formalParams for{
message:"找到SpringBoot可控输入参数"
}

该规则为审计类型的规则,因此也不作为代码是否安全的指导。审计类型规则往往需要配合其它规则,如命令执行,然后判断可控输入参数到命令执行之间的数据流是否是通畅的来确定是否存在漏洞。

这个规则通过搜索所有以Mapping结尾的注解,然后筛选去和注解相关为方法的值**(?{opcode: function} )作为$start**,然后拿到**$start**中所有不是this的形参。

Java的成员方法在被编译的时候,除了方法默认的参数以外,还会在参数第一位添加"this",因而本规则需要排除掉"this"。

最后的结果:

总结

本期文章分享了Java代码常见的内容如何使用SyntaxFlow搜索,并通过分析值的筛选选出我们想要的内容。这些内容都是后续编写复杂规则的基石,因此很有必要掌握。文章的末尾我们自己实现了两个规则不安全哈希算法的检测SpringBoot可控输入参数检测来巩固本章学习的内容。

后面,我们将会进行Java实战中如何使用数据流分析,真正的写出一个实战化的规则。除此之外,还将会对具备Java特色的内容进行讲解,比如一些框架的支持Mybatis、JSP、FreeMarker......

敬请期待^v^


本文首发于 Yak Project 公众号,阅读原文