【安全工具】浅谈编写Java代码审计工具

【安全工具】浅谈编写Java代码审计工具_第1张图片

介绍

笔者是大四学生,初涉安全的萌新,如果文章有错误之处还请大佬指出!

最初考虑采用纯正则等方式匹配,但这种方式过于严格,程序员编写的代码有各种可能的组合

于是尝试自行实现Java词法分析和语法分析,稍作尝试后发现这不现实,一方面涉及到编译原理的一些算法,另外相比C语言等,Java语言本身较复杂,不是短时间能搞定的,深入研究编译原理背离了做审计工具的目的

后来找到了几种解决方案:Antlr,JavaCC,JDT,javaparser

经过对比,最终选择javaparser项目,该项目似乎是基于JavaCC,核心开发者是effective java的作者。使用起来比较方便,可以简单地以依赖的方式导入


    com.github.javaparser
    javaparser-symbol-solver-core
    3.23.0

笔者本想采用Golang编写该工具,查找相关资料后发现,Golang本身提供AST库,可以对Golang本身做语法分析,但找不到实现Java语法分析的库(考虑后续复习下编译原理自己尝试)

实例

javaparser最根本的类是CompilationUnit,如果我们想对代码做分析,首先需要实例化该对象

// code是读入的java代码字符串
// 也有其他重载,但这个比较方便
CompilationUnit compilationUnit = StaticJavaParser.parse(code);

给出一段最简单的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 {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String param = req.getParameter("xss");
        resp.getWriter().write(param);
    }
}

针对于该案例,我们写审计工具的原理

  • 从import来看,比如有request和response,以证明这是HttpServlet
  • 从类来看,必须继承自HttpServlet才能证明这是Servlet
  • 如果req.getParameter得到的值被write了,认为这是XSS

验证导包

关于验证导入包的情况,简单做了一个方法

public static boolean isImported(CompilationUnit compilationUnit, String fullName) {
    // lambda表达式中必须用这种方式修改值
    final boolean[] flag = new boolean[1];
    compilationUnit.getImports().forEach(i -> {
        if (i.getName().asString().equals(fullName)) {
            flag[0] = true;
        }
    });
    return flag[0];
}

如果要验证请求和相应包的导入情况

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;
}

获得类节点

首先拿到Demo这个Class,因为一个java文件中不一定只有一个类

compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
        // 不是接口且不是抽象类
        .filter(c->!c.isInterface()&&!c.isAbstract()).forEach(c->{
            System.out.println(c.getNameAsString());
        });

// 输出
// Demo

进一步,我们需要判断该类是否继承自HttpServlet

compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
        .filter(c->!c.isInterface()&&!c.isAbstract())
        .forEach(c->{
            boolean isHttpServlet = false;
            // 继续用lambda反而不方便
            NodeList eList  = c.getExtendedTypes();
            for (ClassOrInterfaceType e:eList){
                if (e.asString().equals("HttpServlet")){
                    isHttpServlet = true;
                    break;
                }
            }
            if (isHttpServlet){
                // 这里面做进一步的逻辑
                System.out.println("hello");
            }
        });

只有得到类节点,才可以继续遍历抽象语法树拿到方法等信息

获得方法

遍历得到方法节点,并且拿到具体的请求和响应参数名称

之所以要拿到方法参数名,是为了做进一步的追踪

if (isHttpServlet){
    c.getMethods().forEach(m->{
        // lambda不允许直接复制,所以借助map
        Map params = new HashMap<>();
        m.getParameters().forEach(p->{
            // resp(真实情况未必一定是resp)
            if (p.getType().asString().equals("HttpServletResponse")) {
                params.put("response", p.getName().asString());
            }
            // req(真实情况未必一定是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

确认参数可控

审计漏洞的关键点就在于参数的可控,这也是难点

就本案例而言,如果某个参数是req.getParameter("…")获取的,那么就可以认为是可控

实际上这个req并不一定是req,可能是request,requ等,这也是上一步需要一个map保存的原因

可以加上参数校验

if (params.get("request") != null && !params.get("request").equals("") ||
        params.get("response") != null && !params.get("response").equals("")) {
    return;
}

获取所有的赋值表达式,确定是否调用了req.getParameter这样的参数

并且参考上文的方式使用map保存这个参数结果,用于后续校验

Map var = new HashMap<>();
m.findAll(VariableDeclarationExpr.class).forEach(v->{
    MethodCallExpr right;
    boolean isGetParam = false;
    // 获取赋值语句右边部分
    if (v.getVariables().get(0).getInitializer().get() instanceof MethodCallExpr) {
        // 强转不验证会出问题
        right = (MethodCallExpr) v.getVariables().get(0).getInitializer().get();
        if (right.getScope().get().toString().equals(params.get("request"))){
            // 确定是否调用了req.getParameter
            if (right.getName().asString().equals("getParameter")){
                isGetParam = true;
            }
        }
    }
    if(isGetParam){
        var.put("reqParameter",v.getVariables().get(0).getNameAsString());
        logger.info("find req.getParameter");
    }
});

确定触发点

触发点在本案例中是resp.getWriter().write()

这是一个方法调用,所以搜索MethodCallerExpr

m.findAll(MethodCallExpr.class).forEach(im -> {
    if (im.getScope().get().toString().equals(params.get("response"))) {
        // 如果调用了response.getWriter
        if (im.getName().asString().equals("getWriter")) {
            MethodCallExpr method;
            // 直接强转会出问题
            if (im.getParentNode().get() instanceof MethodCallExpr) {
                // 后一步方法
                method = (MethodCallExpr) im.getParentNode().get();
            } else {
                return;
            }
            // response.getWriter.write();
            if (method.getName().asString().equals("write")) {
                // 该案例中write的是常量param,所以搜NameExpr
                method.findAll(NameExpr.class).forEach(name -> {
                    // 这里用到了之前保存在map的reqParameter
                    if (name.getNameAsString().equals(var.get("reqParameter"))) {
                        // 认为存在XSS
                        logger.info("find xss");
                    }
                });
            }
        }
    }
});

针对于这个基础案例,可以再加入几个规则,针对于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");
            }
        });
    }
}

测试

尝试让原来的XSS代码复杂一些,看看审计的效果

public class Demo extends HttpServlet {

    @Override
    protected 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);
    }
}

运行后成功检测到XSS
【安全工具】浅谈编写Java代码审计工具_第2张图片

结尾

这篇文章只针对最基本的Servlet XSS做了审计,实际上无论从广度还是深度,都有巨大的工作量:

  • 广度:SQL注入,XXE,反序列化,文件上传,CSRF等漏洞的审计
  • 深度:如果代码对Servlet做了一定的封装,或者需要跨多个java文件分析
  • 可控参数的追踪:从controller层传入参数到返回,这个参数经历了些什么

代码在github:https://github.com/EmYiQing/XVulnFinder

简单写了个输出html的页面:
【安全工具】浅谈编写Java代码审计工具_第3张图片

最后

【获取网络安全学习资料以及工具】可以关注私我

你可能感兴趣的:(安全,网络,安全工具,java,golang,网络安全,计算机网络)