Advisory

Yorick Koster, November 2017

Clickjacking vulnerability in CSRF error page pfSense

Abstract

pfSense is a free and open source firewall and router. It was found that the pfSense WebGUI is vulnerable to Clickjacking. By tricking an authenticated admin into interacting with a specially crafted webpage it is possible for an attacker to execute arbitrary code in the WebGUI. Since the WebGUI runs as the root user, this will result in a full compromise of the pfSense instance.

/advisory/SFY20171101/clickjacking-demo.gif

Tested versions

This issue was successfully tested on pfSense version 2.4.1.

Fix

pfSense 2.4.2-RELEASE was released that addresses the Clickjacking issue.

Introduction

pfSense is a free and open source firewall and router. It was found that the pfSense WebGUI is vulnerable to Clickjacking. This vulnerability allows an attacker to execute arbitrary code with root privileges.

Details

The pfSense WebGUI uses the csrf-magic library to protect against Cross-Site Request Forgery (CSRF) attacks. This library contains a user friendly error page that is implemented in the csrf_callback() function. This error page is shown whenever the users submits an incorrect (or missing) CSRF token. The error page contains a 'Try again' button that allows the user to re-submit the requested action; the invalid token is replaced with a valid token. The default callback function is listed below, which is also used by pfSense.

/usr/local/www/csrf/csrf-magic.php:
function csrf_callback($tokens) {
   // (yes, $tokens is safe to echo without escaping)
   header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
   $data = '';
   foreach (csrf_flattenpost($_POST) as $key => $value) {
      if ($key == $GLOBALS['csrf']['input-name']) continue;
      $data .= '<input type="hidden" name="'.htmlspecialchars($key).'" value="'.htmlspecialchars($value).'" />';
   }
   echo "<html><head><title>CSRF check failed</title></head>
      <body>
      <p>CSRF check failed. Your form session may have expired, or you may not have
      cookies enabled.</p>
      <form method='post' action=''>$data<input type='submit' value='Try again' /></form>
      <p>Debug: $tokens</p></body></html>
";
}

/advisory/SFY20171101/csrf-magic-error-page.png
Figure 1: CSRF error page with 'Try again' button

The use of this error page introduces a risk as in case of a CSRF attempt, the victim will only be shown this error page. The victim may be enticed to click the 'Try again' button, thus executing the attacker's specially crafted action. What is even more interesting is that the CSRF logic is executed before the WebGUI sets the X-Frame-Options header, which should mitigate Clickjacking. In case of an invalid CSRF token, execution of the script will be stopped after the error page is returned and as a result the X-Frame-Options header will not be set. Consequently, the CSRF error page is prone to Clickjacking attacks.

/usr/local/www/guiconfig.inc:
/* Include authentication routines */
/* THIS MUST BE ABOVE ALL OTHER CODE */
include_once('phpsessionmanager.inc');
if (!$nocsrf) {
   function csrf_startup() {
      global $config;
      csrf_conf('rewrite-js', '/csrf/csrf-magic.js');
      $timeout_minutes = isset($config['system']['webgui']['session_timeout']) ? $config['system']['webgui']['session_timeout'] : 240;
      csrf_conf('expires', $timeout_minutes * 60);
   }
   require_once("csrf/csrf-magic.php");
   if ($_SERVER['REQUEST_METHOD'] == 'POST') {
      phpsession_end(true);
   }
}
/* make sure nothing is cached */
if (!$omit_nocacheheaders) {
   header("Expires: 0");
   header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
   header("Cache-Control: no-cache, no-store, must-revalidate");
   header("Pragma: no-cache");
}
header("X-Frame-Options: SAMEORIGIN");

The CSRF error page does include a Javascript framebreaker script that also mitigates Clickjacking in some cases. In this case it is trivial to bypass this framebreaker script by opening the target page within a sandboxed iframe with the allow-forms attribute set. The allow-forms attribute allows for the form post when a victim clicks the 'Try again' button.

/usr/local/www/csrf/csrf-magic.php:
if ($GLOBALS['csrf']['frame-breaker']) {
   $buffer = str_ireplace('</head>', '<script type="text/javascript">if (top != self) {top.location.href = self.location.href;}</script></head>', $buffer);
}

An attacker can use this issue to perform a Clickjacking attack against an authenticated admin. This requires that the attacker knows the URL of the WebGUI and tricks an authenticated admin into visiting a specially crafted webpage. This webpage will make an arbitrary POST to the WebGUI containing an invalid token. The POST is done to a sandboxed iframe. Using UI redressing the attacker can trick the victim into clicking the 'Try again' button, resulting in the POST to be resend to the WebGUI - this time containing a valid CSRF token. A successful attack will result in the execution of arbitrary code by the WebGUI. Since the WebGUI runs as the root user, this will result in a full compromise of the pfSense instance.

Latest News & Research

Work with us →