前言
github項目地址:
https://github.com/bowu678/php_bugs
首先這篇文章是面向PHP代碼審計的萌新以更好的入坑而準備的,所以會比較詳細,並且是以萌新的角度來解題,hh,我也是萌新。
0x01extract變量覆蓋
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan)){
$content=trim(file_get_contents($flag));
if($shiyan==$content{
echo'ctf{xxx}';
}else{
echo'Oh.no';
}
}
?>
首先通讀一波代碼
這裏有幾個函數
extract()函數從數組中將變量導入到當前的符號表。
isset()檢測變量是否設置,並且不是 NULL。
trim()函數移除字符串兩側的空白字符或其他預定義字符。
file_get_contents()函數把整個文件讀入一個字符串中。
首先extract()函數從數組中將變量導入到當前的符號表。
符號表的概念:
符號表是指當前php頁面中,所有變量名稱的集合,可以使用函數get_defined_vars直接獲得當前所有已定義變量列表的多維數組
$_GET 變量是一個數組,內容是由 HTTP GET 方法發送的變量名稱和值。
這樣或許你還是不理解,那麼好,我們來看一個例子。
<?php
$a = 1;
$b = array("a"=>2);
extract($b);
var_dump($a);
運行結果是:
int(2)
這裏的變量a的值不是1嗎?爲什麼變成2了呢?
extract函數將a這個鍵值映射成變量名,而鍵值被映射爲變量值
也就是說上面一個php等同於
<?php
$a = 1;
$a = 2;
var_dump($a);
好,那麼迴歸正題,看到後面部分
if(isset($shiyan)){
$content=trim(file_get_contents($flag));
if($shiyan==$content{
echo'ctf{xxx}';
}else{
echo'Oh.no';
}
}
判斷shiyan變量是否設置,設置就將trim(file_get_contents($flag))
賦值給content變量,但是這裏我們發現file_get_contents這個函數是拿來讀文本的,也就是說它讀flag變量是什麼都沒有的,然後trim函數移除字符串兩側的空白字符或其他預定義字符。
也就是$content='';
最後將shiyan變量和content變量進行比較,相等即輸出flag。
所以思路很明確,我們只需要傳入一個shiyan變量爲空就可以了
故payload:
/?shiyan=
這裏放出另一種思路,通過php僞協議
/?shiyan=a&flag=php://input
POST:a
0x02繞過過濾的空白字符
<?php
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //爲一個配置選項設置值
error_reporting(0); //關閉所有PHP錯誤報告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP頭顯示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同於 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 語法結構提供了遍歷數組的簡單方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾處的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 檢測變量是否是字符串,addslashes — 使用反斜線引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 獲取變量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 獲取字符串長度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 檢測變量是否爲數字或數字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 獲取變量的整數值
{
$info = "number must be equal to it's integer!! ";
}
else
{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
}
echo $info;
這個代碼比較長,很多萌新肯定就被唬住了
好,沒關係,我們從頭開始分析
$info = ""; //定義string(字符串)
$req = []; //定義array(數組)
$flag = "xxx"; //定義string(字符串)
ini_set("display_error", false); //爲一個配置選項設置值
error_reporting(0); //關閉所有PHP錯誤報告
好,開始看下面一部分
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP頭顯示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同於 exit()
}
這段代碼講的是如果沒有傳入number參數(number參數沒有設置),就會在header頭添加一個hint(提示),並且die一個have a fan!! 這個die的意思差不多就是exit+echo的意思了,這個hint的意思肯定也就是源代碼的存放位置了,但是我們這個審計是直接上源碼的,不存在這些的,好了廢話不多說,看下一部分。
foreach([$_GET, $_POST] as $global_var) { //foreach 語法結構提供了遍歷數組的簡單方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾處的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 檢測變量是否是字符串,addslashes — 使用反斜線引用字符串
}
}
首先使用了foreach循環遍歷數組,$_GET作爲鍵名,$_POST作爲鍵值,那麼上個題目我們也說過了$_GET,所以這裏的$_POST同理,無非就是兩個不同的傳值方法而已,這個foreach循環將$_GET和$_POST傳入的值給賦值到global_val這個變量,然後又使用了一個foreach循環將鍵值和鍵名分別賦值給key和value變量,但是這都不重要,沒錯,一般foreach循環不是拿來輸出變量的,而是拿來遍歷處理數組裏的數據的,很明顯,這裏作爲鍵值的value經過處理了,trim函數上題也說過(trim()函數移除字符串兩側的空白字符或其他預定義字符)
is_string()函數(判斷變量是否爲字符串)如果指定變量爲字符串,則返回 TRUE,否則返回 FALSE。
is_string($value) && $req[$key] = addslashes($value);
這段代碼的意思是,先判斷value變量是否爲字符串,如果是字符串,那麼就將經過addslashes()(在每個雙引號(")前添加反斜槓)函數處理後的值賦給value變量和$req$key
接下來我們看定義的函數is_palindrome_number(看字面意思就是判斷是不是迴文)
function is_palindrome_number($number) {
$number = strval($number); //strval — 獲取變量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 獲取字符串長度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
首先strval函數上面已經有解釋了不多贅述,這段代碼大概的意思就是,如果i<j的時候就會將number的第i位和第j位進行對比,如果不一樣就會返回false
然後i+1,j-1,直到j<i。
if(is_numeric($_REQUEST['number'])) //is_numeric — 檢測變量是否爲數字或數字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 獲取變量的整數值
{
$info = "number must be equal to it's integer!! ";
}
需要繞過is_numeric函數,需要返回false才能進行下個判斷,繞過的方法很簡單加個%00即可返回false,然後看到下個判斷,number這個變量的值得經過兩個類型轉換之後還不能等於自身才會跳到最後一個判斷,而後面一個判斷value1變量和value2(strrev()函數反轉字符串)是否相等,相等就進入下一個判斷,也就是判斷它是否爲迴文數,而最後這個判斷number這個變量是否爲迴文,不是則輸出flag,所以這個點很矛盾,number這個變量又得是迴文數又不能是迴文數。
整個題目的意思是,想要拿到flag,首先需要繞過is_numeric函數,而且有需要是迴文,又不能是迴文數這樣的一個數,繞過is_numeric函數很簡單,在傳入的值最前面和最後面加%00即可,而滿足這種情況的數我們可以使用科學記數法來繞過[0e-0(=0)]
,
故payload:
\?number=0e-0%00
第二個payload:
\?number=%00%2B%00
0x03多重加密
<?php
<?php
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
//把一個或多個數組合併爲一個數組
class db
{
public $where;
function __wakeup()
{
if(!empty($this->where))
{
$this->select($this->where);
}
}
function select($where)
{
$sql = mysql_query('select * from user where '.$where);
//函數執行一條 MySQL 查詢。
return @mysql_fetch_array($sql);
//從結果集中取得一行作爲關聯數組,或數字數組,或二者兼有返回根據從結果集取得的行生成的數組,如果沒有更多行則返回 false
}
}
if(isset($requset['token']))
//測試變量是否已經配置。若變量已存在則返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:進行字符串壓縮
//unserialize: 將已序列化的字符串還原回 PHP 的值
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
//mysql_real_escape_string() 函數轉義 SQL 語句中使用的字符串中的特殊字符。
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}
?>
好傢伙,又這麼多代碼,好勒,之所以叫代碼審計,那肯定是有方法的,肯定不是從頭看到尾,不要槓我,以後要是審thinkphp,難道去把全部代碼都熟悉一次?不可能對吧,那麼我們首先看輸出flag的點
if($login['user'] === 'ichunqiu'){
echo $flag;
}
login這個數組裏的以user爲鍵名的值得等於ichunqiu,那麼得看到login數組是從哪裏傳遞過來的
if(isset($requset['token']))
//測試變量是否已經配置。若變量已存在則返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:進行字符串壓縮
}
可以看到是$request傳入的token然後進行了base64解密,gzuncompress,gz解壓縮,unserialize反序列化操作。
那麼也就是說,我們只要控制login[user]=ichunqiu就能輸出flag,反向操作就行,也就是先進行序列化,然後gzcompress,在進行serialize序列化操作。
那麼生成token
<?php
$a = array('user'=>'ichunqiu');
echo base64_encode(gzcompress(serialize($a)));
故payload:
/?token=eJxLtDK0qi62MrFSKi1OLVKyLraysFLKTM4ozSvMLFWyrgUAo4oKXA==