等我完善下,先开个坑
race condition 漏洞,一般也叫条件竞争漏洞,最容易出现在抽奖、订单、积分、提现等业务场景中,毕竟我们写代码很多时候是单线程,所以导致该漏洞出现得很隐蔽,但是一旦出现,危害就特别大,因此我们从实际编码的场景上看看,怎么才能防御 race condition 漏洞
为了简化开发过程,集中演示漏洞,这里就用 python 的两个 web 框架为例,flask 和 django,为啥要选这俩呢,因为这俩都是 python 最流行的 web 框架,同时这两者在 ORM 上的实现也有所不同,Flask 使用 SQLAlchemy,而 Django 使用自带的 Django ORM。
先来看 flask
这里我们来看一个用户提现的业务场景,首先设计两个 model
# 用户
class User(db.Model, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), unique=True, nullable=False)
email = db.Column(db.String(256), unique=True, nullable=False)
password = db.Column(db.String(128), nullable=False)
money = db.Column(db.Integer, default=0)
# 提现
class WithdrawLog(db.Model):
__tablename__ = 'withdraw_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
amount = db.Column(db.Integer, nullable=False)
created_time = db.Column(db.DateTime, default=datetime.utcnow)
看代码很容易理解的,User 存储了用户对应的账户信息,和余额,WithdrawLog 则关联了用户表作为外键,用户每提现一次就会有一条记录
由于 flask 没有自带的后台管理,于是我们使用 flask-admin
来搞个简易的后台,顺便给后台加了一个验证用户登录
# 自定义 AdminIndexView,添加身份验证
class MyAdminIndexView(AdminIndexView):
@expose('/')
@login_required
def index(self):
return super(MyAdminIndexView, self).index()
# 自定义 ModelView,添加身份验证
class MyModelView(ModelView):
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for('login', next=request.url))
def on_model_change(self, form, model, is_created):
if is_created or form.password.data != '':
model.set_password(form.password.data)
super(MyModelView, self).on_model_change(form, model, is_created)
admin = Admin(app, name='My App Admin', template_mode='bootstrap4', index_view=MyAdminIndexView())
admin.add_view(MyModelView(User, db.session))
admin.add_view(MyModelView(WithdrawLog, db.session))
这样就可以后台管理用户了,先给用户分配 10 money
无锁无事务的 race condition#
@app.route('/withdraw1', methods=['GET','POST'])
@login_required
def withdraw1():
if request.method == 'POST':
amount = int(request.form['amount'])
if current_user.money >= amount:
current_user.money -= amount
db.session.add(WithdrawLog(user_id=current_user.id, amount=amount))
db.session.commit()
flash('Withdrawal successful')
return redirect(url_for('index'))
else:
flash('Insufficient funds')
return render_template('withdraw.html')
此时没有任何的防御,如果有多个请求到达服务,在上一个请求实际扣款前,下一个请求此时的 money 还是原来的,这就导致了重复扣款
使用 yakit 来进行 fuzz
yakit 是一个强大的渗透工具,支持国产!
使用 {{repeat(100)}}
来重复发送 100 个数据包,线程数设置为 200,可以看到出现了 4 条 302 重定向