Автор: Калан
Сервис предлагает регистрироваться и совместно участвовать в долевом строительстве юрт. Внутри — ничего особенного, типичное CRUD-приложение на питоне с некоторой бизнес-логикой.
Некоторая информация — а именно, описание строящихся юрт и описание пользователей — считается конфиденциальной, проверяющая система записывает туда флаги.
Конфиденциальные данные содержатся в описании пользователя, причём описания чужих пользователей недоступны для просмотра — можно смотреть только своё.
Проверяется это так:
async def user_page(request):
user = get_user(request)
...
"description": description if user_id in user else None,
...
Проверка на in
настораживает: неужели переменная user
может содержать несколько пользователей? И действительно: функция get_user
, достающая залогиненного пользователя из куки, возвращает множество, которое, по-видимому, может содержать несколько пользователей, если в куке они перечислены через запятую:
def get_user(request):
...
return {int(i) for i in d.split(",")}
...
Подпись (хеш от строки с прибавлением случайной константы) обмануть не получится. Однако следует посмотреть повнимательнее, какие именно данные попадают в куку:
async def login(request):
user = get_user(request)
...
if user_id:
user.add(user_id[0][0])
...
resp.set_cookie("urtlogin", get_user_str(user))
...
И как эта кука генерируется:
def get_user_str(user):
return str(sum(user)) + " " + hashlib.sha1((str(sum(user)) + RB).encode()).hexdigest()
Здесь user
— это множество чисел, и sum(user)
просто-напросто суммирует идентификаторы как числа, а не перечисляет их через запятую, как можно было бы подумать. Таким образом, если мы можем войти сначала под учётной записью с идентификатором, например, 2, а потом, не выходя, войти под учётной записью 3, то в куке окажется строка не 2,3
, а 5
— и если 5-й пользователь существует, будут видны его конфиденциальные данные: описание и список юрт.
Уязвимость не даст доступа к информации пользователей, зарегистрированных раньше, чем те, которые зарегистрированы нами, но это несущественно: проверяющая система каждый раз регистрирует нового пользователя. Так что можно заранее заготовить несколько учётных записей и суммировать их в любой нужный номер. Множественная авторизация позволяет использовать каждое слагаемое несколько раз, однако слагаемое не должно быть равно текущей сумме (если мы, имея в куке 4
, попробуем зайти от пользователя 4, операция add
не добавит в множество вторую четвёрку, и в куке по-прежнему останется значение 4
).
Самый простой способ гарантировать себе доступ ко всем будущим учётным записям — зарегистрировать все номера от какого-нибудь
Эксплоит, перебирающий пользователей
- Выдавать создаваемым пользователям идентификаторы, которые невозможно перебрать — например, длинные случайные числа.
- Альтернативный вариант: сделать корректной или вовсе отключить множественную авторизацию.
Уязвимость касается описания строящейся юрты. Любой авторизованный пользователь может просматривать любую юрту — однако списка юрт нет, так что для просмотра юрты надо узнать её идентификатор.
Можно получить идентификатор случайной юрты с помощью ссылки «случайная юрта», однако в течение каждой минуты ссылка ведёт на одну и ту же юрту, и только в начале следующей минуты можно получить другую.
Идентификаторы создаваемым юртам присваиваются такие:
async def join_urt(request):
...
if urt_id == 0:
...
t = int(time.time())
...
Таким образом, юрта получит идентификатор, равный округлённому до целого числа текущему Unix-времени (порядка 1606200000). Эти идентификаторы можно без труда перебрать, особенно зная текущее время — перебор всех юрт за последние несколько минут будет совсем лёгким.
Ещё из такого способа создания идентификаторов следует, что во всей системе можно создавать только одну юрту в секунду. Тем самым можно, в начале каждой секунды создавая в чужом сервисе юрту, предотвратить создание юрты проверяющей системой (и, соответственно, начисление очков команде за работающий сервис).
Альтернативный способ получить список юрт — выкачивать информацию о пользователях (эксплуатируя уязвимость I).
- Юртам тоже выдавать идентификаторы, не допускающие перебора.
- Функциональность «случайная юрта» направить на какую-нибудь юрту, чьё описание не содержит флага (действительно случайную или конкретную), либо отключить вовсе.
- Не показывать список юрт на странице пользователя.