Содержание статьи
Каждый раз после того, как в новостях публикуют какую‑то мегакрутую уязвимость, можно наблюдать четыре типа людей:
- Те, кто спешить опубликовать бесплатный эксплоит, который будет неприменим без доработок.
- Те, кто публикует PoC (доказательство концепции) на GitHub, YouTube или других платформах, оставляя ссылку для покупки или контакты.
- Те, кто анализирует уязвимость, подробно описывая ее опасность, но без предоставления PoC.
- Любители покритиковать всех остальных.
Сегодня я совмещу третий и четвертый типы: расскажу тебе об уязвимости и покритикую хакеров из первых двух категорий.
Немного о ситуации
Уязвимость, которую мы рассмотрим, нашел Джон Блэкборн, и она получила идентификатор 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, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее