Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Сегодня я разберу уязвимость в LiteSpeed Cache — популярном плагине для ускорения работы сайтов. Плагин работает с движками WooCommerce, bbPress, ClassicPress и Yoast, на сегодня у него более пяти миллионов установок. Давай посмотрим, как генерация недостаточно качественных случайных чисел привела к возможности повысить привилегии до админа.
Каждый раз после того, как в новостях публикуют какую‑то мегакрутую уязвимость, можно наблюдать четыре типа людей:
Сегодня я совмещу третий и четвертый типы: расскажу тебе об уязвимости и покритикую хакеров из первых двух категорий.
Уязвимость, которую мы рассмотрим, нашел Джон Блэкборн, и она получила идентификатор CVE-2024-28000.
Компания Patchstack утверждает:
Wordfence (конкурент) заявляет:
Уязвимость, по сути, заключается в том, что где‑то в недрах плагина генерируется хеш, который используется в качестве куки, через которую можно создать аккаунт администратора. При этом в основе хеша — случайное число, сгенерированное на основе долей секунды. Число комбинаций для таких чисел ограниченно, что позволяет подобрать хеш.
У 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