参考:使用太阿(Tai-e)进行静态代码安全分析(spring-boot篇三) - 先知社区
1. JavaApi 提取
1.1 分析
预期是提取controller提供的对外API,例如下图中的/sqli/jdbc/vuln
先看一下如何用tai-e去获取router,tai-e的框架工作原理是由Java source code->Soot Jimple IR-> Tai-e IR
后续的pinter anlaysis、taint analysis 都是基于Tai-e IR开展的。
如下是tai-e IR的形式。我们可以根据IR里的注解进行拼接 获取router。
@org.springframework.web.bind.annotation.RestController
@org.springframework.web.bind.annotation.RequestMapping({"/sqli"})
public class org.joychou.controller.SQLI extends java.lang.Object {
private static final org.slf4j.Logger logger;
private static final java.lang.String driver;
@org.springframework.beans.factory.annotation.Value("${spring.datasource.url}")
private java.lang.String url;
@org.springframework.beans.factory.annotation.Value("${spring.datasource.username}")
private java.lang.String user;
@org.springframework.beans.factory.annotation.Value("${spring.datasource.password}")
private java.lang.String password;
@javax.annotation.Resource
private org.joychou.mapper.UserMapper userMapper;
public void <init>() {
[0@L26] invokespecial %this.<java.lang.Object: void <init>()>();
[1@L26] return;
}
@org.springframework.web.bind.annotation.RequestMapping({"/jdbc/vuln"})
public java.lang.String jdbc_sqli_vul(@org.springframework.web.bind.annotation.RequestParam("username") java.lang.String username) {
java.lang.StringBuilder $r0, $r7, $r8, $r10, $r11;
如果要自己看Tai-e IR的形式,可以在配置里边加入ir-dumper: ;
执行后可以在output/tir看具体的结果
1.2 Tai-e 开发一个新的程序分析
由于我们需要的实现不需要依赖指针分析,所以我们开发插件就没必要用指针分析的插件模式。tai-e给我们提供了开发新的程序分析的扩展方式。How to Develop A New Analysis on Tai-e?
tai-e 提供给我们3中扩展模式:
- MethodAnalysis 需要实现
analyze(IR)
方法,这里的输入的IR是每一个method
- ClassAnalysis 需要实现
analyze(Jclass)
方法,这里的输入的IR是每一个Class
- ProgramAnalysis 需要实现
analyze()
方法,因为这里是整个程序的分析,没有参数传入,如果想获取信息可以用World
方法
例子
如果要实现一个自己的Analysis应该如何做?
下边拿一个实现MethodAnalysis
的例子来讲。
首先需要继承 MethodAnalysis
类,并重载analyze
方法。
首先我们需要定义 一个属于自己的ID,比如testmethodanalysis
然后在analyze
里定义要分析的内容,比如现在的代码就是打印所有methodName
package pascal.taie.analysis.extractapi;
import pascal.taie.analysis.MethodAnalysis;
import pascal.taie.config.AnalysisConfig;
import pascal.taie.ir.IR;
public class TestMethod extends MethodAnalysis {
public static final String ID = "testmethodanalysis";
public TestMethod(AnalysisConfig config) {
super(config);
}
@Override
public Object analyze(IR ir) {
//。。。需要分析的内容
System.out.println(ir.getMethod().getName());
return null;
}
}
写完后我们如何让程序运行我们的analyze呢?
找到 resource/tai-e-analyses.yml 加入我们自定义的analysis
- description:描述是做什么的
- analysisClass:指定我们编写的类
- id:对应我们在类里边写的ID,在程序调用时使用
- description: test method analysis
analysisClass: pascal.taie.analysis.extractapi.TestMethod
id: testmethodanalysis
我们加入到资源文件后,需要在程序启动时指定我们的分析有两种方式
- 直接在执行加入参数:-a testmethodanalysis
- 在配置文件options.yml analyses:添加
testmethodanalysis: ;
- 运行查看结果
1.3 获取 Api
通过上边的分析我们可以选择ProgramAnalysis的形式来进行分析,因为我们这个分析需要用到class
和method
两部分。
1.3.1 POJO
我们先定义了2个类来存储路由信息,未来也可以加上parameters信息。下边是定义的2个类。
MethodRouter 用来存储method的path,可以拓展存储parameters。
public record MethodRouter(String methodName,String path) {
}
由于class和method 是1:N的关系,所以我们构建如下对象,来映射class和method关系
public record Router(String className,String classPath,List<MethodRouter> methodRouters){
}
1.3.2 提取api程序分析
由于controller
的注解一般都是Mapping
的形式,我们可以自定义程序获取有Mapping注解的类。
获取所有应用类
因为是对整个program进行分析的,所以我们需要用World
来获取所有应用类
World.get().getClassHierarchy().applicationClasses()
获取含有Mapping注解的Method及Path
获取到Method的Path并存储到MethodRouter对象里
jClass.getDeclaredMethods().forEach(jMethod -> {
//判断method是否有Mapping注解
if (!jMethod.getAnnotations().stream().filter(
annotation -> annotation.getType().matches("org.springframework.web.bind.annotation.\\w+Mapping")
).toList().isEmpty()) {
flag.set(true);
//获取method的注解内容并添加进methodRouter类
MethodRouter methodRouter = new MethodRouter(jMethod.getName(), formatMappedPath(getPathFromAnnotation(jMethod.getAnnotations())));
methodRouters.add(methodRouter);
}
});
注意:tai-e给出的注解需要我们进行一些处理才能获取到注解里的path,下边是代码片段
public String getPathFromAnnotation(Collection<Annotation> annotations) {
ArrayList<String> path = new ArrayList<>();
annotations.stream()
.filter(annotation -> annotation.getType().matches("org.springframework.web.bind.annotation.\\w+Mapping"))
.forEach(annotation -> path.add(Objects.requireNonNull(annotation.getElement("value")).toString()));
return path.size() == 1 ? path.get(0) : null;
}
组建Router对象
通过上边获取到的method path list 和 class 来组建router对象。
Router router = new Router(jClass.getName(), formatMappedPath(getPathFromAnnotation(jClass.getAnnotations())),methodRouters);
routers.add(router);
1.4 结果展示
具体食用方法
下载代码,并移动至spring-boot-3目录下
git clone https://github.com/lcark/Tai-e-demo cd Tai-e-demo/spring-boot-3 git submodule update --init
2. 将java-sec-code文件夹移至与Tai-e-demo文件夹相同目录下
3. 将pojo
和ExtractApi
文件放到src/main/java/pascal.taie/analysis/extractapi 下
4. 添加我们的analysis程序到tai-e main/resources/tai-e-analyses.yml下
5. 构建fatjar包
6. 使用如下命令运行tai-e便可以成功获取到扫描结果java -cp
D:\work\Tai-e\Tai-e\Tai-e\build\tai-e-all-0.5.1-SNAPSHOT.jar pascal.taie.Main --options-file=options.yml
如下图所示,成功获取所有api
2. 添加 Mybatis Sink点
2.2 Mybatis介绍
MyBatis 是一款优秀的持久层框架/半自动的对象关系映射,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
可以看下边的两种形式的例子.
2.2.1 XML形式
2.2.2 注解形式
通过上边的例子我们可以看出 mybatis 执行的sql语句插入的参数有两种形式
#{parameterName}:
#使用预编译,通过 PreparedStatement 和占位符来实现,会把参数部分用一个占位符 ? 替代,而后注入的参数将不会再进行 SQL 编译,而是当作字符串处理。可以避免 SQL 注入漏洞。${parameterName}
:$表示使用拼接字符串,将接受到参数的内容不加任何修饰符拼接在 SQL 中。易导致 SQL 注入漏洞
虽然#可以预防SQL注入,但是在处理orderby、like、in等语句的情况会报错需要特殊处理。
2.3 注解形式分析
由于mybatis的形式是#{}和${}的形式进行参数拼接,这也就导致我们没办法直接将某个函数的parameter当作sink点来检查SQLI,所以需要我们进行判断是否该函数的parameter传入了执行sql语句中是用$进行拼接的,然后加入sink点。
也就是如下代码中的username。
@Select("select * from users where username = '${username}'")
List<User> findByUserNameVuln01(@Param("username") String username);
2.3.1 代码实现
总结下来就是如下的步骤:
1.筛选出存在Mapper(org.apache.ibatis.annotations.Mapper)
注解的类
List<JClass> list = World.get().getClassHierarchy().applicationClasses().toList();
for (JClass jClass : list) {
if (!jClass.getAnnotations().stream().filter(
annotation -> annotation.getType().matches("org.apache.ibatis.annotations.Mapper")
).toList().isEmpty()
}
2.筛选出有Select注解的method(order 、in等暂时没处理).
jClass.getDeclaredMethods().forEach(jMethod -> {
if (!jMethod.getAnnotations().stream().filter(annotation -> annotation.getType().matches("org.apache.ibatis.annotations.Select")).toList().isEmpty()){
}
3.对$进行正则匹配筛选,匹配出里边的内容(username)
String valueFromAnnotation = getValueFromAnnotation(jMethod.getAnnotations());
if (valueFromAnnotation!=null){
if (valueFromAnnotation.contains("$")){
// System.out.println(jMethod);
Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
Matcher matcher = pattern.matcher(valueFromAnnotation);
由于需要从注解里获取value ,我们写了一个method从annotations获取value
public static String getValueFromAnnotation(Collection<Annotation> annotations) {
ArrayList<String> value = new ArrayList<>();
annotations.stream()
.filter(annotation -> annotation.getType().matches("org.apache.ibatis.annotations..*"))
.forEach(annotation -> value.add(Objects.requireNonNull(annotation.getElement("value")).toString()));
return value.size() == 1 ? value.get(0) : null;
}
4.对method的参数进行处理,找到名字和$里的内容一致的参数,组装成为sink点,然后存储进入一个List。
while (matcher.find()) {
String sink = matcher.group(1);
int paramCount = jMethod.getParamCount();
for (int i = 0 ; i< paramCount;i++){
String paramValue = getValueFromAnnotation(jMethod.getParamAnnotations(i));
if (paramValue.contains(sink)){
Sink sink1 = new Sink(jMethod, new IndexRef(IndexRef.Kind.VAR, i,null));
sinkList.add(sink1);
}
}
}
5.在程序加载config sink点后加入我们的mybatis sink点。
这里我们创建了一个静态方法来返回我们找到的sink点。然后就需要加入到程序的sinks中。
这里可以在java/pascal/taie/analysis/pta/plugin/taint/TaintConfig.java
加载config后 加入进去,至于为什么加在这里,可以看下边Taint-config 加载流程
。
Taint-config加载流程
由于我们需要将sink点加入sink list 中。但是我们在 sinkhandler处没办法直接加入list内,由于该字段是final类型。
尝试删除final,发现该类是UnmodifiableCollection
看名字顾名思义是不可以修改的类,所以会报错。
为此我们需要分析tai-e加载sink的流程,找到合适的加入sink点的位置。
1.在TaintAnalysis
setSolver 函数内会用TaintConfig
来加载taint-config
文件。
2.利用jackson 自定义反序列化 读取taintconfig文件
3.查看自定义 Deserializer
类,我们可以看到会deserializerSinks
会把config里的sinks获取出来
4.我们可以看到deserializerSinks
在加载sinks后会将list为不可修改,所以我们在返回前添加我们的sink点。
2.4 XML形式分析
xml形式比上述流程就是多了一个步骤,就是用id寻找method的步骤。如下图,所以此处就不多赘述了。
2.5 结果展示
具体食用方法
1. 下载代码,并移动至spring-boot-3目录下
git clone https://github.com/lcark/Tai-e-demo cd Tai-e-demo/spring-boot-3 git submodule update --init
2. 将java-sec-code文件夹移至与Tai-e-demo文件夹相同目录下
3. 将AddMybatisSinkHandler
移动到java/pascal/taie/analysis/pta/plugin/taint
文件下
在TaintConfig.java里deserializeSinks
加入如下位置加入代码
List<Sink> mybatisSinks = AddMybatisSinkHandler.AddMybatisSink();
sinks.addAll(mybatisSinks);
4. 构建fatjar包
5. 使用如下命令运行tai-e便可以成功获取到扫描结果java -cp
D:\work\Tai-e\Tai-e\Tai-e\build\tai-e-all-0.5.1-SNAPSHOT.jar pascal.taie.Main --options-file=options.yml
成功检测mybatis的sqli注入漏洞