從SUCTF2019到python源碼


搶先領取全套VIP視頻教程
+10天免費學習名額





報名CTF挑戰賽, 預約名師指導


前言
前段時間打的SUCTF2019中有一個題目叫Pythongin思路大概來源于黑帽大會
https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf
不怎么明白漏洞的原理,就準備復現一下,在復現的過程中出現了很多坑,來記錄一下。
踩坑過程
整個SUCTF2019的源碼都已經開源了,地址如下
https://github.com/team-su/SUCTF-2019
具體題目的分析過程就不再贅述了,感覺師傅們分析的一個比一個詳細,我在文末也放了幾個師傅的分析的writeup
搭建好docker的環境,直接按照文檔中的命令可以直接復現成功
docker build -t dockerflask .
docker run -p 3000:80 dockerflask
open http://localhost:3000
然后按照題目的payload可以直接復現成功
然而問題來了,我比較懶,我是直接把文件放到了我的sublime 然后,使用一個官方的payload 居然沒有打成功懷疑是我的環境和編輯器的編碼出現了問題,然后放到我的WSL系統里面運行(這里說一句題外話,最近剛剛給自己的電腦安裝了WSL win下的ubuntu,感覺很好用 ,想用linux的時候不必再去開虛擬機了)大家可以去嘗試一下,同時也順便美化一下自己的終端。
再win下的環境是
在linux下的環境是
/mnt/d/CTF/SUCTF python3
Python 3.6.8 (default, Jan 14 2019, 11:02:34)
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
在win下的環境是
C:\Users\11466>python3
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
可以看版本不一樣,同時python最后更新的時間也不一樣問題就出現在這里了。
對源碼做出的簡單的修改,用來測試payload
def getUrl2(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 "success"
else:
return "我扌 your problem? 333"
if __name__=="__main__":
# get_unicode()
# try:
# print_unicode()
# except:
# print("something_error")
url = "file://suctf.c?sr%2ffffffflag @111"
print(url)
print(getUrl2(url))
# print(getUrl(url))
# get_unicode()
可以出運行不同的結果:
在win下
file://suctf.c?sr%2ffffffflag @111
Traceback (most recent call last):
File "1.py", line 72, in <module>
print(getUrl2(url))
File "1.py", line 45, in getUrl2
host = parse.urlparse(url).hostname
File "E:\python3\lib\urllib\parse.py", line 368, in urlparse
splitresult = urlsplit(url, scheme, allow_fragments)
File "E:\python3\lib\urllib\parse.py", line 461, in urlsplit
_checknetloc(netloc)
File "E:\python3\lib\urllib\parse.py", line 407, in _checknetloc
"characters under NFKC normalization")
ValueError: netloc 'suctf.cc/usr%2ffffffflag @111' contains invalid characters under NFKC normalization
在WSL下
/mnt/d/CTF/NUCA python3 1.py
file://suctf.c?sr%2ffffffflag @111
success
原來沒怎么分析過python的源碼
但是從報錯信息上可以找到,問題就出現在python3\lib\urllib\parse.py
于是就來簡單分析了下parse.py的源碼
源碼對比
在win下是比較新的一個python,很明顯對于這類漏洞已經修補
在WSL下的是一個比較舊的python
使用在win下找到E:\python3\lib\urllib\parse.py
在WSL下找到 /usr/lib/python3.6/urllib/parse.py
/mnt/d/CTF/SUCTF python3
Python 3.6.8 (default, Jan 14 2019, 11:02:34)
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/lib/python36.zip', '/usr/lib/python3.6', '/usr/lib/python3.6/lib-dynload', '/home/fangzhang/.local/lib/python3.6/site-packages', '/usr/local/lib/python3.6/dist-packages', '/usr/lib/python3/dist-packages']
>>>
/mnt/d/CTF/SUCTF cd /usr/lib/python3.6/urllib/
/usr/lib/python3.6/urllib
使用文本對比工具
得到以下結果:
--- E:\python3\Lib\urllib\parse.py
+++ /usr/lib/python3.6/urllib
@@ -390,21 +390,6 @@
if wdelim >= 0: # if found
delim = min(delim, wdelim) # use earliest delim position
return url[start:delim], url[delim:] # return (domain, rest)
-
-def _checknetloc(netloc):
- if not netloc or netloc.isascii():
- return
- # looking for characters like \u2100 that expand to 'a/c'
- # IDNA uses NFKC equivalence, so normalize for this check
- import unicodedata
- netloc2 = unicodedata.normalize('NFKC', netloc)
- if netloc == netloc2:
- return
- _, _, netloc = netloc.rpartition('@') # anything to the left of '@' is okay
- for c in '/?#@:':
- if c in netloc2:
- raise ValueError("netloc '" + netloc2 + "' contains invalid " +
- "characters under NFKC normalization")
def urlsplit(url, scheme='', allow_fragments=True):
"""Parse a URL into 5 components:
@@ -424,6 +409,7 @@
i = url.find(':')
if i > 0:
if url[:i] == 'http': # optimize the common case
+ scheme = url[:i].lower()
url = url[i+1:]
if url[:2] == '//':
netloc, url = _splitnetloc(url, 2)
@@ -434,8 +420,7 @@
url, fragment = url.split('#', 1)
if '?' in url:
url, query = url.split('?', 1)
- _checknetloc(netloc)
- v = SplitResult('http', netloc, url, query, fragment)
+ v = SplitResult(scheme, netloc, url, query, fragment)
_parse_cache[key] = v
return _coerce_result(v)
for c in url[:i]:
@@ -458,7 +443,6 @@
url, fragment = url.split('#', 1)
if '?' in url:
url, query = url.split('?', 1)
- _checknetloc(netloc)
v = SplitResult(scheme, netloc, url, query, fragment)
_parse_cache[key] = v
return _coerce_result(v)
@@ -600,7 +584,7 @@
# if the function is never called
global _hextobyte
if _hextobyte is None:
- _hextobyte = {(a + b).encode(): bytes.fromhex(a + b)
+ _hextobyte = {(a + b).encode(): bytes([int(a + b, 16)])
for a in _hexdig for b in _hexdig}
for item in bits[1:]:
try:
@@ -750,7 +734,7 @@
_ALWAYS_SAFE = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b'abcdefghijklmnopqrstuvwxyz'
b'0123456789'
- b'_.-~')
+ b'_.-')
_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE)
_safe_quoters = {}
@@ -782,17 +766,14 @@
Each part of a URL, e.g. the path info, the query, etc., has a
different set of reserved characters that must be quoted.
- RFC 3986 Uniform Resource Identifiers (URI): Generic Syntax lists
+ RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
the following reserved characters.
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
- "$" | "," | "~"
+ "$" | ","
Each of these characters is reserved in some component of a URL,
but not necessarily in all of them.
-
- Python 3.7 updates from using RFC 2396 to RFC 3986 to quote URL strings.
- Now, "~" is included in the set of reserved characters.
By default, the quote function is intended for quoting the path
section of a URL. Thus, it will not encode '/'. This character
可以明顯得看出,主要是多了一個處理函數
-def _checknetloc(netloc):
- if not netloc or netloc.isascii():
- return
- # looking for characters like \u2100 that expand to 'a/c'
- # IDNA uses NFKC equivalence, so normalize for this check
- import unicodedata
- netloc2 = unicodedata.normalize('NFKC', netloc)
- if netloc == netloc2:
- return
- _, _, netloc = netloc.rpartition('@') # anything to the left of '@' is okay
- for c in '/?#@:':
- if c in netloc2:
- raise ValueError("netloc '" + netloc2 + "' contains invalid " +
- "characters under NFKC normalization")
同時也可以看出,這次主要更新的地方。
如下對上面的代碼進行分析
源碼分析
unicode規范化處理
如下引用一下https://python3-cookbook.readthedocs.io
關于unicode的規范化處理,有如下說明
unicode的規范化格式有幾種,每種的處理方式有些不一樣。
NFC
Unicode 規范化格式 C。如果未指定 normalization-type,那么會執行 Unicode 規范化。
NFD
Unicode 規范化格式 D。
NFKC
Unicode 規范化格式 KC。
NFKD
Unicode 規范化格式 KD。
在Unicode中,某些字符能夠用多個合法的編碼表示。為了說明,考慮下面的這個例子:
>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15
>>>
這里的文本”Spicy Jalapeño”使用了兩種形式來表示。
第一種使用整體字符”ñ”(U+00F1),第二種使用拉丁字母”n”后面跟一個”~”的組合字符(U+0303)。
在需要比較字符串的程序中使用字符的多種表示會產生問題。
為了修正這個問題,你可以使用unicodedata模塊先將文本標準化:
>>> import unicodedata
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True
>>> print(ascii(t1))
'Spicy Jalape\xf1o'
>>> t3 = unicodedata.normalize('NFD', s1)
>>> t4 = unicodedata.normalize('NFD', s2)
>>> t3 == t4
True
>>> print(ascii(t3))
'Spicy Jalapen\u0303o'
>>>
normalize()
第一個參數指定字符串標準化的方式。NFC表示字符應該是整體組成(比如可能的話就使用單一編碼),而NFD表示字符應該分解為多個組合字符表示。
Python同樣支持擴展的標準化形式NFKC和NFKD,它們在處理某些字符的時候增加了額外的兼容特性。比如:
>>> s = '\ufb01' # A single character
>>> s
'?'
>>> unicodedata.normalize('NFD', s)
'?'
# Notice how the combined letters are broken apart here
>>> unicodedata.normalize('NFKD', s)
'fi'
>>> unicodedata.normalize('NFKC', s)
'fi'
>>>
漏洞分析
根據以上分析
主要的修復方式就是通過對url中的unicode進行規范化處理了,現在通過具體的例子來分析一哈。
import unicodedata
netloc2 = unicodedata.normalize('NFKC', netloc)
if netloc == netloc2:
return
用我們的WSL的環境(也就是沒有打上補丁的環境)進行測試
>>> from urllib.parse import urlsplit
>>> u = "https://example.com\uFF03@bing.com"
#不處理的結果
>>> SplitResult(scheme='https', netloc='example.com#@bing.com', path='', query='', fragment='')
#規范化處理的結果
>>>import unicodedata
>>>u2 = unicodedata.normalize('NFKC', u)
>>> urlsplit(u2)
SplitResult(scheme='https', netloc='example.com', path='', query='', fragment='@bing.com')
#特殊編碼處理的結果
>>>u3 = u.encode("idna").decode("ascii")
>>> urlsplit(u3)
SplitResult(scheme='https', netloc='example.com', path='', query='', fragment='@bing.com')
以上就是漏洞的原理,不同的編碼經處理之后,經過urlsplit() 處理之后,得到的的netloc是不一樣的
IDNA(Internationalizing Domain Names in Applications)IDNA是一種以標準方式處理ASCII以外字符的一種機制,它從unicode中提取字符,并允許非ASCII碼字符以允許使用的ASCII字符表示。
unicode轉ASCII發生在IDNA中的TOASCII操作中。如果能通過TOASCII轉換時,將會以正常的字符呈現。而如果不能通過TOASCII轉換時,就會使用“ACE標簽”,“ACE”標簽使輸入的域名能轉化為ASCII碼
所以在新的urlsplit函數中會增加一個判斷,如果規范化處理的結果和原來的結果一樣,才能返回正確的值。
題目分析
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, timeout=2).read()
else:
return "我扌 your problem? 333"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
根據以上分析,題目就比較簡單了,只需要滿足hostnameencode('idna').decode('utf-8'))
處理之前不是suctf.cc
處理之后是suctf.cc
就好了
后記
漏洞不難理解,只是覺得應該記錄一下
看到了一句話,摘自某位大佬:
如果翻譯器對程序進行了徹底的分析而非某種機械的變換, 而且生成的中間程序與源程序之間已經沒有很強的相似性, 我們就認為這個語言是編譯的. 徹底的分析和非平凡的變換, 是編譯方式的標志性特征.
如果你對知識進行了徹底的分析而非某種機械的套弄, 在你腦中生成的概念與生硬的文字之間已經沒有很強的相似性, 我們就認為這個概念是被理解的. 徹底的分析和非凡的變換, 是獲得真知的標志性特征.
與君共勉。
參考鏈接
參考很多鏈接,不過我覺得,遇到問題看官方的文檔和源碼更有效果
https://xz.aliyun.com/t/6042#toc-29
https://www.anquanke.com/post/id/184858#h3-13
https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf
https://bugs.python.org/issue36216
https://python3-cookbook.readthedocs.io/zh_CN/latest/c02/p09_normalize_unicode_text_to_regexp.html