CTFd账户接管漏洞(CVE-2020-7245)


[toc]

0x00 漏洞介绍

在 CTFd v2.0.0 - v2.2.2 的注册过程中,如果知道用户名并在 CTFd 实例上启用电子邮件,则攻击者可以修改任意帐户的密码。

0x02 漏洞分析

这个漏洞主要出现在注册的逻辑中。

CTFd v2.0.0版本注册部分的代码:

  • CTFd/CTFd/auth.py#159
  • CTFd/CTFd/auth.py#207
159 def register():
160     errors = get_errors()
161     if request.method == "POST":
162         name = request.form["name"]
163         email_address = request.form["email"]
164         password = request.form["password"]
165
166         name_len = len(name) == 0
167         names = Users.query.add_columns("name", "id").filter_by(name=name).first()
168         emails = (
169             Users.query.add_columns("email", "id")
170             .filter_by(email=email_address)
171             .first()
172             )
173         pass_short = len(password) == 0
174         pass_long = len(password) > 128
175         valid_email = validators.validate_email(request.form["email"])
176         team_name_email_check = validators.validate_email(name)

    # 省略部分代码......

207     else:
208         with app.app_context():
209             user = Users(
210                 name=name.strip(),
211                 email=email_address.lower(),
212                 password=password.strip(),
213             )

如上面代码,可以看到,当我们通过POST发来注册请求的时候,其中的name参数并没有经过处理,而是直接被再次当作参数传入。然后,当name通过了各种检查之后,即将存入时,使用了strip方法对name进行了处理(处理去掉首尾的空字符)。

因此我们只要注册一个首位加空格的用户名即可绕过用户名不能重复的限制。这就意味着,如果我们在用户名的首尾添加上空格的时候,可以绕过用户名重复的检查,而存入数据库之前空格会被删去,那么就会导致注册的用户名会和数据库中已有的重复。

我们再来看一下找回密码的逻辑:

  • CTFd/CTFd/auth.py#95
95 @auth.route("/reset_password", methods=["POST", "GET"])
96 @auth.route("/reset_password/<data>", methods=["POST", "GET"])
97 @ratelimit(method="POST", limit=10, interval=60)
98 def reset_password(data=None):
99      if data is not None:
100         try:
101             name = unserialize(data, max_age=1800)
102         except (BadTimeSignature, SignatureExpired):
103             return render_template(
104 "reset_password.html", errors=["Your link has expired"]
105             )
106         except (BadSignature, TypeError, base64.binascii.Error):
107             return render_template(
108 "reset_password.html", errors=["Your reset token is invalid"]
109             )
110         
111            if request.method == "GET":
112             return render_template("reset_password.html", mode="set")
113         if request.method == "POST":
114             user = Users.query.filter_by(name=name).first_or_404()
115             user.password = request.form["password"].strip()
116             db.session.commit()
117             log(
118                 "logins",
119                 format="[{date}] {ip} - successful password reset for {name}",
120                 name=name,
121             )
122             db.session.close()
123             return redirect(url_for("auth.login"))

这里发现,找回密码时从链接参数中取data值,将其反序列化后可获得用户名,即可更改任意用户的密码。我们再看一下这个链接是怎么得到的,也就是data是怎么生成的

  • CTFd/utils/email/init.py#L19
def forgot_password(email, team_name):
    token = serialize(team_name)
    text = """Did you initiate a password reset? Click the following link to reset your password:
{0}/{1}
""".format(url_for('auth.reset_password', _external=True), token)
    sendmail(email, text)

def verify_email_address(addr):
    token = serialize(addr)
    text = """Please click the following link to confirm your email address for {ctf_name}: {url}/{token}""".format(
    ctf_name=get_config('ctf_name'),
    url=url_for('auth.confirm', _external=True),
    token=token
    )
    sendmail(addr, text)

可以看到这个token是直接将team_name做一次序列化处理后拼接到url中发送到用户的邮箱。查看 serialize方法的实现:

  • CTFd/utils/security/signing.py#L10
def serialize(data, secret=None):
    if secret is None:
        secret = current_app.config["SECRET_KEY"]
    s = URLSafeTimedSerializer(secret)
    return s.dumps(data)

正是利用flask自身的类似客户端cookie的序列化方式做了一个加密。

结合上面的注册以及找回密码的流程,我们的攻击方式便很明显了:

利用该漏洞需要以下几步:

  • 利用首尾添加空格绕过限制,注册一个与受害者用户名相同的账号
  • 然后,退出登录,点击找回密码,并填上自己注册用的的那个邮箱,获取重置密码链接
  • 将自己注册的账号用户名修改成其他(与受害者不同)
  • 点击重置密码链接,即可重置受害者的密码

0x03 漏洞复现

自己搭建环境

在github上下载对应版本的CTFd,文件夹中有dockerfile,可以选择手动搭建,或者在docker中搭建。这里选择使用docker-compose来搭建,比较方便。

输入

docker-compose up

等待安装完成,之后访问ip:8000端口即可。

CTF实战题目

这里用buuctf上的[V&N2020 公开赛]HappyCTFd:

我们可以点击注册一个账号,当我们注册admin账号时报错说admin账户已经存在了:

所以我们的思路就是想办法登录admin用户,得到flag。利用上面的漏洞,我们进行操作。

(1)利用首尾添加空格绕过限制,注册一个与管理员用户名admin相同的账号:

这里要注意,由于buuctf是内网环境,其靶机无法访问外网,所以需要在buuoj的邮件系统注册一个邮箱。

http://mail.buuoj.cn/admin/ui/user/signup/mail.buuoj.cn

QQ截图20201015162014

然后填写注册:

注册成功后可以发现自己的用户名前后的空格没了:

并且此时数据库中有两个admin用户了:

(2)然后,有别的浏览器另外开一个页面,点击忘记密码,并填上自己注册用的的那个邮箱,获取重置密码链接。

注意:不要退出登录。

在收件箱中即可收到重置密码的链接:

image-20201015172537766

先不要操作,需要去用户后台页面修改用户名(任意)。

(3)登录并进入用户后台页面,将自己注册的账号用户名修改成其他(与受害者不同即可):

(4)修改完自己的用户名直接点击邮箱里的重置密码链接,对admin账号进行密码重置,设置一个你想要的密码:

即可成功修改管理员用户admin的密码。再次用admin和你设置的密码登录,如下图成功登录admin:

flag在Admin Panel里的challenges里面找一下:

发现有个”flagflag你在哪”的挑战:

打开后找到一个”miaoflag.txt”的附件,即可flag:


Author: WHOAMI
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source WHOAMI !
评论
  TOC