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