一个PHP安全问题带来的思考
背景
上周末我收到了一个做安全的朋友的消息,让我帮忙看一个PHP源文件的漏洞,起先我以为是他只无聊想攻击别人,所以只是扫了一眼就下意识告诉他“写的不规范但是好像也没什么大的漏洞”。后来他又告诉我这是一个CTF试题(这里是网络安全竞赛),这时候我才意识到我有点马虎了,这里面应该触及我的知识盲区,或者太想当然。必需认证对待这个问题了。
问题
问题描述
简单地说,我们已经获得了一个部署在服务器上通过WEB访问的PHP源码文件,需要想办法攻击它来读取靶机上的其他文件。
PHP源码
<?php
if(!isset($_GET['file'])) {
header('Location: /?file=log.txt');
die();
}
$file = $_GET['file'];
$re = '/^\w*\.\w*$/m';
preg_match_all($re, $file, $matches, PREG_SET_ORDER, 0);
if(count($matches) != 1) {
die('illegal operation!');
}
echo file_get_contents($file);
?>
思路分析
- 从源码上来看,想进行靶机其他文件读取,相比
preg_match_all
函数,file_get_contents
更像是我们的目标,而其接受的变量刚好可以通过构造$_GET['file']
来控制。 -
$_GET['file']
变量仅用了简单的正则校验,那么很明显,此题解法应该是构造payload进行代码漏洞攻击。 - 从上面第2点看来,此题的关键就在这个正则
/^\w*\.\w*$/m
了,那么能否构造出符合这个正则的其他目录文件的路径呢?- 首先,\w仅能是字母、数字和_,^和$限制了字符串首尾位置,因此可以看出源文件作者是想控制仅能读取当前目录下*.*格式的文件。
- 但是这个正则有个很大的漏洞,就是最后的m,m意味着可以进行多行匹配,也就是说可以通过添加换行符来控制一个字符串匹配项的数量和内容。举例说明的话,比如这里如果没有m(正则
/^\w*\.\w*$/
),那么仅有类似于log.log
这样的字符串能够匹配出内容,但有了这个m后,诸如/www/\nlog.log
这样的字符串也可以匹配出内容了。(下为示例)
<?php
$name_1 = "log.log";
$name_2 = "/www/\nlog.log";
$re = '/^\w*\.\w*$/';
preg_match_all($re, $name_1, $matches, PREG_SET_ORDER, 0);
var_dump($matches);
/*
array(1) {
[0]=>
array(1) {
[0]=> string(7) "log.log"
}
}
*/
preg_match_all($re, $name_2, $matches, PREG_SET_ORDER, 0);
var_dump($matches);
/*
array(0) {}
*/
$re = '/^\w*\.\w*$/m';
preg_match_all($re, $name_1, $matches, PREG_SET_ORDER, 0);
var_dump($matches);
/*
array(1) {
[0]=>
array(1) {
[0]=> string(7) "log.log"
}
}
*/
preg_match_all($re, $name_2, $matches, PREG_SET_ORDER, 0);
var_dump($matches);
/*
array(1) {
[0]=>
array(1) {
[0]=> string(7) "log.log"
}
}
*/
- 漏洞当然不止于此,可以看到源码中还有判断匹配项数量必须为1的逻辑,组合起来似乎就豁然开朗了,这里应该就是那个漏洞,通过构造带有换行符的、带有正确文件名的文件路径就可以了。
- 既然到了这里,事不宜迟,我们立即开始着手构造这样的文件路径(例如
/www/%0a.env
)进行请求——然而我们这时候都忽略了一个问题,%0a(urlencode的换行符)作为路径传入后,换行符是不会被file_get_contents
函数忽略的,所以果不其然,报了warning,找不到文件。
困境
到了这里,其实我们两个开始了各种“胡思乱想”了
- “既然是因为路径找不到,那么我自己搭建服务器,让他远程读我的文件算漏洞么”
- “……人家要读的是靶机。”
- “……”
看起来,唯一的构造路径、使用file_get_contents
来获取文件的路已经越走越窄了。
- “说到读远端文件,其实php读取post输入还有一种php://input方式,这种php伪协议路径从格式上来说也很类似。”
- “我感觉应该很接近了。”
从这个角度触发,我们找到了PHP的伪协议——输入/输出流,其中有一项php://filter
的功能与我们的需求极为相似:
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
可惜我们陷入了文件路径中无法添加进换行符的牛角尖里,直到最后我们还是没能想出题解。
答案
该题的答案是: php://filter/read=%0aconvert.base64%0a-encode/resource=xxxx
答案是我们之后找来的,其实看到答案的时候我们就顿悟了,发现我们的思路其实已经及其接近,但是最后陷入了误区——我们一直坚定认为构造的一定是且仅是一个完整路径,无论是文件路径还是协议路径。
但是大家应该发现了,答案里在源resource
前指定了一个convert.base64的过滤器,而这个过滤器的名字格式正好与正则要求的格式一致!所以这里resource
对应的靶机目标文件路径不需要做任何处理,需要处理的仅是这个过滤器的名字。
思考
问题到这里就结束了吗?
有细心的朋友应该又想提出问题了:convert.base64前后加了换行符还能正常使用吗?答案是过滤器会因路径找不到报warning,但是依旧会去读取resource
源,只是不做处理。所以这里的答案其实可以是
php://filter/read=%0anystring.anystring%0a/resource=xxxx
只需要前后包裹换行符用于正则匹配就行了。
最后
如果有兴趣的同学想自己尝试一下,使用最上面的源码,自己搭建一个本地web服务就可以了,有时间也不妨发散一下看看有哪些过滤器、哪些php协议。
写到这里我其实挺惭愧的,相比其他语言,PHP算是用得最久的,但这样的用法(我也不知道算不算高级用法)还是第一次。我突然有点羡慕做安全的同学了,能真正切实地接触到各项语言、技术的细节和核心。在我们自己学习的过程中,单一的角度确实会限制视野和想象力,偶尔找找类似技术攻防或者学习下别人分析漏洞的思路或许能找到不一样的天地。
共勉。