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.
> 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.