參考:使用太阿(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.Resourceprivate 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);}@Overridepublic 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 analysisanalysisClass: pascal.taie.analysis.extractapi.TestMethodid: 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注入漏洞