Authentication Vulnerabilities (PortSwigger Academy) - 비밀번호 기반 로그인 (Part 2)
Brute-force 방어 메커니즘의 설계 결함과 Account Locking이 어떻게 공격에 악용될 수 있는지 살펴본다
브루트포스 방어 로직의 결함 (Flawed brute-force protection)
이전 글에서는 Password 기반 로그인에서 발생할 수 있는 Brute-force 공격과 Username Enumeration을 살펴보았다.
이번 글에서는 많은 웹사이트들이 이러한 공격을 막기 위해 구현하는 Brute-force 방어 메커니즘을 살펴보고, 그 방어가 어떤 방식으로 우회될 수 있는지를 이야기해보려고 한다.
많은 개발자들이 로그인 시도를 제한하면 brute-force 공격을 막을 수 있다고 생각한다. 실제로 어느 정도 효과는 있다. 하지만 방어 로직이 잘못 설계되어 있으면, 오히려 공격자가 그 로직을 이용해 방어를 우회하는 상황도 발생한다.
Brute-force 공격은 기본적으로 수많은 로그인 시도를 반복하는 공격이다.
따라서 대부분의 웹 애플리케이션은 공격을 어렵게 만들기 위해 로그인 시도를 제한하는 장치를 둔다.
대표적으로 많이 사용되는 방식은 다음 두 가지다.
- 일정 횟수 이상 로그인에 실패하면 계정을 잠그는 방식
- 짧은 시간 동안 너무 많은 로그인 요청이 발생하면 해당 IP를 차단하는 방식
이 두 방법 모두 어느 정도의 방어 효과는 있지만, 문제는 구현 방식에 따라 쉽게 우회될 수 있다는 점이다.
특히 로그인 실패 횟수를 관리하는 방식이 잘못 설계된 경우 공격자가 이를 이용해 방어를 무력화할 수 있다.
IP 기반 로그인 시도 제한
많은 웹사이트는 동일한 IP 주소에서 너무 많은 로그인 요청이 발생하면 이를 의심스러운 활동으로 간주한다.
일정 횟수 이상 로그인 실패가 발생하면 해당 IP를 일정 시간 동안 차단하는 방식이다.
겉보기에는 꽤 합리적인 방어 방법처럼 보인다. 하지만 실제 구현을 보면 논리적인 허점이 존재하는 경우가 종종 있다.
예를 들어 어떤 시스템에서는 다음과 같은 방식으로 로그인 시도를 관리한다.
- 로그인 실패 시 실패 횟수 증가
- 로그인 성공 시 실패 횟수 초기화
이 방식은 정상 사용자에게는 편리하다. 실수로 몇 번 틀리더라도 한 번 로그인에 성공하면 다시 초기 상태로 돌아가기 때문이다.
하지만 공격자 입장에서는 이 로직이 방어를 우회하는 방법이 될 수 있다.
공격자가 방어를 우회하는 방식
공격자는 먼저 피해자의 계정을 대상으로 brute-force 공격을 시도한다.
몇 번의 로그인 실패가 발생하면 실패 횟수가 누적된다.
그런데 차단 기준에 도달하기 직전에 자신의 계정으로 정상 로그인을 수행한다.
이때 서버는 로그인 성공을 확인하고 실패 횟수 카운터를 초기화한다.
그 결과 공격자는 다시 처음부터 brute-force 공격을 이어갈 수 있게 된다.
즉 공격자는 다음과 같은 패턴을 반복하게 된다.
- 피해자 계정으로 여러 번 로그인 시도
- 차단 기준에 가까워지면 자신의 계정으로 로그인
- 실패 카운터 초기화
- 다시 피해자 계정 brute-force 진행
이 방식이 가능한 이유는 서버가 로그인 실패 횟수를 IP 단위로 관리하면서, 로그인 성공 시 이를 초기화하기 때문이다.
Wordlist를 이용한 우회 공격
이 공격은 자동화 도구를 이용하면 매우 쉽게 구현할 수 있다.
공격자는 사용하는 wordlist 안에 자신의 계정 정보를 주기적으로 삽입하면 된다.
예를 들어 다음과 같은 형태다.
1
2
3
4
5
6
7
8
carlos:password1
carlos:password2
carlos:password3
attacker:attackerpassword
carlos:password4
carlos:password5
carlos:password6
attacker:attackerpassword
이렇게 하면 공격 도구는 피해자 계정에 대해 여러 번 로그인 시도를 하다가, 중간에 공격자의 계정으로 로그인한다.
이 로그인은 성공하게 되고, 서버는 실패 횟수를 초기화한다.
결과적으로 공격자는 IP 차단에 걸리지 않고 brute-force 공격을 계속 진행할 수 있게 된다.
Account locking
또 다른 흔한 방어 방식은 Account Locking이다.
특정 계정에서 로그인 실패가 일정 횟수 이상 발생하면 해당 계정을 잠그는 방식이다.
예를 들어 다음과 같은 정책이 있을 수 있다.
- 로그인 실패 5회 발생
- 해당 계정 잠금
- 일정 시간 이후 잠금 해제
이 방식은 brute-force 공격을 어느 정도 제한할 수 있다.
하지만 흥미롭게도 이 기능 역시 다른 취약점으로 이어질 수 있다.
Account locking을 이용한 Username Enumeration
Account Locking 기능이 있는 사이트에서는 종종 에러 메시지가 서로 다르게 반환되는 경우가 있다.
예를 들어 존재하지 않는 username으로 로그인하면 다음과 같은 메시지가 표시될 수 있다.
1
Invalid username or password
하지만 실제로 존재하는 계정에 대해 로그인 실패를 반복하면, 어느 순간 다음과 같은 메시지가 나타날 수 있다.
1
Account locked
이 메시지는 단순한 에러 메시지처럼 보이지만, 공격자에게는 매우 중요한 정보가 된다.
왜냐하면 이 메시지는 해당 username이 실제로 존재하는 계정이라는 사실을 알려주기 때문이다.
공격자는 다음과 같은 방식으로 이를 활용할 수 있다.
- 여러 username 후보를 준비한다
- 각각에 대해 여러 번 로그인 시도를 한다
- 계정 잠금 메시지가 나타나는 username을 확인한다
이 과정을 통해 공격자는 유효한 username 목록을 빠르게 확보할 수 있다.
이렇게 얻은 username 목록은 이후 brute-force 공격이나 credential stuffing 공격에서 훨씬 높은 성공률을 만들어낸다.
정리
많은 웹사이트는 brute-force 공격을 방어하기 위해 다음과 같은 기능을 구현한다.
- IP 기반 로그인 시도 제한
- Account locking
하지만 이러한 기능이 잘못된 로직으로 구현될 경우, 오히려 공격자가 이를 이용해 다음과 같은 공격을 수행할 수 있다.
- brute-force protection bypass
- username enumeration
따라서 안전한 인증 시스템을 설계하기 위해서는 단순히 방어 기능을 추가하는 것만으로는 충분하지 않다.
각 방어 메커니즘이 어떤 방식으로 악용될 수 있는지까지 고려한 설계가 필요하다.
BurpSuite 예제
Lab #1: Broken brute-force protection, IP block Lab #2: Username enumeration via account lock