CVE-2020-7245 CTFd 任意账户接管漏洞 分析

影响版本:CTFd v2.0.0 - v2.2.2

0x01 漏洞简介

影响版本:CTFd v2.0.0 - v2.2.2

漏洞危害:接管任意帐户 (已知用户名且开启了电子邮件)

官方补丁:https://github.com/CTFd/CTFd/pull/1218/files

0x02 漏洞分析

使用 CTFd v2.2.0 版本进行分析,直接本地以 debug 模式运行,在后台开启邮件验证并设置好 smtp 服务器配置信息用于后面发送重置邮件

1. 漏洞复现

注册一个用户名为 targetUser 的用户,将该用户作为目标用户

image-20200301203639978

新注册一个用户,用户名为 target 且用户名头部或者尾部添加空格

image-20200301210021213

通过忘记密码来生成重置链接

image-20200301221537528

访问链接即可重置目标用户的密码接管 target

2. 漏洞分析

当我们再注册新用户的时候 CTFd 通过 /CTFd/auth.py 中的 register 函数进行处理

image-20200301204717551

用户名,也就是代码中的 name 的值是直接从表单中获取的,然后用该 name 值查询数据库中是否有同名用户,如果 names 返回值不为空,则会得到一个用户名已存在的错误

在往下跟进

image-20200301205200682

在其他如用户名长度、邮箱、密码等检查项没有问题之后,将该注册用户插入到数据库中,这时的 name 经过了 strip() 函数的处理, strip() 也是常用来移除字符串头尾指定字符的函数,默认是去除首尾的空格或换行符,也就是说这里入库前后的 name 是不同的

那么在用户名头部或者尾部添加若干空格即可绕过有关用户名唯一性的检测,使用该方法新注册一个账号,结果如下

image-20200301210021213

那么只需要重置该用户密码即可接管 targetUser 用户,跟进看下 reset_password 函数,/CTFd/auth.py ,98 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def reset_password(data=None):
if data is not None:
try:
name = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired):
return render_template(
"reset_password.html", errors=["Your link has expired"]
)
except (BadSignature, TypeError, base64.binascii.Error):
return render_template(
"reset_password.html", errors=["Your reset token is invalid"]
)

if request.method == "GET":
return render_template("reset_password.html", mode="set")
if request.method == "POST":
user = Users.query.filter_by(name=name).first_or_404()
user.password = request.form["password"].strip()
db.session.commit()
log(
"logins",
format="[{date}] {ip} - successful password reset for {name}",
name=name,
)
db.session.close()
return redirect(url_for("auth.login"))

if request.method == "POST":
email_address = request.form["email"].strip()
team = Users.query.filter_by(email=email_address).first()

get_errors()

if config.can_send_mail() is False:
return render_template(
"reset_password.html",
errors=["Email could not be sent due to server misconfiguration"],
)

if not team:
return render_template(
"reset_password.html",
errors=[
"If that account exists you will receive an email, please check your inbox"
],
)

email.forgot_password(email_address, team.name)

return render_template(
"reset_password.html",
errors=[
"If that account exists you will receive an email, please check your inbox"
],
)
return render_template("reset_password.html")

可以输入自己的 email ,接着通过 email.forgot_password(email_address, team.name)发送找回密码的邮件

image-20200301215024033

跟进 forgot_password 函数,/CTFd/utils/email/__init__.py,19 行

1
2
3
4
5
6
7
8
9
10
11
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
)

return sendmail(email, text)

此处的 team_name 是要接管的用户名,查看序列化生成 token 的方法, /CTFd/utils/security/signing.py

1
2
3
4
5
6
7
8
9
10
11
12
def serialize(data, secret=None):
if secret is None:
secret = current_app.config["SECRET_KEY"]
s = URLSafeTimedSerializer(secret)
return s.dumps(data)


def unserialize(data, secret=None, max_age=432000):
if secret is None:
secret = current_app.config["SECRET_KEY"]
s = URLSafeTimedSerializer(secret)
return s.loads(data, max_age=max_age)

使用了 URLSafeTimedSerializer 生成 token

到这里,利用流程就很明确了

得到重置密码链接

image-20200301221537528

访问链接,填写新密码,再重新登陆即可接管目标用户

中间在访问重置链接之前的修改用户名的步骤不做也是可以的,因为在 reset_password 函数中反序列化重置链接的 data 得到用户名后,查询方法是 Users.query.filter_by(name=name).first_or_404() ,一般来说目标用户的 id 总是靠前的

Reference:

https://www.colabug.com/2020/0204/6940556/amp/

https://github.com/CTFd/CTFd/pull/1218/files

文章作者: J0k3r
文章链接: http://j0k3r.top/2020/03/01/ctfd-CVE-2020-7245/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog