Blog

Combining static and dynamic tools to analyse PHP code

Understanding the security implications of Superglobals like $_GET is crucial when navigating a vast PHP codebase, as these variables can bypass the built-in security mechanisms of frameworks or CMSs. These globally accessible variables allow for the reading of user (or in an adversarial context, attacker) input. The interest for an attacker in exploiting these Superglobals is evident, as identifying them within large codebases is straightforward. However, crafting the correct URLs with the necessary parameters for exploitation can be a complex and laborious task, especially when seeking to validate findings through dynamic analysis tools like Burp Suite.

This blog post aims to introduce a practical solution to these challenges, with a focus on the capabilities of Joern, a static code analysis tool renowned for its effectiveness in addressing such issues.

Please be aware that the examples and techniques discussed are tailored for Joern version 2.0.323. To ensure full compatibility and to maximize the utility of this guide, it is recommended to use this version or any later one.

> Joern - the bughunters workbench

Let’s take a closer look at Joern, a static code analysis tool at heart that’s built on the foundation of Code Property Graphs (CPG). It has a growing community (join us on Discord) and user base, which shaped Joern over time. It has grown into a versatile framework.

One of Joern’s notable strengths lies in its ability to dissect and query code that isn’t constrained by compilation prerequisites or specific build environments (though, as always, exceptions apply). With its approachable query language and scripting capabilities, Joern becomes an ideal tool for initiating initial analyses and combine it with Burp Suite.

We recommend you to checkout the quickstart guide, accompanied by the comprehensive documentation.

However, we start with a brief introduction based on a simple PHP example, which merely reads a name variable from a GET request and prints it:

1$ cat /tmp/testapp/test.php
2
3<?php
4$name = $_GET['name'];
5echo "Name: " . $name . "<br>";
6?>

Joern’s REPL (Read-Eval-Print Loop), makes it easy to import code as typing importCode("[path/to/code]"). A successful import operation results in a graph object, with a node count. For the present example, our graph shows a tally of 58 nodes:

1joern> importCode("/tmp/testapp")
2Using generator for language: PHP: PhpCpgGenerator
3Creating project `testapp` for code at `/tmp/testapp`
4[...]
5val res0: io.shiftleft.codepropertygraph.Cpg = Cpg (Graph [58 nodes])

With our code imported into Joern’s realm, we’re ready to use the query language and retrieve the insights we need. Each query has a cpg. prefix followed by so called steps. There are many obvious steps like method, call and name but in our case we need the identifier followed by a code step.

The following query has only non obvious part, .p standing for (pretty) print.

1joern> cpg.identifier.codeExact("$_GET").code.p
2val res1: List[String] = List("$_GET")

Finding $_GET is just assuring that we can query the obvious but the more interesting part remains the variable name. The following two queries introduce the required steps inCall and file(.name). The first prints the complete code of the call while the latter is giving us the file(s), where those calls are appearing.

The reason why Joern is treating $_GET["name"] as a call is that it is implemented like an index access on an array.

 1joern> cpg.identifier.
 2           codeExact("$_GET").
 3           inCall.
 4           code.p
 5val res2: List[String] = List("$_GET[\"name\"]")
 6
 7joern> cpg.identifier.
 8           codeExact("$_GET").
 9           inCall.file.name.p
10val res3: List[String] = List("test.php")

This provides us with all the necessary information to create URLs and send out requests to Burp Suite.

> Bridging Joern with Burp Suite

Now, it’s time to connect the dots and let Joern enrich your Burp experience. We combine the previous queries and iterate over all $_GET usages which are in calls, extract the string parameter and the file name. A subsequent format string combines everything to a final URL, before sending out a GET request. Iterating is done using a (Scala) foreach. Since we’re dealing with lists (of calls, arguments, files ..), we have to verify if they’re not empty and reverting to a default value when necessary.

Turning our attention to request dispatching, we tap into the convenience of the Scala requests library, which is already imported and accessible within the Joern REPL. Because using this library requires just three self-explanatory lines to send a request, we have added it to the code snippet below.

 1 import requests._
 2 //default settings
 3 val proxy = ("localhost", 8080)
 4 val baseUrl = "https://testdomain.cc"
 5 cpg.identifier.
 6     codeExact("$_GET").
 7     inCall.foreach{call=>
 8 
 9       // file name check, "no File" if empty
10       def fileName = call.file.name.
11                       headOption.
12                       getOrElse("no File")
13 
14       // parameter check, "no Param" if empty
15       def literals = call.argument.
16                           isLiteral.code.
17                           headOption.
18                           getOrElse("no Param")
19 
20       // parsing param name, adding * as value
21       val parameter = s"${literals.replace("\"","")}=*"
22 
23       // final url
24       val url = s"$baseUrl/$fileName?$parameter"
25 
26       try{
27         requests.get(url,proxy = proxy,verifySslCerts = false)
28       } catch {
29         case e:Exception => println(s"Problem sending request: $url")
30       }
31 }

That’s it! Now the requests should show up in the burp interceptor.

Burp intercepts request

> Just give me the cake!

You might think it’s pretty neat but wouldn’t want to copy and paste everything into a REPL session just to get your URLs into Burp.

And you’re right! We wouldn’t do it either.

This is why Joern offers a recipe for this as well. We encapsulate the existing code in a single method, add a call to it from a main function, and save it in a file named extractUrlsToBurp.sc.

 1import requests._
 2// Default settings     
 3val proxy = ("localhost", 8080)
 4def sendUrlsToBurp(baseUrl:String): Unit ={
 5 cpg.identifier.
 6     codeExact("$_GET").
 7     inCall.foreach{call=>
 8 
 9       // file name check, "no File" if empty
10       def fileName = call.file.name.
11                       headOption.
12                       getOrElse("no File")
13 
14       // parameter check, "no Param" if empty
15       def literals = call.argument.
16                           isLiteral.code.
17                           headOption.
18                           getOrElse("no Param")
19 
20       // parsing param name, adding * as value
21       val parameter = s"${literals.replace("\"","")}=*"
22 
23       // final url
24       val url = s"$baseUrl/$fileName?$parameter"
25 
26       try{
27         requests.get(url,proxy = proxy,verifySslCerts = false)
28       } catch {
29         case e:Exception => println(s"Problem sending request: $url")
30       }
31 }
32}
33@main def main(inputPath: String, domain:String) = {
34  // ! deleting all projects in the workspace
35  workspace.reset
36  importCode(inputPath)
37  sendUrlsToBurp(domain)
38}

Save the code to extractUrlsToBurp.sc and invoke the script with the following command line. Of course, you have to adjust the inputPath and the baseUrl:

./joern --script extractUrlsToBurp.sc --params inputPath=/tmp/testapp/,baseUrl=https://testdomain.cc/

Now, the next step entails conducting practical tests on an actual codebase, which will be detailed in a subsequent blog post.

> Conclusion

This blog post demonstrates the simplicity and effectiveness of employing Joern to bridge the divide between static analysis and dynamic testing tools. While our example serves as a foundational guide, it’s important to acknowledge its limitations—it lacks comprehensive error handling, support for various request types, and considerations for data flow among others.