這篇文章將為大家詳細講解有關如何進行CVE-2020-7245漏洞分析,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
該漏洞是一個CTFd的賬戶接管漏洞,在注冊和修改密碼處,存在邏輯漏洞,從而導致可以修改任意賬號密碼。
影響版本:v2.0.0-2.2.2
首先定位到用戶注冊處:/CTFd/auto.py
@auth.route("/register", methods=["POST", "GET"])
@check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5)
def register():
errors = get_errors()
if request.method == "POST":
name = request.form["name"]
email_address = request.form["email"]
password = request.form["password"]
name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)
pass_short = len(password.strip()) == 0
pass_long = len(password) > 128
valid_email = validators.validate_email(request.form["email"])
team_name_email_check = validators.validate_email(name)
if not valid_email:
errors.append("Please enter a valid email address")
if email.check_email_is_whitelisted(email_address) is False:
errors.append(
"Only email addresses under {domains} may register".format(
domains=get_config("domain_whitelist")
)
)
if names:
errors.append("That user name is already taken")
if team_name_email_check is True:
errors.append("Your user name cannot be an email address")
if emails:
errors.append("That email has already been used")
if pass_short:
errors.append("Pick a longer password")
if pass_long:
errors.append("Pick a shorter password")
if name_len:
errors.append("Pick a longer user name")
if len(errors) > 0:
return render_template(
"register.html",
errors=errors,
name=request.form["name"],
email=request.form["email"],
password=request.form["password"],
)
else:
with app.app_context():
user = Users(
name=name.strip(),
email=email_address.lower(),
password=password.strip(),
)
db.session.add(user)
db.session.commit()
db.session.flush()
login_user(user)
if config.can_send_mail() and get_config(
"verify_emails"
): # Confirming users is enabled and we can send email.
log(
"registrations",
format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
)
email.verify_email_address(user.email)
db.session.close()
return redirect(url_for("auth.confirm"))
else: # Don't care about confirming users
if (
config.can_send_mail()
): # We want to notify the user that they have registered.
email.sendmail(
request.form["email"],
"You've successfully registered for {}".format(
get_config("ctf_name")
),
)
log("registrations", "[{date}] {ip} - {name} registered with {email}")
db.session.close()
if is_teams_mode():
return redirect(url_for("teams.private"))
return redirect(url_for("challenges.listing"))
else:
return render_template("register.html", errors=errors)上述代碼,有一大半是進行輸入檢測的,提取出來關鍵部分:
def register():
errors = get_errors()
if request.method == "POST":
name = request.form["name"]
email_address = request.form["email"]
password = request.form["password"]
name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)
pass_short = len(password.strip()) == 0
pass_long = len(password) > 128
valid_email = validators.validate_email(request.form["email"])
team_name_email_check = validators.validate_email(name)
if len(errors) > 0: #檢測出錯
'''注冊賬戶密碼插入數據庫'''
else:
with app.app_context():
user = Users(
name=name.strip(),
email=email_address.lower(),
password=password.strip(),
)
db.session.add(user)
db.session.commit()
db.session.flush()
login_user(user)
if config.can_send_mail() and get_config(
"verify_emails"
): # Confirming users is enabled and we can send email.
log(
"registrations",
format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
)
email.verify_email_address(user.email)
db.session.close()
return redirect(url_for("auth.confirm"))上方的上半部分,接受用戶的輸入信息:
def register():
errors = get_errors()
if request.method == "POST":
name = request.form["name"]
email_address = request.form["email"]
password = request.form["password"]
name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)
pass_short = len(password.strip()) == 0
pass_long = len(password) > 128
valid_email = validators.validate_email(request.form["email"])
team_name_email_check = validators.validate_email(name)其關鍵在于這里:
names = Users.query.add_columns("name", "id").filter_by(name=name).first()在判斷用戶是否已經注冊時,是直接用的name,也就是用戶輸入的賬戶名,并且沒有任何的過濾。
在下半部分,注冊成功時,將賬戶、密碼、郵箱插入到數據庫中:
with app.app_context(): user = Users( name=name.strip(), email=email_address.lower(), password=password.strip(), ) db.session.add(user) db.session.commit() db.session.flush()
但是這里又對用戶輸入的賬戶進行了去除空格的操作。(也就是說,如果數據庫中存在m1sn0w這個賬戶,但是,如果我在注冊時輸入的賬戶名為:空格m1sn0w,那么,注冊時不會提示賬戶已存在,而是將m1sn0w這個用戶名插入到數據庫中,也就是數據庫中有了同名用戶)
接下來是第二個利用點(修改密碼):提取出主要代碼
@auth.route("/reset_password", methods=["POST", "GET"])
@auth.route("/reset_password/<data>", methods=["POST", "GET"])
@ratelimit(method="POST", limit=10, interval=60)
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"))我們知道,在修改密碼時,會向相應的郵箱發送一封郵件,點擊之后,才能修改密碼。(上面的data值,也就是發送給指定郵箱的URL后面的一串值)
接下來,看看data值是什么:/CTFd/utils/email/__init__.py
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)可以看到,它是將用戶名序列化之后,拼接到相應URL后面,發送給郵箱。(通過前面的分析,我們知道數據庫中的賬號有兩個是同名,那么進行修改密碼操作時,就會修改第一個用戶的密碼)
(有些文章說需要修改當前用戶為其他的用戶名,但感覺好像不需要)
if request.method == "POST": user = Users.query.filter_by(name=name).first_or_404() user.password = request.form["password"].strip() db.session.commit()
它這里取出來的用戶就是第一個用戶(也就是先前注冊的那個用戶)
所以,大致的利用方法如下:
1、注冊一個賬號,和想要修改的那個用戶名同名,但在注冊時加上空格
2、點擊修改密碼,在郵箱確認,即可修改指定用戶密碼
關于如何進行CVE-2020-7245漏洞分析就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。