【静态分析】在springboot使用太阿(Tai-e)03

参考:使用太阿(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 的形式来进行分析,因为我们这个分析需要用到classmethod两部分。

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
  1. 将java-sec-code文件夹移至与Tai-e-demo文件夹相同目录下

  2. pojoExtractApi文件放到src/main/java/pascal.taie/analysis/extractapi 下

  3. 添加我们的analysis程序到tai-e main/resources/tai-e-analyses.yml下

  1. 构建fatjar包
  1. 使用如下命令运行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
  1. 将java-sec-code文件夹移至与Tai-e-demo文件夹相同目录下

  2. AddMybatisSinkHandler移动到java/pascal/taie/analysis/pta/plugin/taint文件下

在TaintConfig.java里deserializeSinks加入如下位置加入代码

复制代码
List<Sink> mybatisSinks = AddMybatisSinkHandler.AddMybatisSink();
                sinks.addAll(mybatisSinks);
  1. 构建fatjar包
  1. 使用如下命令运行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注入漏洞

相关推荐
龚思凯2 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响4 分钟前
枚举在实际开发中的使用小Tips
后端
on the way 1237 分钟前
行为型设计模式之Mediator(中介者)
java·设计模式·中介者模式
保持学习ing9 分钟前
Spring注解开发
java·深度学习·spring·框架
wuhunyu10 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi10 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
酷爱码14 分钟前
Spring Boot 整合 Apache Flink 的详细过程
spring boot·flink·apache
异常君35 分钟前
Spring 中的 FactoryBean 与 BeanFactory:核心概念深度解析
java·spring·面试
weixin_461259411 小时前
[C]C语言日志系统宏技巧解析
java·服务器·c语言
cacyiol_Z1 小时前
在SpringBoot中使用AWS SDK实现邮箱验证码服务
java·spring boot·spring