Nginx/Apache Path Confusion to Auth Bypass in PAN
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>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>/unauth/foo/bar/baz</span>, we will have <span>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:
# 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
If the file requested is a PHP file, Apache will then pass through the request via <span>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, you may be suspicious of the above <span>RewriteRule</span>:
# ...
RewriteRule ^(.*)(\/PAN_help\/)(.*)\.(css|js|html|htm)$ $1$2$3.$4.gz [QSA,L]
# ...
However, because this RewriteRule is in a location block, this rewrite happens in so-called "per-dir" context, 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>RewriteRule</span>. To explain this better, let's consider this simplified configuration:
RewriteEngine On
RewriteRule (.*)\.abc$ $1 [L]
This rule strips the <span>.abc</span> extension from the end of a request, so <span>/foo/hello.html</span> and <span>/foo/hello.html.abc</span> end up requesting the same file. If we send a request to <span>/foo/hello.html.abc</span>, what happens in order is as follows:
- <span>mod_rewrite</span> gets the requested file as <span>foo/hello.html.abc</span> and tests it against the rewrite rule.
- Since the rule matches, <span>mod_rewrite</span> does the substitution and ends up with <span>foo/hello.html</span>
- <span>mod_rewrite</span> then appends a slash as the front to get <span>/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>/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>/foo/hello.html</span>, since the request is URL decoded before processing. However, if we double encode and request <span>/foo/hello%252ehtml</span>, we will get a 404. This is because Apache only url decodes once, and a file <span>hello%25html</span> doesn't exist. However, if we request <span>/foo/hello%252ehtml.abc</span>, we do in fact see the content of <span>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>/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>/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>..</span> sequence. Then Nginx will test this <span>$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>X-pan-AuthCheck</span> header to <span>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>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>mod_php</span>.
/php/ztp_gate.php/PAN_help/x.css.gz
<span>mod_php</span> sees that this looks like a request for a path <span>SCRIPT_FILENAME=/php/ztp_gate.php</span> with some path info afterwards (<span>PATH_INFO=/PAN_help/x.css.gz</span>), so it will execute <span>/php/ztp_gate.php</span>. And since the <span>X-pan-AuthCheck</span> header was set to <span>off</span> by Nginx, no authentication is required!
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
Zero Touch Provisioning
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 issue is fixed in PAN-OS 10.2.14, PAN-OS 11.0.7, PAN-OS 11.2.5, and all later PAN-OS versions. Internally, this was assigned as PAN-273971. Palo Alto recommends whitelisting 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.
How it works
Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.
Questions are cached — you'll always get the same 5 for this article.