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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。