White-box penetration testing with Xdebug: Debugging for PHP vulnerabilities

July 19, 2024

Having access to your target’s source code is obviously an invaluable advantage of white-box penetration testing.

This article will help you leverage that benefit in the context of PHP applications and using the Xdebug PHP debugger.

Specifically, you will learn how to set up a PHP web application within a docker environment; how to set up Xdebug; how to detect PHP vulnerabilities using Xdebug; common PHP vulnerabilities; and how to leverage your white-box pentest findings to enhance future black-box testing.

Follow these steps to not only unearth vulnerabilities during white-box engagements, but to also craft custom payloads you can unleash to great effect in future black-box tests.

Outline

  • Necessary resources
  • Install necessary resources
  • Setup
    • File structure
    • Configure necessary files
    • Verify our setup
  • Hunt for vulnerabilities with Xdebug
  • Common weaknesses related to PHP
    • CWE-98: Improper Control of Filename for Include/Require Statement in PHP Program ('PHP Remote File Inclusion')
    • CWE-624: Executable Regular Expression Error
    • CWE-434: Unrestricted Upload of File with Dangerous Type
  • Improve future black-box testing with code analysis
  • Conclusion
  • References

Necessary resources

  • Visual Studio Code (VS Code) (or your preferred IDE)
  • Xdebug VS Code extension
  • Docker

Install necessary resources

You can install Visual Studio Code here.

Once you have installed Visual Studio Code, you can install the Xdebug extension from the extension tab on the left-hand side. Simply search for "Xdebug" and install it:

Setup

For our demonstration, we will use a vulnerable PHP code snippet from YesWeHack’s Vulnerable Code Snippets Github repository (which also contains snippets written in a variety of other programming languages).

If you are completely new to using Xdebug, we recommend using the same snippet: sqli-escape-invalid-blacklist-order. Otherwise, feel free to take a different code snippet that better suits your preferences.

File structure

Our project will use the following file structure:

.
├── config
│   ├── apache.conf
│   ├── mysql.sql
│   ├── php-custom.ini
│   ├── supervisord.conf
│   └── xdebug.ini
├── docker-compose.yml
├── Dockerfile
└── vsnippet
├── 1-sqli-escape-invalid-blacklist-order.php
└── ignore
├── db
│   └── db.php
├── design
│   └── design.php
└── README.txt

Certain files are extra-important and must be added or modified, including:

launch.json

This will be the Xdebug listener configuration that is used inside Visual Studio Code on our host system.

xdebug.ini

This file contains the configurations for our vulnerable web application, which is running inside a docker container.

Dockerfile

As well as building the docker image, the Dockerfile installs and activates PHP Xdebug inside the docker container.

docker-compose.yml

To this file we will add our host’s hostname to enable our docker container to communicate with Xdebug, which will be hosted by our host system in Visual Studio Code.

Configure the necessary files

launch.json

{
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Listen for Xdebug",
			"type": "php",
			"request": "launch",
			"port": 9003,
			"pathMappings": {
				"/var/www/html": "${workspaceFolder}/php/vsnippet"
			},
			"log": true
		}
	]
}

xdebug.ini

In the config folder provided, we will create a new file in xdebug.ini and add the following configuration:

zend_extension=xdebug.so
Xdebug.mode=debug
Xdebug.start_with_request=yes
Xdebug.client_host=host.docker.internal
Xdebug.client_port=9003

Dockerfile

In the existing Dockerfile, we will add the code below immediately under the first line (which is: FROM php:8.2-apache). This will allow us to install and enable Xdebug in our vulnerable docker container.

RUN apt-get update && apt-get install -y \
	&& pecl install Xdebug \
	&& docker-php-ext-enable Xdebug
COPY config/Xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-Xdebug.ini

docker-compose.yml

The docker-compose.yml file will be used to set up services for the docker container to run and make the PHP web application accessible via port 1337. The extra_hosts parameter and the value we give it is used to enable the docker container to communicate with our host system, where the Xdebug listener is running in our Visual Studio Code IDE.

version: '3.8'
services:
	php-apache:
		container_name: vsnippet-sqli-escape-invalid-blacklist-order-1
		build:
			context: .
			dockerfile: Dockerfile
		depends_on:
			- db-mysql
		volumes:
			- ./vsnippet:/var/www/html/
		ports:
			- "127.0.0.1:1337:80"
		extra_hosts:
			- host.docker.internal:host-gateway
	db-mysql:
		container_name: vsnippet-db-mysql-sqli-escape-invalid-blacklist-order-1
		volumes:
			- ./config/mysql.sql:/docker-entrypoint-initdb.d/mysql.sql
		image: mysql
		environment:
			MYSQL_DATABASE: ywhvsnippet
			MYSQL_USER: vsnippet
			MYSQL_PASSWORD: vsnippet
			MYSQL_ROOT_PASSWORD: supervulnerableroot
		ports:
			- "127.0.0.1:1338:3306"

Take note that we added two final lines to the php-apache service that contains the extra_hosts parameter with its values:

extra_hosts:
	- host.docker.internal:host-gateway

Verify our setup

Now our setup is ready, we can start debugging our vulnerable web application.

To make sure our debugger works, we will open Visual Studio Code on our host system and set a couple of breakpoints in the vulnerable code snippet. You can do this by simply clicking on the left side of the line number identifiers. This will make red dots appear, as you can see in the image below. This red dot represents a breakpoint that our debugger will stop at.

When a HTTP request is sent by a client to the vulnerable web application, our debugger will stop at our first breakpoint (if it hits that code section) and display all necessary debugging information.

We can trigger the Xdebug listener in Visual Studio Code by sending a HTTP request to our vulnerable web application with curl:

curl "http://localhost:1337/?q=MYINPUT" -H 'Cookie: XDEBUG_SESSION=1'

Example of a raw HTTP request:

GET /?q=MYINPUT HTTP/1.1
Host: localhost:1337
Cookie: XDEBUG_SESSION=1

If you want to debug a HTTP request in Visual Studio Code, make sure you include the cookie XDEBUG_SESSION with a random value such as XDEBUG_SESSION=1 before you send it. This tells our Xdebug listener to start debugging. It will stop debugging at the first breakpoint, which is triggered by our HTTP request.

Hunt for vulnerabilities with Xdebug

Xdebug is a great tool to use both for development and white box penetration testing. By providing a detailed overview of debugging results generated from each code section encountered, this utility means you, as a penetration tester, can more easily see where your input is used and how the payload is modified.

By using our previously described setup (see ‘Configure the necessary files’), we can now start analysing our vulnerable code.

Let’s start by sending another HTTP request with curl, this time with a simple SQL injection payload ('+or+1=1+--+) as the value of the GET parameter q.

curl "http://localhost:1337/?q='+or+1=1+--+" -H 'Cookie: XDEBUG_SESSION=1'

After sending this HTTP request we can see that it hit our source code and, in the left sidebar, that our q parameter’s value has been determined by our payload.

Next, by pressing the curved blue arrow (second blue icon from the left) in the debug bar (on the top-left of Visual Studio Code by default), we will jump to the second breakpoint we set in the source code.

Continue pressing the arrow until you reach line 19 as shown in the image below. Then right click on the $query variable and click Add to Watch. Repeat these steps for the $char variable.

Once added to the Watch section, these two variables will appear at the bottom left under the debug sidebar.

We are now monitoring these two variables because $query is our payload and $char is the character to be escaped. If our payload contains the value presented in the variable $char, a backslash will be added before all characters within the payload (this backslash is the $query variable value).

Continue pressing the blue debug arrow until the $char variable contains a single quote ('). This indicates that this character should be escaped in the next loop, and our payload should be modified by the filter.

Press the blue debug arrow again and you will see that our payload was modified by the filter, as shown in the image below:

However, as you can see, the $char variable now contains the \ character, which it just used to escape our single quote (') character. This means that, for the next loop, it should escape the backslash character in our payload.

As you can see below, this is exactly what happens. The backslash character did replace the backslash character that was previously used to escape our single quote. This makes our current backslash escape itself in the payload, leaving the single quote character unescaped.

By pressing the blue debug arrow one more time, we can see that we have jumped outside the filter and that our final payload remains as:

\\' or 1=1 -- 

Finally, we stop the debugging process by pressing the red square in the debug field. We can then see that our curl contains the results from the products table.

Common weaknesses related to PHP

Below are three of the most common weaknesses associated with the PHP programming language.

CWE-98: Improper Control of Filename for Include/Require Statement in PHP Program ('PHP Remote File Inclusion')

The application uses input data that can be controlled by the user. This input value is then later used within an include, require or similar function.

An attacker can exploit this by creating a specific value that dupes the application into including an unintended file, which can then be used to obtain arbitrary file reads or, in most cases, a remote code execution (RCE). This vulnerability is mainly known as Local/Remote File Inclusion.

CWE-624: Executable Regular Expression Error

In PHP below version 7 the regex modifier e was supported. This regex modifier made it possible to execute pure PHP code. As an example, this vulnerable code snippet below (if it's running a PHP version below 7) would be vulnerable to a code injection vulnerability.

<?php
$payload = $_GET['input'];
echo preg_replace('/^(.*)/e', 'strtoupper($1)', $payload);
?>

The payload below would result in a successful PHP code injection and lead to RCE.

system($_GET[0])&0=id

CWE-434: Unrestricted Upload of File with Dangerous Type

PHP applications are more likely to have ‘unrestricted upload of file with dangerous type’ vulnerabilities than applications written in other programming languages. The reason is that PHP will execute any file with the extension php (unless it’s configured not to). By contrast, when Node.js uploads a js file it will not execute as server-side code if you access it directly in the browser.

So if you manage to upload a php file to a PHP web application, there is a good chance the file will execute as PHP code when you visit your uploaded file. This potential vulnerability is known to lead to a PHP code injection with the impact of remote code execution.

Improve future black-box testing with code analysis

Doing code analysis can teach you about useful behaviours and code patterns specific to various programming languages. By adapting your payloads to these behaviours, you can greatly enhance the effectiveness of your black-box testing.

For instance, in the ‘Hunt for vulnerabilities with Xdebug’ section, we learned how a certain payload was modified by a particular filter.

We recommend making a note of such behaviours and creating custom payloads for future use. Add these payloads to a custom dictionary and use them with fuzzers the next time you encounter a pentest in a black-box environment – you could reap some impressive results.

Conclusion

Efficiently debugging an application’s source code means you can more easily track executed code that is triggered by the client (attacker). Emulating the steps we’ve outlined improves your testing flow and spotlights behaviours that enable you to discover where user input is specifically being handled.

You will also learn of unique or unexpected behaviours that will generally enhance your black-box penetration testing of PHP applications in the future. Building custom payloads based on these findings will improve your chances of finding vulnerabilities and help to avoid surfacing duplicate reports.

References