Найдите RCE в обёртке nslookup на Flask
Эта вьюха Flask запускает nslookup для домена из формы, используя shell. Определите уязвимость и как атакующий доходит до выполнения команд, затем опишите исправление.
Ограничения:
domainберётся прямо изrequest.formи контролируется атакующим- команда запускается с
shell=True
domain = request.form["domain"]
output = subprocess.check_output(f"nslookup {domain}", shell=True)
Определите причину.
OS command injection — domain подставляется в shell-строку при shell=True, и спецсимволы shell выполняют произвольные команды (RCE). Фикс: shell=False со списком аргументов (ввод — один аргумент) и проверка имени хоста.
- ✗Считать проблемой DoS или SSRF вместо инъекции команд
- ✗Думать, что blacklist опасных символов надёжнее списка аргументов и shell=False
- ✗Полагать, что shell=True обязателен для запуска внешней команды
- →Почему shell=False со списком аргументов устраняет инъекцию у корня?
- →Почему чёрный список спецсимволов — ненадёжная защита здесь?
Уязвимость
domain контролируется атакующим и подставляется в строку команды, запускаемой через shell:
output = subprocess.check_output(f"nslookup {domain}", shell=True)
При shell=True интерпретатор разбирает спецсимволы (|, ;, &&, $()). Любой такой символ во вводе разрывает границу «команда/данные»: часть строки перестаёт быть именем домена и трактуется как отдельная команда — это OS command injection (RCE). Признак в коде: строка команды собирается конкатенацией и уходит в shell.
Исправление
Не используйте shell; передайте аргументы списком, тогда ввод станет одним аргументом:
import re, subprocess
domain = request.form["domain"]
if not re.fullmatch(r"[a-zA-Z0-9.-]+", domain):
abort(400)
output = subprocess.check_output(["nslookup", domain], shell=False)
✅ shell=False + список аргументов устраняет инъекцию у корня: спецсимволы больше не интерпретируются shell. Дополнительно валидируем domain по строгому шаблону. ⚠️ Чёрный список опасных символов ненадёжен — всегда находится обход.