banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

如何防止並發漏洞,以Flask/Django為例

等我完善下,先開個坑

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 重定向

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。