优惠卷的并发使用 主要是在形成订单的那个数据包那里,进行并发,如果可以创建多个订单并且显示在前端页面,尝试刷新页面防止前端误报,若还是存在,则存在漏洞。基本到可以创建订单就差不多了,也可以尝试去支付,但最好金额不要太大。
漏洞的原理
这个漏洞属于条件竞争漏洞,核心本质是多个进程或线程同时争夺一个资源,最后的结果取决于一些"运气"(执行的先后顺序和微妙的时间差)。
后端服务器(Nginx,Apache HTTP Server,Tomcat)基本都是多线程(或进程)去处理请求的
当你并发几十个请求一起发送到后端服务器,后端服务器会并发的处理这些请求,但是并发并不是物理意义上的一起处理,是按照一定的顺序,先处理这个请求一部分,在处理这个请求一部分,这样循环的。
加上对于优惠卷使用的检查流程大概是这样的:先检查数据库这个用户是否有优惠卷,然后请求使用优惠券,最后才会修改数据库的数据。
这样就会导致每个请求和数据修改之间会有一个微妙的时间差,也就是TOCTOU(检查时与使用时之间的时序漏洞),在请求一通过了检查,但是还没有修改数据库的时候,这是如果轮转到了请求二,那因为此时还没有修改数据库,所以检查仍然通过,这就导致了优惠卷重复使用。
根据此原理,主要的修补方案有两种:数据库方面加锁 和 分布式锁。
一、数据库层面加锁
一般来说查询数据用的sql语句是
sql
SELECT is_used FROM coupons WHERE id = 123
而修改查询语句使用FOR UPDATE就可以解决这个问题
sql
SELECT is_used FROM coupons WHERE id = 123 FOR UPDATE
当线程1执行到这条查询的时候,数据库会把id=123这一行锁住,其他的线程查询会显示在使用,只有线程1完全完成才会把id=123这条放开,此时数据库已经更改完成,线程2去请求则不会出现条件竞争的情况
二、分布式锁
在如今的现实中通常都是分布式的高并发情况,一般会采用Redis做一个分布式锁
Redis是单线程的、基于内存的、高性能的键值对(Key-Value)数据库服务器
一般不会用普通数据库比如MySQL数据库,因为它们的数据都是存放在硬盘上的,读写硬盘涉及大量的I/O操作速度相对较慢,高并发很容易卡死;而Redis是全部存在内存(RAM)里的,读写速度比硬盘快成千上百倍,官方显示Redis一秒钟可以处理10万次以上的读写操作请求。而且Redis是单线程执行命令的,无论多少请求都得一个个处理,很适合做锁,但是因为现在都是分布式高并发,为了适应那些的速度,Redis的处理速度必须很快,不过好在Redis并不去处理很复杂的东西,只是快速判断是否存在该键值对,有就等待或丢弃,没有就创建然后返回给后台的分布式服务器去执行服务,执行完后再回Redis销毁该键值对。
比如说你并发了50个请求,然后Redis会先让第一个请求进去,然后根据用户或者商品创建一个key,比如是按照用户创建的,那第二个请求进入的时候会发现已经创建了一个改用户的key,此时就会停止,停止后有两种可能(看后端开发式怎么写的),一是直接返回错误,二是等待每隔一段时间再去请求直到空闲,然后再去处理第二个请求。但是不管哪一种,都不再存在条件竞争,在key一样的情况下会被强制等待或丢弃。
整体的流程大概是这样的

Redis的锁,颗粒度很小很精准,所住的是具体某个用户的特定资源,而不是整个业务。而且锁的时间相对于整个的过程来说时间很短。
Redis只锁同键,不锁异键。
当然Redis锁什么也很有思考,比如购物时锁的如果是商品ID,那全网购买这个商品的用户都会被强制等待,就算你用bp的Turbo Intruder也没用,因为你并发多少次也都是那个商品ID;但是如果他锁的是用户的ID,就可以尝试用多个用户并发去尝试,因为他锁的是用户ID,那多个不同的用户并发就不会受到Redis锁的影响,就回去同时去向底层的MySQL数据库去扣库存(多个线程并发扣)就可能导致最后被扣成负数。
这些就是关于优惠卷并发使用的一些原理,当然上述说的都是可能成功,并发和网速的关系也很大,网速快成功概率和成功并发的数量都会更多(因为是在短时间内发大量包,网速越快,发的越多,就越可能抓住 TOCTOU 也就是检查和使用之间的这个时间差)。