Dreamhack | login-1
Dreamhack login-1
본 문제는 Dreamhack을 통해서 풀어 보실 수 있습니다.
해답을 이해하며 생각을 해보면서 풀이 해보시길 바랍니다.
문제 내용
이번엔 로그인 서비스에 대해서 admin 권한의 계정으로 로그인하라는 것으로 어떠한 취약점을 기반으로 할 지에 대해서는 무지한 상태입니다.
문제 풀이
로그인 Form과 등록, 비밀번호 찾기가 가능한 서비스로 되어 있다. 보자마자 exploit할 방법을 생각해보니
-
Brute Force
-
SQL injection
-
비밀번호 찾기 우회
-
중복 계정 등록
이 네 가지 방법이 떠올랐지만 Brute Force 방법은 시간이 오래 걸리므로 마지막에 시도하기로 하고 아래에서 부터 시작하려고 한다.
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
return render_template('register.html')
else:
userid = request.form.get("userid")
password = request.form.get("password")
name = request.form.get("name")
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone()
if user:
return "<script>alert('Already Exists userid.');history.back(-1);</script>";
backupCode = makeBackupcode()
sql = "INSERT INTO user(id, pw, name, level, backupCode) VALUES (?, ?, ?, ?, ?)"
cur.execute(sql, (userid, hashlib.sha256(password.encode()).hexdigest(), name, 0, backupCode))
conn.commit()
return render_template("index.html", msg=f"<b>Register Success.</b><br/>Your BackupCode : {backupCode}")
중복 계정 등록을 위해 register 관련 함수를 보니 기존에 존재하는 아이디가 있는지 검사하는 로직이 있었다. 혹시나 해서 admin으로 계정을 만드니 성공적으로 되었다.
결국 중복 계정 등록을 통해 admin 계정 접근은 안된 것으로 판정했지만 BackupCode가 있는 것으로 보아 비밀번호 찾기를 이용해 타인의 계정을 변경하는 방법을 이용할 수 있을 것 같았다.
<img src ="https://user-images.githubusercontent.com/78135526/236114905-4a7908fd-ec95-4240-8458-4b0de2503d0b.png" width = 50%>
@app.route('/user/<int:useridx>')
def users(useridx):
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE idx = ?;', [str(useridx)]).fetchone()
if user:
return render_template('user.html', user=user)
return "<script>alert('User Not Found.');history.back(-1);</script>";
생성한 admin ID로 로그인하고 계정으로 들어가는 부분이 있어서 들어가니 아래와 같았는데
<img src ="https://user-images.githubusercontent.com/78135526/236115164-2e1b100a-b9cf-417d-97bf-0e25107bcad4.png" width = 40%>
계정 정보에 대한 내용이 출력되는 것을 알 수 있었는데 UserLevel이 1인 계정을 찾으면 될 것으로 보이는데 useridx를 변경 시 해당 계정에 대한 인증 및 인가가 없는 불충분한 인증 및 인가 취약점이 있는 것을 확인할 수 있습니다.
분명 생성한 계정의 useridx는 17이였지만 URL을 통해 1로 변경하여 접속을 시도하면
-
UserID: Apple
-
UserName: Apple
-
UserLevel: 1
해당 계정의 정보를 확인이 가능했고 UserLevel이 1로 admin인 계정임을 확인할 수 있습니다.
<img src ="https://user-images.githubusercontent.com/78135526/236115662-2fa51d5f-9f25-4e4d-adb1-7093990a6316.png" width = 50%>
비밀번호 찾기 서비스는 사용자의 현재 비밀번호가 일치한지 확인하는 본인 인증을 수행하지 않기에 BackupCode를 이용하여 admin UserLevel를 탈취할 수 있습니다.
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if user:
# security for brute force Attack.
time.sleep(1)
if user['resetCount'] == MAXRESETCOUNT:
return "<script>alert('reset Count Exceed.');history.back(-1);</script>"
if user['backupCode'] == backupCode:
newbackupCode = makeBackupcode()
updateSQL = "UPDATE user set pw = ?, backupCode = ?, resetCount = 0 where idx = ?"
cur.execute(updateSQL, (hashlib.sha256(newpassword.encode()).hexdigest(), newbackupCode, str(user['idx'])))
msg = f"<b>Password Change Success.</b><br/>New BackupCode : {newbackupCode}"
else:
updateSQL = "UPDATE user set resetCount = resetCount+1 where idx = ?"
cur.execute(updateSQL, (str(user['idx'])))
msg = f"Wrong BackupCode !<br/><b>Left Count : </b> {(MAXRESETCOUNT-1)-user['resetCount']}"
conn.commit()
return render_template("index.html", msg=msg)
Brute Force Attack을 막기 위해 최대 비밀번호 변경 횟수가 정해져 있지만 time.sleep(1) 코드가 있다.
하지만 해당 sleep()은 브라우저를 통해서 Request, Response가 있을 경우에 작동하지만 python requests, burp suite intruder를 이용하게 된다면 돌아오는 response에 상관없이 request를 보내기에 의미가 없을 것 입니다.
또한, if user['resetCount'] == MAXRESETCOUNT: MAXRESETCOUNT이 5번 초과하면 끝내는게 아니라 5번이어야만 해당 Alert이 발생한다는 것은 6번, 7번이 되어도 문제 될 게 없다는 것이다.
burp suite를 이용하여 빠르게 5번을 넘기게 된다면 가능할 것입니다.
userid=Apple&newpassword=Apple&backupCode=0
BackupCode를 이용하여 Intruder하면 변경된 패스워드로 로그인이 가능하며 이로 admin 태그 접속으로 플래그를 볼 수 있습니다.