Over the course of the past year, our team added many new checks to the Acunetix scanner. Several of these checks were related to the debug modes of web applications as well as components/panels used for debugging. These debug modes and components/panels often have misconfigurations, which may lead to the disclosure of sensitive information or even to remote command execution (code injection).
As I was working on these checks, I remembered cases when I discovered that applications expose a special port for remote debugging. When I was working as a penetration tester, I often found that enterprise Java applications exposed a Java Debug Wire Protocol (JDWP) port, which would easily allow an attacker to get full control over the application.
When I was writing the new Acunetix checks, I became curious about similar cases regarding other programming languages. I also checked what capabilities Nmap has in this respect and found only checks for JDWP. Therefore, I decided to research this blind spot further.
Every developer uses some kind of a debugging tool but remote debugging is less common. You use remote debugging when you cannot investigate an issue locally. For example, you use it when you need to debug an enterprise Java application that is too big to develop locally and that has strong connections with the environment or processed data. Another typical scenario for remote debugging is debugging a Docker container.
A debugger is a very valuable target for an attacker. The purpose of a debugger is to give the programmer maximum capabilities. It means that, in almost all cases, the attacker can very easily achieve remote code execution once they access the remote debugger.
Moreover, remote debugging usually happens in a trusted environment. Therefore, many debuggers don’t provide security features and use plain-text protocols without authentication or any kind of restrictions. On the other hand, some debuggers make the attack harder – they provide authentication or client IP restrictions. Some go even further and don’t open a port but instead initiate the connections to the IDE. There are also cases when the programmer passes a remote connection to the debugger through SSH.
Below you can find examples of RCE attacks on various debuggers. I tried to cover all common languages but focused on the most popular debuggers only and those that are most commonly misconfigured.
Attacks on debuggers
JPDA is an architecture for debugging Java applications. It uses JDWP, which means that you can easily detect its port using Nmap. The port is, however, not always the same – it typically depends on the application server. For example, Tomcat uses 8000, ColdFusion uses 5005.
To gain access to a shell through a successful RCE attack, I used an exploit from Metasploit: exploit/multi/misc/java_jdwp_debugger.
Also note that all other JVM-based languages (Scala, Kotlin, etc.) also use JPDA, so this presents an attacker with a wide range of potential targets.
XDebug is different from all other debuggers described in this article. It does not start its own server like all other debuggers. Instead, it connects back to the IDE. The IP and port of the IDE are stored in a configuration file.
Due to the nature of XDebug, you cannot detect it and attack it using a port scan. However, with a certain configuration of XDebug, you can attack it by sending a special parameter to the web application, which makes it connect to our IDE instead of the legitimate IDE.
Acunetix includes a check for such a vulnerable configuration. Details of this attack are available on this blog.
pdb is a common Python debugger and the remote_pdb package (and other similar packages) enables remote access to pdb. The default port is 4444. After you connect using ncat, you have full access to pdb and can execute arbitrary Python code.
debugpy is a common debugger for Python, provided by Microsoft. There is also a deprecated version of this debugger called ptvsd.
debugpy uses a debug protocol developed by Microsoft – DAP (Debug Adapter Protocol). This is a universal protocol that may also be used for debuggers for other languages. The protocol is similar to JSON messages with a preceding Content-Length header. The default port is 5678.
Microsoft uses this protocol in VSCode so the easiest way to communicate using this protocol is by using VSCode. If you have VSCode with an installed default Python extension, all you need to do is to open an arbitrary folder in VSCode, click the Run and Debug tab, click Create a launch.json file, choose Python→Remote Attach, and enter a target IP and port. VSCode will generate a launch.json file in the .vscode/ directory. Then click Run→Start Debugging and when you connect, you can enter any Python code in the Debug console below, which will be executed on your target.
The ruby-debug-ide (rdebug-ide) gem uses a custom but simple text protocol. This debugger typically uses the 1234 port.
To execute arbitrary code, you can use VSCode and follow the same steps as for Python. Note that if you want to disconnect from a remote debugger, VSCode sends quit instead of detach (like RubyMine would do), so VSCode stops the debugger completely.
Versions of Node.js earlier than v7 use the Node.js Debugger. This debugger uses the V8 Debugger protocol (which looks like HTTP headers with a JSON body). The default port is 5858.
The Node.js Debugger allows you to execute arbitrary JS code. All you need to do is use Metasploit with the exploit/multi/misc/nodejs_v8_debugger/ module.
Newer versions of Node.js use the Node.js Inspector. From the attacker’s point of view, the main difference is that the WebSocket transport protocol is now used and the default port is now 9229.
You can use several methods to interact with this debugger. Below you can see how to do it directly from Chrome, using chrome://inspect.
Delve is a debugger for Go. For remote debugging, Delve uses the JSON-RPC protocol, typically on port 2345. The protocol is quite complex, so you definitely need to use, at least, delve itself (dlv connect server:port).
Go is a compiled language and I was unable to find a direct way to achieve RCE as with other languages. Therefore, I recommend that you use a proper IDE (for example, Goland) because you will have to do some debugging yourself to be able to achieve RCE. Note that the source code is not necessary but it comes in handy.
Below is an example of connecting to Delve using Goland.
Delve provides a way to invoke functions imported to an application. However, this feature is still in beta testing and it doesn’t allow to pass static strings as function arguments.
The good news is that we can change the values of local variables and pass them to a function. Therefore, we need to pause an application in a non-runtime thread within a scope that interests us. We can use standard libraries for that.
Below you can see how to pause an application on a standard HTTP library and invoke the os.Environ() function, which returns the env of the application (possibly containing sensitive data). If you want to execute arbitrary OS commands, you need to execute exec.Command(cmd,args).Run(). However, if so, you need to find and stop in a position with variables of type String and String.
The gdbserver allows you to debug apps remotely with gdb. It has no security features. For communication, it uses a special plain-text protocol – the GDB Remote Serial Protocol (RSP).
The most convenient way to interact with this debugger is by using gdb itself: target extended-remote target.ip:port. Note that gdb provides very convenient commands remote get and remote put (for example, remote get remote_path local_path), which allow you to download/upload arbitrary files.
Get the latest content on web security
in your inbox each week.