This repository has been archived by the owner. It is now read-only.
Switch branches/tags
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
README.md
vanyaklimenko_hackergrom_afr.py
vanyaklimenko_hackergrom_effectiveness.py

README.md

Hackergrom

Автор: @vanyaklimenko

Невероятно безопасная социальная сеть для хакеров и всех тех, кому важна безопасность.

Сервис написан на Экспрессе — это такой серверный фреймворк для JS. О прелестях и особенностях этого, несомненно, лидирующего языка я расскажу как-нибудь потом. А сейчас — о структуре сервиса.

Процесс таков:

  1. Зарегистрироваться с логином, почтой и паролем: /signin
  2. Войти с почтой и паролем: /signup
  3. Попать в ленту, в которой by design видно лишь собственные посты. У настоящих хакеров друзей не бывает.
  4. Опубликовать фоточку-другую с остроумным сопроводительным текстом: /new
  5. Надеяться на лучшее ¯\_(ツ)_/¯

Уязвимость 1: эффективный хешинг и роутинг

Описание

В подземельных НИИ одного подмосковного города был разработан сверхэффективный алгоритм хеширования паролей. Во вполне себе не подземельном НИИ одного югорского города, во время разработки «Хакергрома», совершалась попытка его реализовать, но что-то пошло не так: достаточно взглянуть на функцию проверки паролей:

// check very carefully
const checkPassword = (req, hashed) => {
  const matched = security.compareSync(req.password, hashed.password);
  if (hashed.admin) {
    fasterHashing = false; // admin password should be hashed propelry
  }
  return (fasterHashing || matched);
}

Кажется, что всё нормально и функция работает. Однако стоит лишь узнать, что такое fasterHashing, как все вопросы моментально отпадут: let fasterHashing = true;

Из курса логики за восьмой класс следует, что если хотя бы одна переменная в выражении под return истинна, то таково и всё выражение. Говоря простым языком, пароли всех пользователей, кроме админов, не проверялись совсем никак.

Теперь осталось найти какой-нибудь логин. Это очень просто. Дело в том, что эффективность — девиз «Хакергрома» — любая строчка кода подвергалась адовой нещадящей оптимизации. Например, поскольку ручки /signin, /signup и /signfail содержат подстроку /sign, обрабатываются они одним и тем же методом:

app.get('/sign*', (req, res, next) => {
  const users = db.get('users').value();
  res.render('sign', {
    state: req.session.state,
    type: req.path.slice(1),
    users: users
  });
});

Надо лишь понять, что произойдёт, если пользователь сходит по несуществующей ручке типа /signasdfa. Сходим в код рендерера /views/sign.pug:

else
    pre= JSON.stringify(users)

Да, дамп базы. Очень жаль. Воспользуемся полученными знаниями и слепим работающий эксплоит.

Как закрыть

Ошибку с «быстрым хешированием» исправить довольно легко. Снова воспользуемся знаниями из восьмого класса:

// before
let fasterHashing = true;
// after
let fasterHashing = false;

Ошибка про рендеринг тоже чинится довольно тривиально. Например, можно удалить последний блок else в рендерере или оставить и передать через него послание соперникам:

else
    pre= JSON.stringify({name: 'sorry', password: 'my bad'})

Уязвимость 2: arbitrary file reading

Описание

Внимательный читатель заметит, что фотографии для ленты получаются из запросов к /media/{id поста}. Это выглядит безопасно: о расположении фотографии на сервере никаких сведений не передаётся, да и наверняка по дороге осуществляется какая-нибудь валидация по базе... Убедимся, что это правда лишь отчасти:

app.get('/media/*', (req, res, next) => {
  if (req.session.state) {
    const id = req.path.slice(7);
    const post = db.get('posts')
      .find({ id: id })
      .value()

    return res.sendFile(path.resolve(path.resolve('pics/' + id) + (post ? post.ext : '')));
  }
  return res.redirect('/');
});

Намёк на валидацию действительно есть, но это лишь несудьбоносный намёк. Сервер в любом случае пойдет отдавать то, что мы попросим. О чём вы мечтали всю жизнь? Я, например,— о файле db.json. И о дыре в хешировании паролей. Но она уже есть, поэтому ограничимся базой. Её содержимое поможет:

  • во 1) получить все флаги на текущий момент разом
  • вовторых получить имейлы всех пользователей и, совместив это знание со знанием о дырявом хешировании, так же получить все флаги
  • в тетьих 3) это повод задуматься о безопасности мира, в котором мы живём.

Эксплоит.

Как закрыть

Тут способов много. Можно избавиться от конструкции path.resolve, которая принимает любой путь и возвращает абсолютный, и воспользоваться служебной константой __dirname:

return res.sendFile(__dirname + '/pics/' + id) + (post ? post.ext : '')));

Метод res.sendFile никогда не отдаёт файлы по запросам с относительными путями.

А можно было просто запретить отдавать файл, если его нет в базе:

if (post) {
 return res.sendFile(path.resolve(path.resolve('pics/' + id) + (post ? post.ext : '')));
}

Бонус. Уязвимость 2+i

Время скоротечно. Люди легкомысленны. Не всё, что задумывается, успевается в срок. В сервисе есть ещё хотя бы две ошибки, потенциально приводящие к катастрофическим последствиям и флагам. Они не эксплуатировались в рамках этого соревнования — не вписывались в концепцию, а код и структура данных не позволяла этому произойти физически. Преданным фанатам ЦТФ-движения предлагается попробовать понять, о чём это я, с помощью невнятных намёков:

  1. Общие секреты печенья
  2. Хеширование высокой слабости
  3. Столетние сугробы лазаретов-питекантропов

Пишите, если вас озарит: t.me/vanyaklimenko.