等我完善下,先開個坑
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 重定向