下面我通过一个充值业务来剖析高并发漏洞
1、用户提交充值码
2、数据库查询该码,如果查到库里有这个码并且码未被消费过,则走到第3步的逻辑里,否则返回code not useful
3、更新码的状态,更新消费该码的用户,
4、更新用户的余额,比如兑换的充值码为20元,用户money字段增加20
@user_blue.route('/exchange',methods=['POST'])
@user_auth
def exchange():
data = request.json
code = data.get('code') #1.用户提交充值码
try:
flag = Flag.query.filter_by(code=code).first() #数据库查询该码
if not flag or flag.isused is True: #表示无该充值码或该码已被消费过
return {"code":40001,"msg":"code not useful"}
else:
flag.used_id = session.get('uid') #设置消费该码的用户
flag.isused = True #将该码的状态更新为已使用
db.session.add(flag)
db.session.commit()
return jsonify({"msg":"exchange success"})
except Exception as e:
db.session.rollback()
raise e
这里的代码我先把第4步省略了
接下来我们来兑换一下充值码:880099
可以看到,该码的isused已经被更新成True(已使用),关联用户(userid=1)
所以当我们再次使用该码进行充值时,提示code not useful
那么这里有什么安全问题呢?现代化的应用都是多线程的,例如服务端支持10个多线程。
gunicorn -w4 -t10 -b0.0.0.0:5000 app:app
那么我发送6个并发请求过去,会发生什么呢?
兑换成功4次,失败2次。
通过SQL日志分析,并发请求了6次,后端创建了6个数据库事务(Engine BEGIN)。有4次COMMIT,2次ROLLBACK
下面,我完善了第4步,增加用余额
@user_blue.route('/exchange',methods=['POST'])
@user_auth
def exchange():
data = request.json
code = data.get('code')
try:
flag = Flag.query.filter_by(code=code).first()
if not flag or flag.isused is True: #表示无该充值码不可用
return {"code":40001,"msg":"code not useful"}
else:
flag.user_id = session.get('uid')
flag.isused = True
db.session.add(flag)
db.session.commit()
user = User.query.filter_by(id=user_id).first()
'''增加用户余额'''
user.money = user.money+flag.price
db.session.add(user)
db.session.commit()
return jsonify({"code":10001,"msg":"exchange success"})
except Exception as e:
db.session.rollback()
raise e
使用该充值码进行高并发充值
minzhizhou这个号当前余额为0
用burp Turbo Intruder插件进行30个并发请求
可以看到,8次兑换成功,那么看一下账户余额是不是160
余额只有80,和预期的不太一样,但有一点是肯定的,一个码被兑换了多次。
看一下SQL日志,可以看到有好几个rollback. 这是因为数据库事务的隔离性导致。
隔离性有4种,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。关于数据库事务的更多内容可以参阅:再谈数据库事务隔离性_51CTO博客_数据库隔离性,这篇文章已经解释很详细了。
漏洞环境已同步到:https://api.itner.net
目前包含的漏洞场景如下:
1、 兑换充值码的高并发漏洞(一码多换)
2、 登陆暴力破解漏洞
3、CSRF添加管理员账号
4、 微信小程序泄漏session_key
5、评论区XSS