포스트

GraphQL 실습(3) - Finding a hidden GraphQL endpoint (PortSwigger Academy)

숨겨진 GraphQL endpoint를 찾아 introspection 우회를 통해 Schema를 획득하고, 사용자 계정을 삭제하는 과정

GraphQL 실습(3) - Finding a hidden GraphQL endpoint (PortSwigger Academy)

Lab: Finding a hidden GraphQL endpoint

Lab Link: Finding a hidden GraphQL endpoint


핵심 포인트와 취약점 개념

이번 실습은 숨겨진 endpoint 탐색 + introspection 우회 + 권한 없는 mutation 실행까지 이어진다.

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

  • 숨겨진 GraphQL endpoint 탐색
  • Introspection 방어 우회
  • Schema 분석
  • 권한 검증 없는 mutation 실행

최종 목표는 carlos 계정을 삭제하는 것이다.


공격 흐름

1. 숨겨진 GraphQL endpoint 탐색

설명

사이트를 탐방한 이후에도 endpoint가 노출되지 않음을 확인할 수 있다. endpoint를 찾기 위해 일반적으로 사용되는 경로들을 추측하여 (예시: /graphql, /api, /api/graphql 등) 무작위로 입력 해본다.

설명

/api에서 “Query not present” 라는 응답을 내비친것으로 봐서 해당 endpoint가 특정 query를 기대하고 있다는 것을 알 수 있다.

또한, Query not present라는 메시지는 일반적인 REST API에서는 잘 나타나지 않는 반면,
GraphQL 서버는 query 파라미터가 없을 경우 이와 유사한 에러를 반환하는 경우가 많다.

따라서 /api endpoint가 GraphQL일 가능성을 의심해볼 수 있다.


2. GraphQL endpoint 검증

해당 GET 요청을 Burp Repeater로 가져와서 분석 해보자.

설명

GET /api 요청 내용

GET request를 허용하지 않는 endpoint도 존재하기 때문에
Request method를 POST로 변경해서 시도해 볼수도 있다.
POST /api요청으로 바꿔서 전송 해보자.

설명

POST /api 요청

설명

응답

Method Not Allowed를 반환 하는것을 확인할 수 있다.
따라서 POST 요청은 허용되지 않고, GET 요청만 처리되는 것으로 보인다.

이번 lab에서는 GraphQL endpoint가 GET 요청을 통해 동작하고 있다.
일반적으로 GraphQL은 POST를 사용하는 경우가 많지만,
캐싱 등의 이유로 GET을 허용하는 경우도 존재한다.

POST 요청은 허용되지 않기 때문에 다시 GET 요청으로 돌아간다.

이 endpoint가 실제 GraphQL인지 확인하기 위해 query{__typename}과 같은 universal query를 보낼 수도 있지만,

이번에는 Schema 노출 여부를 확인하기 위해 introspection query를 바로 전송해보았다.

1
2
GET /api?query=query{__schema
{queryType{name}}}
설명

요청

설명

응답

__schema를 포함한 Query를 차단한 것으로 봐서 서버에서 Introspection을 차단하고 있는 모습이다.
여기서 시도해볼 수 있는 방법은

  • URL Encoding
  • Newline 삽입
    등이 있다. 단순 문자열 필터링 기반으로 방어가 적용되어 있다면 Newline을 삽입하거나 URL Encoding을 하는것만으로도 Bypass가 가능하다.
    문법적으로는 동일한 Query지만, 필터는 우회될 수 있다.
설명

Newline 을 URL Encoding해서 추가해준 모습

설명

Schema 응답

결과적으로 Introspection이 정상적으로 수행되며 Schema를 획득할 수 있다.


3. GraphQL Schema 분석과 Mutation 실행

Introspection 우회가 가능하다는 점을 알았으니 Schema 전체를 확인하기 위해 Full-Introsepction query를 보내준다.

설명

해당 Query로 newline을 함께 넣어줘야 정상적으로 응답한다.

설명

Full-Schema 응답

Schema를 분석하다가 보면 getUser필드가 나오게 되는데, 해당 필드는 id를 입력으로 받아 User오브젝트를 반환해주는 역할을 한다.
이 구조는 Direct Object Reference 형태로, 적절한 권한 검증이 없다면 다른 사용자의 정보도 조회할 수 있다.

설명

User오브젝트에 어떤 값들이 포함되어 있는지 확인 해보면

설명

idusername만 포함되어있는것을 확인할 수 있다. 만약 해당 필드가 password도 함께 반환해줬다면 getUser만으로도 다른 사용자의 계정 접근이 가능했을 것이다.

설명

조금 더 살펴보면, deleteOrganizationUser라는 필드도 찾아볼 수 있다. 해당 mutation을 활용하면 carlos의 계정 삭제가 가능할 수도 있기 때문에 확인 해보도록 하자.

설명

deleteOrganizationUser

id를 변수로 가져오기 때문에 유저 carlosid값을 먼저 알아내고 나서 진행이 가능하다.
Full-Schema에서 찾았던 getUser를 활용해 carlosid값을 알아 볼 수 있겠다.

설명

getUser 요청

설명

getUser 응답

초기 요청에는 null이 반환되기 때문에 id값을 바꿔가면서 테스트 해본 결과,
위 요청과 같이, "id":3일 때 유저 carlos를 반환하는것을 확인할 수 있다.
따라서 deleteOrganizationuser필드의 input 변수로 "id:3"을 입력했을 때, 별도의 권한 검증이 설정되어있지 않다면 carlos 계정 삭제가 가능하다.

이제 찾은 id를 이용해서 삭제 mutation을 실행한다.

특히 해당 mutation은 관리자 기능으로 보이지만,
별도의 권한 검증 없이 누구나 호출 가능한 상태였다.


//Query
mutation($input: DeleteOrganizationUserInput) {
  deleteOrganizationUser(input: $input) {
    user {
      id
      username
    }
  }
}

//Variables
{"input":{"id":3}}
설명

DeleteOrganizationUserInput요청

설명

삭제 성공 응답

요청을 보내면 성공적으로 carlos 계정이 삭제되며 lab이 완료된다.


정리

이번 Lab에서는 다음과 같은 보안 요소들이 적용되어 있었다.

  • endpoint 숨김
  • Introspection 요청 필터링

하지만 두 가지 모두 충분한 방어가 되지 못했다.

숨겨진 endpoint는 /api와 같은 일반적인 경로 추측만으로 쉽게 발견할 수 있었고,
Introspection 필터링 또한 단순 문자열 기반으로 구현되어 있어 newline을 추가하는 것만으로 우회가 가능했다.

그 결과 Schema 전체가 노출되었고,
이를 통해 deleteOrganizationUser와 같은 민감한 mutation을 확인할 수 있었다.

가장 큰 문제는 해당 mutation에 대한 권한 검증이 존재하지 않았다는 점이다.
결국 공격자는 특정 사용자의 ID만 알아내면, 별도의 인증 없이 계정을 삭제할 수 있었다.

따라서 이번 실습에서는, 기능을 숨기거나 필터링하는 것만으로는 충분하지 않으며, 모든 GraphQL 필드와 mutation에 대해 명확한 접근 제어가 필요하다는 점을 보여준다.

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