题目
强网拟态的一道题EasyFilter,就一个php
1 |
|
理解一下代码:
- open_basedir没啥用
- action为w时,写的文件内容被base64,文件名不可控,相当于是
./files/xxxxx
,但是会输出路径给我们,相当于文件路径已知。 - action为r时,可以拼接一部分路径到
./files/[用户可控]
,后面又拼接到php://filter/resource=
这一部分中,所以思路是如果能让这个php://filter的过滤器convert.base64-decode生效,并且成功包含到我们的文件
payload
1 | ?action=w&c<%3fphp+eval($_REQUEST[0])%3b%3f> |
调试
调试正常流程
正常情况下,是这么写的php://filter/read=[过滤器]/resource=[文件路径]
,先基本的猜测一下php源码的解析过程,大概可能是匹配到read之后,把+2的地方作为过滤器部分进一步处理,匹配到resource之后把+2的地方作为文件路径来读取,而且肯定会有读文件的解析过程。
看一下源码怎么处理的。断点打在ext/standard/php_fopen_wrapper.c
的php_stream_url_wrap_php
函数的339行,该函数用来处理php_stream_wrapper
1 | php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, |
在*p = '\0';
前面的处理,将php://filter/read=convert.base64-decode/resource=./files/88c8ad25c9
处理为
- “/resource=./files/88c8ad25c9”,赋值给p
- “/read=convert.base64-decode/resource=./files/88c8ad25c9”,赋值给pathup
注意,这里的p和pathup是两个不同的指针,指向不同的字符串进行处理
接下来\0
截断处理后,此时整个串变为:/read=convert.base64-decode\0resource=./files/88c8ad25c9
再调用p = php_strtok_r(pathdup + 1, "/", &token);
,以字符串的第一个/
作为分割点,后半段赋值给token,前半段赋值给p
,而在切割的过程中,从/read
的+1的地方r
开始扫描,中间出现了\0
直接截断停止扫描,这里是一个关键点,payload的处理过程和这里不同,以下值更新为:
- token:空值(被
\0
截断了) - p:
read=convert.base64-decode
之后判断p是否有前缀read=
或write=
,有的话就截断掉,然后传入php_stream_apply_filter_list处理过滤器,即convert.base64-decode|..|..
的部分,在这个循环中以/
切割token
,不断取前半部分赋值给p
处理完过滤器的部分之后,就直接返回stream了。
调试payload
我们可以带着payload调试一下,重点关注path、p、pathup、token这几个表示路径的变量,其都是指针类型
调用strncasecmp
函数比较path的前七个字符是不是filter/
,不是则直接跳过,是才进入php://filter
的处理,然后判断时读操作还是写操作。
然后截取掉filter后半部分赋值给pathup,此时为/resource=./files/convert.base64-decode/../88c8ad25c9
然后调用strstr
函数判断pathup中/resource
第一次出现的位置,刚好,出现在开头,这里很关键,因为这使得pathup和p的值相同,所以他们指向的字符串值也是相同的
接下来调用php_stream_open_wrapper
,实际上是调用_php_stream_open_wrapper_ex
来封装我们的stream,在stream中,保留了处理的路径,以及过滤器,即filter。处理的路径在这一步就会确定下来,而filter在后续还会进一步处理,所以这里的路径就决定了我们包含文件的路径
p是/resource=./files/convert.base64-decode/../88c8ad25c9
,在正常情况下,p也是/resource=xxx
这样的值,而pathup是/read=xxx
,而这里,p和pathup是一样的,所以p+10的部分是./files/convert.base64-decode/../88c8ad25c9
,只要生成steam的函数中不会做其他过滤处理的话,那么我们最终的路径就是./files/88c8ad25c9
,完全可以实现包含。并且还有一个条件是,对于后续过滤器的处理,不会因为异常过滤器而报错退出(见后面分析),所以我们接下来先跟进php_stream_open_wrapper函数看一下生成stream时,会不会处理我们的path
最后保存path的是stream中orig_path这个变量,可以看到和我们传进来的是一模一样的。
接下来的分析只需要保证过滤器的处理部分不会报错退出即可,*p = '\0';
,这一部分处理,截断了/resource=./files/convert.base64-decode/../88c8ad25c9
的开头,整个串的情况和上面正常调试的一样,但是此时pathup指向的字符串的p是一样的,且头部是\0
,这意味着,在接下来pathup+1之后,\0
被跳过了
接下来调用php_strtok_r
将pathup+1(跳过/0
)的部分,以/
作为截断,后半部分赋值给token
,即files/convert.base64-decode/../88c8ad25c9
,前半部分以返回值的形式,赋值给p
,即resource=.
接下来就是和正常过程一样的循环,而这里的token是files/convert.base64-decode/../88c8ad25c9
,先截取的是files
,这不是一个合法的过滤器,但是并不会报错退出正常流程(只是Warning),在截取convert.base64-decode
的部分时,传入php_stream_apply_filter_list
中处理是一个合法的过滤器,会被正常处理。剩下的..
和88c8ad25c9
也和files
一样
所以最终这里没有报错,过滤器是convert.base64-decode
,路径是./files/convert.base64-decode/../88c8ad25c9
,可以实现base64解码后包含,所以以下两个是等价的
php://filter/read=convert.base64-decode/resource=./files/88c8ad25c9
php://filter/resource=./files/convert.base64-decode/../88c8ad25c9
去掉files
后也还是可以的
php://filter/read=convert.base64-decode/resource=./88c8ad25c9
php://filter/resource=./convert.base64-decode/../88c8ad25c9
总结
总结一下关键流程,以上面两个串为例
- 截去
php://filter
,赋值给pathup
- 关键点1:寻找子串
/resource
,子串所在及其后半部分赋值给p
,对于两种写法- 正常:
p
是/resource=./convert.base64-decode/../88c8ad25c9
pathup
是/read=convert.base64-decode/resource=./files/88c8ad25c9
- payload:
p
、pathup
都是/resource=./convert.base64-decode/../88c8ad25c9
- 正常:
- 关键点2:生成stream,路径是p+10,即除去
/resource=
,且路径不会被过滤 - 切断p的开头
*p='\0'
,目的是为了取出过滤器的部分,原串都会变为...\0resource=...
- 关键点3:从
pathup+1
处调用php_strtok_r
,以/
为分割并赋值给token和p- 正常:
p
是read=convert.base64-decode
token
是空,因为被\0
截断了,从read
处读到\0resource
被截断
- payload:
pathup+1
跳过了\0
p
是resource=.
token
是convert.base64-decode/../88c8ad25c9
- 正常:
- 循环对token调用
p = php_strtok_r(NULL, "/", &token);
,取出/
前半部分给p,p判断前缀是否是read=
或write=
(是则截取掉),不是一样正常处理。然后传入php_stream_apply_filter_list注册过滤器到stream的相关成员变量中- 正常:
p
截取为convert.base64-decode
,注册,token为空,一次循环直接退出 - payload:
p
不截取,以resource=.
传入,虽然是不合法过滤器,但是只报Warning,token依次切割成"convert.base64-decode"、".." "88c8ad25c9"
,最终可以注册convert.base64-decode
- 正常:
发散
这里在发散看一下一些灵光乍现的payload写法
写法1
1 | php://filter/[任意串]/resource=./files/convert.base64-decode/../88c8ad25c9 |
不行,因为\0
截断之后,获取的过滤器部分就是任意串
中的了,只有任意串符合read=xxx
或xxx
(其中xxx
才是想要注册的过滤器),不写read/write的话默认是read,所以下面这样也是可以的
1 | php://filter/convert.base64-decode/resource=./88c8ad25c9 |
其他写法貌似都不太行了。
总结
/resource
分割了过滤器部分和路径部分,所以过滤器部分必须放在/resource
前,除非/resource
前没有任何除php://filter
之外的字符(payload的情况)- 路径是
/resource
截断之后去除/resource
的部分,这是固定的