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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。