Reaching non-default conditional blocks during grey and blackbox webapp testing

Intro

I want to touch on the very common problem of incomplete coverage of code paths inherent to blackbox and greybox security testing of web apps. I have never been a fan of this approach to security testing, mostly due to the very limited target visibility leading to a disproportionately high amount of guesswork resulting in tons of unnecessary requests with payloads blindly searching for potential issues. Having said that, the fact is that for many different reasons blackbox and greybox is how most pentests are conducted and I do not expect this to change in the foreseeable future.

In addition to the problem of efficient resource utilization, testing without filesystem and source code access can never provide the same accuracy in terms of code coverage. And I am not even talking about potential backdoors or actual edge cases, but rather about something way more common – regular conditional blocks.

Note: while all examples, tests and solutions presented in this article use BurpSuite Professional, the described problem as well as the attempted approach to address it are universal and can be addressed and applied using different tooling.

The problem

Take the following PHP script:

The relevant view from a web browser as well as the raw HTTP request will look as follows:

It is the simplest possible variant of OS command injection, which every scanner and every manual tester will instantly discover without seeing the code. There is only one parameter to manipulate and there are no conditional blocks.

Now, let us take this one:

Here the vulnerable code is in a conditional block. The condition is met if the “action” parameter is equal “ping”, which is the default value propagated in the HTML form, leading to the following base request:

If we leave the base request intact and trigger an active scan on it, the command injection will as well be very easily detected:

So far, so good. But now let us have a look at the third example:

Now, since “nslookup” is the default and only value we see from interacting with the interface, and the vulnerable code lies in the “else” block, an active scan triggered on the original request will NOT reveal the issue, leading to a false negative:

This happens because the vulnerability can only be reached when the value of the “action” parameter is different than “nslookup”, which is not the case in the unmodified base request:

The way Active Scanner behaves when auditing requests with multiple parameters is that it iterates over them, one after another, and sends modified versions of the base request, in which the currently audited parameter is modified with various test payloads, while all the other parameters remain at their default values.

In the screenshot below we can see the output from the Flow plugin (a slightly more advanced alternative to the built in Logger utility), depicting one of the requests targeting the “host” parameter (while the “action” parameter remains default):

And here a request targeting the “action” parameter, with the “host” parameter at its default:

In other words, in default scanner behavior, parameters are fuzzed one at a time instead of both/all in the same request.

So, to detect this particular case using Active Scanner, we have to manually change the value of the “action” parameter (e.g. in Repeater) in the base request and then trigger an active scan on it. Which means that to provide coverage reaching both conditional blocks we ended up having to run two separate scans on two different base requests derived from the original one.

This is the first and simplest example of a non-default code execution path. False negatives like this are what this article is about.

Now, let us take this example:

Here we have two nested conditional blocks. To trigger detect the command injection not only the “action” parameter needs to be set to a non-default value (other than “nslookup”), but also the “input_filtered” parameter has to be set to “true”, which is not the default value either. Remember this one - case4.php - as we will get back to it later in this article.

Attempted solution

As already mentioned, reaching both conditional blocks of an “if/else” section with Active Scanner requires triggering two separate scans on two variants of the original base request. One scan on the original base request that was captured from the browser, and another one on its modified version in which the parameter we suspect takes part in condition evaluation is set to a non-default value.

Flat mode

To simply reach the top-level “else” blocks (without nesting), it will suffice to create one alternative base request for every single parameter. Let us evaluate this using an example containing three boolean-like parameters:

one=true&two=true&three=true

Now, we generate additional three base requests based on this one. In each we only flip one of the parameters and leave the other ones intact:

one=false&two=true&three=true

one=true&two=false&three=true

one=true&two=true&three=false

So, we have the base request plus three additional more, giving us the total of 4 base requests to schedule scans on:

1.        one=true&two=true&three=true

2.        one=false&two=true&three=true

3.        one=true&two=false&three=true

4.        one=true&two=true&three=false

The following screenshot depicts a mock target PHP script with “if/else” blocks along with comments stating which base requests will reach them. Keep in mind this is a mock script and in a real target there would be no empty code blocks, but rather code that we want to reach with an active scan:

This simple exercise helps us demonstrate that the first base request (the original one) is redundant if our goal is to only reach all these non-nested conditional blocks. So, let’s remove it:

1.        one=false&two=true&three=true

2.        one=true&two=false&three=true

3.        one=true&two=true&three=false

Now there’s no redundant requests that we could remove while still reaching all blocks.

Deep mode
Now, the flat mode will not allow us to reach nested conditional blocks (as presented earlier on the example of case4.php). But additionally let us evaluate a nested variant of the mock script above:

So, even though again we have 8 blocks here, only three of them will be reached by the three base requests we generated in the flat mode (marked green):

The full set of base requests to reach all these nested blocks is:

1.        one=false&two=false&three=false

2.        one=false&two=false&three=true

3.        one=false&two=true&three=false

4.        one=false&two=true&three=true

5.        one=true&two=false&three=false

6.        one=true&two=false&three=true

7.        one=true&two=true&three=false

8.        one=true&two=true&three=true

Burp Plugin

Of course this algorithm, especially in the flat version can be applied manually and many testers do it, sometimes without even thinking about it. As it is quite easy to get lost in tracking all combinatoric variants of parameter sets we would like to cover, I myself decided to facilitate this process by implementing a simple BurpSuite Pro plugin: https://github.com/ewilded/grey_reach. It is written in Python, so you will need Jython set up to load it.

Using the plugin is very straightforward:

For the base requests we want to selectively schedule deep scans on, we right-click and choose one of the modes:

Additionally, there is a tab allowing us to configure parameters we want excluded from the algorithm:

We can observe the output from the plugin in its relevant “Extensions” -> “Output” subtab:

The screenshots below demonstrate how case4.php was successfully picked up based on the seventh and eight base request generated with the deep scan mode:

Default value handling

For string parameters, the flipped version is set to “nondefault”. For integers, depending on whether they are positive, negative or 0, both values from the rest of the spectrum are generated. So for example if the original value of a parameter is “0”, two extra base requests – one with –“1” and one with “1” – will be generated.

Optimization

The default scanning policy in Burp makes it hit various additional insertion points (not just parameters), such as URI itself, request headers, param names, the entire body, messing with the request structure (HTTP smuggling checks and the like), whenever we pick “Do active scan” on an entire request. This can be easily observed in Flow:

Those checks add a significant number of requests to every scan. While trying to reach hidden code paths, we don’t want those checks repeated against the same endpoint. Active scanning takes long as it is, especially when we want to multiply scans by combinatorically creating variants of base requests. Therefore, the plugin only picks parameters, creates base requests with their flipped versions, converts them into custom insertion points and schedules active scans on them selectively.

Total number of requests sent

Depending on several factors, such as the number of parameters, the BurpSuite version we use, loaded scanner extensions as well as the current scan policy, every active scan of a base request results in different numbers of requests sent by Burp, ranging from over a hundred to even thousands. Those numbers can be peeked in Dashboard -> Active Scans -> Audit Items view:

When multiplying the number of active scans per base requests by creating alternative base requests with combinations of flipped parameter values, we increase these numbers proportionally. Which will directly reflect on the amount of time for our full scan to complete. Keep that in mind, especially when running scans on base requests with high number of parameters.

By the way, why 8 base requests based on the same template, with only difference being particular parameter values flipped, have led to different numbers of requests sent by the scanner (as visible in the screenshot) is an interesting question, however irrelevant to and beyond the scope of the plugin and this article.

Concurrent requests

By default, BurpSuite allows 10 concurrent requests in its resource pool configuration, which practically means that if the plugin generates 12 base requests and schedules an active scan for each one of them, there will be 10 scans running at the same time until the first 3 finish. Depending on the target and the system BurpSuite is running on, this might lead to stability issues. Therefore before using the plugin, you might want to go to Settings -> Project -> Tasks -> Resource pool and change the number to a lower value:

On the other hand, setting this value too low will make scans take even longer, however it will assure better stability and therefore result accuracy.

Not a silver bullet

An astute reader with programming and testing experience already noticed that simply flipping values of parameters to something different than what we have observed in requests generated on the client side can only cover the “else” blocks, but will not reach code blocks behind additional conditions from more complex sets, especially when conditions refer to particular values or consist of multiple sub-conditions.

For example, in the code depicted below, only the default value condition and the last block after “else” would get coverage, while missing all the vulnerable block in the middle:

To fill this gap, we would also need to introduce more granular and flexible rules for defining the range of values to attempt in base requests, significantly increasing the total number of all requests sent to the target as well as the time required for the full scan to complete. Which crosses into the madness territory, but might still be considered feasible, especially for longer tests with narrow attack surface, or simply for scans scheduled during periods when no other test activities are conducted (e.g. overnight), or in fully automated fashion e.g. using Burp Enterprise – or any other solution where the logic presented here is implemented.

Possible improvements

To provide better coverage and flexibility, it would be nice to introduce the possibility of configuring custom value ranges for specific parameters, and even make the plugin automatically learn those values by using passive scan results.

No one really cares about cookies and neither do I