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

1,417 阅读5分钟

网络安全工具 的图像结果

介绍

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

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

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

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

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

 <dependency>
    <groupId> com.github.javaparser </groupId>
    <artifactId> javaparser-symbol-solver-core </artifactId>
    <version> 3.23.0 </version>
</dependency> 

笔者本想采用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 < ClassOrInterfaceType > 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 < String , String > 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 < String , String > 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

 

结尾

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

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

代码在github:github.com/EmYiQing/XV…

简单写了个输出html的页面:

最后

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