【安全工具】浅谈编写Java代码审计工具
介紹
筆者是大四學(xué)生,初涉安全的萌新,如果文章有錯(cuò)誤之處還請(qǐng)大佬指出!
最初考慮采用純正則等方式匹配,但這種方式過(guò)于嚴(yán)格,程序員編寫的代碼有各種可能的組合
于是嘗試自行實(shí)現(xiàn)Java詞法分析和語(yǔ)法分析,稍作嘗試后發(fā)現(xiàn)這不現(xiàn)實(shí),一方面涉及到編譯原理的一些算法,另外相比C語(yǔ)言等,Java語(yǔ)言本身較復(fù)雜,不是短時(shí)間能搞定的,深入研究編譯原理背離了做審計(jì)工具的目的
后來(lái)找到了幾種解決方案:Antlr,JavaCC,JDT,javaparser
經(jīng)過(guò)對(duì)比,最終選擇javaparser項(xiàng)目,該項(xiàng)目似乎是基于JavaCC,核心開(kāi)發(fā)者是effective java的作者。使用起來(lái)比較方便,可以簡(jiǎn)單地以依賴的方式導(dǎo)入
<dependency><groupId>com.github.javaparser</groupId><artifactId>javaparser-symbol-solver-core</artifactId><version>3.23.0</version> </dependency>筆者本想采用Golang編寫該工具,查找相關(guān)資料后發(fā)現(xiàn),Golang本身提供AST庫(kù),可以對(duì)Golang本身做語(yǔ)法分析,但找不到實(shí)現(xiàn)Java語(yǔ)法分析的庫(kù)(考慮后續(xù)復(fù)習(xí)下編譯原理自己嘗試)
實(shí)例
javaparser最根本的類是CompilationUnit,如果我們想對(duì)代碼做分析,首先需要實(shí)例化該對(duì)象
// code是讀入的java代碼字符串 // 也有其他重載,但這個(gè)比較方便 CompilationUnit compilationUnit = StaticJavaParser.parse(code);給出一段最簡(jiǎn)單的XSS代碼
package testcode.xss.servlets;import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;public class Demo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String param = req.getParameter("xss");resp.getWriter().write(param);} }針對(duì)于該案例,我們寫審計(jì)工具的原理
- 從import來(lái)看,比如有request和response,以證明這是HttpServlet
- 從類來(lái)看,必須繼承自HttpServlet才能證明這是Servlet
- 如果req.getParameter得到的值被write了,認(rèn)為這是XSS
驗(yàn)證導(dǎo)包
關(guān)于驗(yàn)證導(dǎo)入包的情況,簡(jiǎn)單做了一個(gè)方法
public static boolean isImported(CompilationUnit compilationUnit, String fullName) {// lambda表達(dá)式中必須用這種方式修改值final boolean[] flag = new boolean[1];compilationUnit.getImports().forEach(i -> {if (i.getName().asString().equals(fullName)) {flag[0] = true;}});return flag[0]; }如果要驗(yàn)證請(qǐng)求和相應(yīng)包的導(dǎo)入情況
final String SERVLET_REQUEST_IMPORT = "javax.servlet.http.HttpServletRequest"; final String SERVLET_RESPONSE_IMPORT = "javax.servlet.http.HttpServletResponse";boolean imported = isImported(compilationUnit, SERVLET_REQUEST_IMPORT) &&isImported(compilationUnit, SERVLET_RESPONSE_IMPORT); if (!imported) {logger.warn("no servlet xss");return results; }獲得類節(jié)點(diǎn)
首先拿到Demo這個(gè)Class,因?yàn)橐粋€(gè)java文件中不一定只有一個(gè)類
compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()// 不是接口且不是抽象類.filter(c->!c.isInterface()&&!c.isAbstract()).forEach(c->{System.out.println(c.getNameAsString());});// 輸出 // Demo進(jìn)一步,我們需要判斷該類是否繼承自HttpServlet
compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream().filter(c->!c.isInterface()&&!c.isAbstract()).forEach(c->{boolean isHttpServlet = false;// 繼續(xù)用lambda反而不方便NodeList<ClassOrInterfaceType> eList = c.getExtendedTypes();for (ClassOrInterfaceType e:eList){if (e.asString().equals("HttpServlet")){isHttpServlet = true;break;}}if (isHttpServlet){// 這里面做進(jìn)一步的邏輯System.out.println("hello");}});只有得到類節(jié)點(diǎn),才可以繼續(xù)遍歷抽象語(yǔ)法樹(shù)拿到方法等信息
獲得方法
遍歷得到方法節(jié)點(diǎn),并且拿到具體的請(qǐng)求和響應(yīng)參數(shù)名稱
之所以要拿到方法參數(shù)名,是為了做進(jìn)一步的追蹤
if (isHttpServlet){c.getMethods().forEach(m->{// lambda不允許直接復(fù)制,所以借助mapMap<String,String> params = new HashMap<>();m.getParameters().forEach(p->{// resp(真實(shí)情況未必一定是resp)if (p.getType().asString().equals("HttpServletResponse")) {params.put("response", p.getName().asString());}// req(真實(shí)情況未必一定是req)if (p.getType().asString().equals("HttpServletRequest")) {params.put("request", p.getName().asString());}});System.out.println("request:"+params.get("request"));System.out.println("response:"+params.get("response"));}); }// 輸出 // request:req // response:resp確認(rèn)參數(shù)可控
審計(jì)漏洞的關(guān)鍵點(diǎn)就在于參數(shù)的可控,這也是難點(diǎn)
就本案例而言,如果某個(gè)參數(shù)是req.getParameter("…")獲取的,那么就可以認(rèn)為是可控
實(shí)際上這個(gè)req并不一定是req,可能是request,requ等,這也是上一步需要一個(gè)map保存的原因
可以加上參數(shù)校驗(yàn)
if (params.get("request") != null && !params.get("request").equals("") ||params.get("response") != null && !params.get("response").equals("")) {return; }獲取所有的賦值表達(dá)式,確定是否調(diào)用了req.getParameter這樣的參數(shù)
并且參考上文的方式使用map保存這個(gè)參數(shù)結(jié)果,用于后續(xù)校驗(yàn)
Map<String,String> var = new HashMap<>(); m.findAll(VariableDeclarationExpr.class).forEach(v->{MethodCallExpr right;boolean isGetParam = false;// 獲取賦值語(yǔ)句右邊部分if (v.getVariables().get(0).getInitializer().get() instanceof MethodCallExpr) {// 強(qiáng)轉(zhuǎn)不驗(yàn)證會(huì)出問(wèn)題right = (MethodCallExpr) v.getVariables().get(0).getInitializer().get();if (right.getScope().get().toString().equals(params.get("request"))){// 確定是否調(diào)用了req.getParameterif (right.getName().asString().equals("getParameter")){isGetParam = true;}}}if(isGetParam){var.put("reqParameter",v.getVariables().get(0).getNameAsString());logger.info("find req.getParameter");} });確定觸發(fā)點(diǎn)
觸發(fā)點(diǎn)在本案例中是resp.getWriter().write()
這是一個(gè)方法調(diào)用,所以搜索MethodCallerExpr
m.findAll(MethodCallExpr.class).forEach(im -> {if (im.getScope().get().toString().equals(params.get("response"))) {// 如果調(diào)用了response.getWriterif (im.getName().asString().equals("getWriter")) {MethodCallExpr method;// 直接強(qiáng)轉(zhuǎn)會(huì)出問(wèn)題if (im.getParentNode().get() instanceof MethodCallExpr) {// 后一步方法method = (MethodCallExpr) im.getParentNode().get();} else {return;}// response.getWriter.write();if (method.getName().asString().equals("write")) {// 該案例中write的是常量param,所以搜NameExprmethod.findAll(NameExpr.class).forEach(name -> {// 這里用到了之前保存在map的reqParameterif (name.getNameAsString().equals(var.get("reqParameter"))) {// 認(rèn)為存在XSSlogger.info("find xss");}});}}} });針對(duì)于這個(gè)基礎(chǔ)案例,可以再加入幾個(gè)規(guī)則,針對(duì)于response.getOutputStream方式
if (im.getName().asString().equals("getOutputStream")) {MethodCallExpr method;if (im.getParentNode().get() instanceof MethodCallExpr) {method = (MethodCallExpr) im.getParentNode().get();} else {return;}// response.getOutputStream.print();// response.getOutputStream.println();if (method.getName().asString().equals("print") ||method.getName().asString().equals("println")) {method.findAll(NameExpr.class).forEach(name -> {if (name.getNameAsString().equals(var.get("reqParameter"))) {logger.info("find xss");}});} }測(cè)試
嘗試讓原來(lái)的XSS代碼復(fù)雜一些,看看審計(jì)的效果
public class Demo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String param = req.getParameter("xss");if(param.equals("hello world")){// do other}else{demoService.doSearch();}int a = 1;int b = 2;logger.log(String.format("%d+%d=%d",a,b,a+b));try{// todo}catch (Exception e){e.printStackTrace();}resp.getWriter().write(param);} }運(yùn)行后成功檢測(cè)到XSS
結(jié)尾
這篇文章只針對(duì)最基本的Servlet XSS做了審計(jì),實(shí)際上無(wú)論從廣度還是深度,都有巨大的工作量:
- 廣度:SQL注入,XXE,反序列化,文件上傳,CSRF等漏洞的審計(jì)
- 深度:如果代碼對(duì)Servlet做了一定的封裝,或者需要跨多個(gè)java文件分析
- 可控參數(shù)的追蹤:從controller層傳入?yún)?shù)到返回,這個(gè)參數(shù)經(jīng)歷了些什么
代碼在github:https://github.com/EmYiQing/XVulnFinder
簡(jiǎn)單寫了個(gè)輸出html的頁(yè)面:
最后
【獲取網(wǎng)絡(luò)安全學(xué)習(xí)資料以及工具】可以關(guān)注私我
總結(jié)
以上是生活随笔為你收集整理的【安全工具】浅谈编写Java代码审计工具的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【CTF大赛】陇剑杯-机密内存-解题过程
- 下一篇: “白痴“上帝视角调节反序列化链之CC2