[security tools] talking about writing Java code audit tools

Keywords: Java Go network security Cyber Security

introduce

The author is a senior student who is new to safety. If there are mistakes in the article, please point out!

At first, it was considered to use pure matching, but this method is too strict, and the code written by programmers has various possible combinations

Therefore, I tried to realize java lexical analysis and syntax analysis by myself. After a little attempt, I found that this is unrealistic. On the one hand, it involves some algorithms of compilation principle. In addition, compared with C language, Java language itself is more complex and can not be solved in a short time. The in-depth study of compilation principle deviates from the purpose of being an audit tool

Later, several solutions were found: ANTLR, JavaCC, JDT and javaparser

After comparison, we finally choose the javaparser project, which seems to be based on JavaCC, and the core developer is the author of effective java. It is easy to use and can be simply imported in a dependent manner

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

I wanted to use Golang to write this tool. After looking up relevant materials, I found that Golang itself provides AST library, which can do syntax analysis on Golang itself, but I can't find a library to realize Java syntax analysis (I'll try to review the compilation principle later)

example

The most fundamental class of javaparser is CompilationUnit. If we want to analyze the code, we need to instantiate the object first

// Code is the java code string read in
// There are other overloads, but this is more convenient
CompilationUnit compilationUnit = StaticJavaParser.parse(code);

Give the simplest XSS code

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

For this case, we write the principle of audit tool

  • From the perspective of import, for example, there are request and response to prove that this is an HttpServlet
  • From the class point of view, you must inherit from HttpServlet to prove that it is a Servlet
  • If the value obtained by req.getParameter is written, it is considered as XSS

Verify Guide Package

About verifying the imported package, a simple method is made

public static boolean isImported(CompilationUnit compilationUnit, String fullName) {
    // You must modify the value in this way in a lambda expression
    final boolean[] flag = new boolean[1];
    compilationUnit.getImports().forEach(i -> {
        if (i.getName().asString().equals(fullName)) {
            flag[0] = true;
        }
    });
    return flag[0];
}

If you want to verify the import of the request and the corresponding package

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

Get class node

First, get the Demo Class, because there is not necessarily only one Class in a java file

compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
        // Is not an interface and is not an abstract class
        .filter(c->!c.isInterface()&&!c.isAbstract()).forEach(c->{
            System.out.println(c.getNameAsString());
        });

// output
// Demo

Further, we need to determine whether this class inherits from HttpServlet

compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
        .filter(c->!c.isInterface()&&!c.isAbstract())
        .forEach(c->{
            boolean isHttpServlet = false;
            // It's inconvenient to continue using lambda
            NodeList<ClassOrInterfaceType> eList  = c.getExtendedTypes();
            for (ClassOrInterfaceType e:eList){
                if (e.asString().equals("HttpServlet")){
                    isHttpServlet = true;
                    break;
                }
            }
            if (isHttpServlet){
                // This makes further logic
                System.out.println("hello");
            }
        });

Only when the class node is obtained can we continue to traverse the abstract syntax tree to get information such as methods

Acquisition method

Traverse the method node and get the specific request and response parameter names

The reason to get the method parameter name is for further tracking

if (isHttpServlet){
    c.getMethods().forEach(m->{
        // lambda does not allow direct replication, so map is used
        Map<String,String> params = new HashMap<>();
        m.getParameters().forEach(p->{
            // resp (the real situation may not necessarily be resp)
            if (p.getType().asString().equals("HttpServletResponse")) {
                params.put("response", p.getName().asString());
            }
            // Req (the real situation may not be 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"));
    });
}

// output
// request:req
// response:resp

Confirm that the parameters are controllable

The key point of audit loopholes lies in the controllability of parameters, which is also a difficulty

In this case, if a parameter is obtained by req.getParameter("..."), it can be considered controllable

In fact, this req is not necessarily req, but may be request, requ, etc., which is why a map needs to be saved in the previous step

Parameter verification can be added

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

Get all assignment expressions and determine whether parameters such as req.getParameter are called

Referring to the above method, use map to save the parameter results for subsequent verification

Map<String,String> var = new HashMap<>();
m.findAll(VariableDeclarationExpr.class).forEach(v->{
    MethodCallExpr right;
    boolean isGetParam = false;
    // Get the right part of the assignment statement
    if (v.getVariables().get(0).getInitializer().get() instanceof MethodCallExpr) {
        // There will be problems if the forced conversion is not verified
        right = (MethodCallExpr) v.getVariables().get(0).getInitializer().get();
        if (right.getScope().get().toString().equals(params.get("request"))){
            // Determines whether req.getParameter was called
            if (right.getName().asString().equals("getParameter")){
                isGetParam = true;
            }
        }
    }
    if(isGetParam){
        var.put("reqParameter",v.getVariables().get(0).getNameAsString());
        logger.info("find req.getParameter");
    }
});

Determine trigger point

The trigger point in this case is resp.getWriter().write()

This is a method call, so search MethodCallerExpr

m.findAll(MethodCallExpr.class).forEach(im -> {
    if (im.getScope().get().toString().equals(params.get("response"))) {
        // If response.getWriter is called
        if (im.getName().asString().equals("getWriter")) {
            MethodCallExpr method;
            // There is something wrong with the direct transfer
            if (im.getParentNode().get() instanceof MethodCallExpr) {
                // Next step method
                method = (MethodCallExpr) im.getParentNode().get();
            } else {
                return;
            }
            // response.getWriter.write();
            if (method.getName().asString().equals("write")) {
                // In this case, the constant param is written, so search NameExpr
                method.findAll(NameExpr.class).forEach(name -> {
                    // The reqParameter previously saved in the map is used here
                    if (name.getNameAsString().equals(var.get("reqParameter"))) {
                        // XSS is considered to exist
                        logger.info("find xss");
                    }
                });
            }
        }
    }
});

For this basic case, you can add several more rules for the response.getOutputStream method

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

test

Try to make the original XSS code more complex and see the effect of audit

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 was successfully detected after running

ending

This article only audits the most basic Servlet XSS. In fact, there is a huge workload in both breadth and depth:

  • Breadth: audit of SQL injection, XXE, deserialization, file upload, CSRF and other vulnerabilities
  • Depth: if the code encapsulates the Servlet, or needs to be analyzed across multiple java files
  • Tracking of controllable parameters: what happened to the parameters from the controller layer to the return

Code in GitHub: https://github.com/EmYiQing/XVulnFinder

Simply write a page that outputs html:

last

[Access to network security learning materials and tools ]You can focus on your private self

Posted by tommyrulez on Mon, 27 Sep 2021 03:25:41 -0700