Се­год­ня я раз­беру уяз­вимость в LiteSpeed Cache — популяр­ном пла­гине для уско­рения работы сай­тов. Пла­гин работа­ет с движ­ками WooCommerce, bbPress, ClassicPress и Yoast, на сегод­ня у него более пяти мил­лионов уста­новок. Давай пос­мотрим, как генера­ция недос­таточ­но качес­твен­ных слу­чай­ных чисел при­вела к воз­можнос­ти повысить при­виле­гии до адми­на.

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

  1. Те, кто спе­шить опуб­ликовать бес­плат­ный экс­пло­ит, который будет неп­рименим без дорабо­ток.
  2. Те, кто пуб­лику­ет PoC (доказа­тель­ство кон­цепции) на GitHub, YouTube или дру­гих плат­формах, оставляя ссыл­ку для покуп­ки или кон­такты.
  3. Те, кто ана­лизи­рует уяз­вимость, под­робно опи­сывая ее опас­ность, но без пре­дос­тавле­ния PoC.
  4. Лю­бите­ли пок­ритико­вать всех осталь­ных.

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

 

Немного о ситуации

Уяз­вимость, которую мы рас­смот­рим, нашел Джон Блэк­борн, и она получи­ла иден­тифика­тор CVE-2024-28000.

Ком­пания Patchstack утвер­жда­ет:

За эту уяз­вимость была наз­начена самая высокая наг­рада в исто­рии поис­ка оши­бок в WordPress. Прог­рамма Patchstack Zero Day наг­радила иссле­дова­теля 14 400 дол­ларов США налич­ными.

Wordfence (кон­курент) заяв­ляет:

Хо­тя об этой уяз­вимос­ти не сооб­щалось в прог­рамме Wordfence Bug Bounty, за нее, ско­рее все­го, было бы при­суж­дено воз­награж­дение в раз­мере око­ло 23 400–31 200 дол­ларов США во вре­мя нашего текуще­го кон­курса Superhero Challenge, учи­тывая извес­тную нам информа­цию об уяз­вимос­ти.

 

Анализ уязвимости

Уяз­вимость, по сути, зак­люча­ется в том, что где‑то в нед­рах пла­гина генери­рует­ся хеш, который исполь­зует­ся в качес­тве куки, через которую мож­но соз­дать акка­унт адми­нис­тра­тора. При этом в осно­ве хеша — слу­чай­ное чис­ло, сге­нери­рован­ное на осно­ве долей секун­ды. Чис­ло ком­бинаций для таких чисел огра­ничен­но, что поз­воля­ет подоб­рать хеш.

 

Зачем генерируется хеш?

У LiteSpeed Cache есть фун­кция для симуля­ции поль­зовате­ля, внут­ри которой соз­дает­ся и сох­раня­ется хеш. Этот хеш исполь­зует­ся как кука litespeed_hash. Для его генера­ции нуж­но вклю­чить фун­кцию кра­уле­ра. Если ты поищешь в коде get_hash, то уви­дишь, что эта фун­кция вызыва­ется в двух мес­тах. Преж­де чем начать ана­лиз того, как генери­рует­ся хеш, давай раз­берем­ся, в каких слу­чаях эта фун­кция вызыва­ется.

Фун­кция self_curl пред­назна­чена для выпол­нения HTTP-зап­роса с исполь­зовани­ем биб­лиоте­ки cURL:

public function self_curl($url, $ua, $uid = false, $accept = false)
{
// $accept not in use yet
$this->_crawler_conf['base'] = home_url();
$this->_crawler_conf['ua'] = $ua;
if ($accept) {
$this->_crawler_conf['headers'] = array('Accept: ' . $accept);
}
if ($uid) {
$this->_crawler_conf['cookies']['litespeed_role'] = $uid;
$this->_crawler_conf['cookies']['litespeed_hash'] = Router::get_hash();
}
$options = $this->_get_curl_options();
$options[CURLOPT_HEADER] = false;
$options[CURLOPT_FOLLOWLOCATION] = true;
$ch = curl_init();
curl_setopt_array($ch, $options);
curl_setopt($ch, CURLOPT_URL, $url);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code != 200) {
self::debug('❌ Response code is not 200 in self_curl() [code] ' . var_export($code, true));
return false;
}
return $result;
}

Эта фун­кция при­нима­ет нес­коль­ко парамет­ров, таких как URL, User-Agent и иден­тифика­тор поль­зовате­ля ($uid). Она уста­нав­лива­ет базовый URL, исполь­зуя метод home_url(), и сох­раня­ет передан­ный User-Agent. Если передан иден­тифика­тор поль­зовате­ля, соз­дают­ся куки litespeed_role и litespeed_hash.

Де­ло в том, что фун­кция self_curl исполь­зует­ся в фун­кции prepare_html.

public function prepare_html($request_url, $user_agent, $uid = false)
{
$html = $this->cls('Crawler')->self_curl(add_query_arg('LSCWP_CTRL', 'before_optm', $request_url), $user_agent, $uid);

Фун­кция prepare_html исполь­зует­ся в фун­кции _send_req.

private function _send_req($request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $type, $is_mobile, $is_webp)
{
// Check if has credit to push or not
$err = false;
$allowance = $this->cls('Cloud')->allowance(Cloud::SVC_CCSS, $err);
if (!$allowance) {
Debug2::debug('[CCSS] No credit: ' . $err);
$err && Admin_Display::error(Error::msg($err));
return 'out_of_quota';
}
// Update css request status
$this->_summary['curr_request_' . $type] = time();
self::save_summary();
// Gather guest HTML to send
$html = $this->prepare_html($request_url, $user_agent, $uid);
if (!$html) {
return false;
}
.....

Фун­кция _send_req вызыва­ется из _cron_handler.

private function _cron_handler($type, $continue)
{
$this->_queue = $this->load_queue($type);
if (empty($this->_queue)) {
return;
}
$type_tag = strtoupper($type);
// For cron, need to check request interval too
if (!$continue) {
if (!empty($this->_summary['curr_request_' . $type]) && time() - $this->_summary['curr_request_' . $type] < 300 && !$this->conf(self::O_DEBUG)) {
Debug2::debug('[' . $type_tag . '] Last request not done');
return;
}
}
$i = 0;
$timeoutLimit = ini_get('max_execution_time');
$this->_endts = time() + $timeoutLimit;
foreach ($this->_queue as $k => $v) {
if (!empty($v['_status'])) {
continue;
}
if (function_exists('set_time_limit')) {
$this->_endts += 120;
set_time_limit(120);
}
if ($this->_endts - time() < 10) {
// self::debug("🚨 End loop due to timeout limit reached " . $timeoutLimit . "s");
// return;
}
Debug2::debug('[' . $type_tag . '] cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']);
if ($type == 'ccss' && empty($v['url_tag'])) {
unset($this->_queue[$k]);
$this->save_queue($type, $this->_queue);
Debug2::debug('[CCSS] wrong queue_ccss format');
continue;
}
if (!isset($v['is_webp'])) {
$v['is_webp'] = false;
}
$i++;
$res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp']);
if (!$res) {
// Status is wrong, drop this this->_queue
unset($this->_queue[$k]);
$this->save_queue($type, $this->_queue);
.....

Фун­кция _cron_handler вызыва­ется из cron_ccss.

public static function cron_ccss($continue = false)
{
$_instance = self::cls();
return $_instance->_cron_handler('ccss', $continue);
}

Фун­кция cron_ccss слу­жит обер­ткой для запус­ка фун­кции _cron_handler. Она вызыва­ет фун­кцию _cron_handler, переда­вая в качес­тве типа ccss и флаг $continue. Фун­кция _cron_handler заг­ружа­ет оче­редь задач для ука­зан­ного типа (в дан­ном слу­чае — ccss) через метод load_queue. Если оче­редь пус­тая, фун­кция завер­шает выпол­нение, так как нет задач для обра­бот­ки.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии