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

Статья Пишем хакерские расширения для Visual Studio Code

petrinh1988

X-pert
Эксперт
Регистрация
27.02.2024
Сообщения
243
Реакции
493
Автор petrinh1988
Источник
https://xss.pro

Всем привет!

Продолжим навешивать крутой функционал для популярных программ. Сегодня у нас Visual Studio Code. Наверное, один из самых популярных редакторов кода. Особенно с его возможностью создавать расширения, которых уже тысячи миллионов. Сделаем несколько своих, полезных для пентестеров и хакеров расширений.

Идея для этих расширений зрела у меня давно. Периодически, мне удается вытащить полное или почти полное веб-приложение. Через какой-нибудь LFI или другим способом. Речь о ситуациях, когда LFI представляет собой какой-нибудь file_get_contents(), т.е. фактически мы можем попытаться “вычитать” весь проект, но не более того. Соответственно, в руках может оказаться очень ценный материал, который потом можно достаточно долго исследовать. Наши расширения будут облегчать этот процесс, значительно сокращая время. Вобщем, план такой:

  1. Расширение для поиска чувствительной информации в исходниках. Речь идет о паролях, ключах, токенах, адресах внутренних серверов и апи, ну и прочих интересностях.
  2. Расширение автоматического поиска потенциальных уязвимостей.
  3. Расширение восстанавливающее вероятную структуру проекта (на мой взгляд очень мощная штука).

Писать расширение будем на Javascript, хотя большую часть кода пишут на TypeScript. Сложностей в переводе одного в другое, практически нет, но я привык сразу по простому.

Как создать расширение?​

Расширение представляет собой набор файлов, таких как package.json и исходников. Можно, конечно, идти в жестком стиле и все накидать самому, но есть и готовая тулза “Yo Code”, которая все сделает при помощи удобного мастера:

Bash:
npm install -g yo generator-code

После установки, достаточно ввести команду и получить заготовку под расширение:

Bash:
yo code

1739905719511.png


Остается пройти по нескольким шагам мастера. Для первого расширения мои настройки выглядят так:

1739905728413.png


На выходе получаем готовую структуру файлов с примером расширения, даже unit-тест добавлен. Нас интересует файл extension.js

1739905735972.png


Для теста внесем небольшие правки в файл extension.js:

JavaScript:
const vscode = require('vscode');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

    console.log('Congratulations, your extension "xss-is-fsi" is now active!');

    const disposable = vscode.commands.registerCommand('xss-is-fsi.sensinfo', async function () {
        vscode.window.showInformationMessage('Hello World from Find something interesting!');
    });

    context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
    activate,
    deactivate
}

Правки также нужно сделать и в package.json. По-умолчанию, мастер добавляет команду “Hello world”. В командах прописать тот же идентификатор команды, что есть в registerCommand()

JSON:
  "contributes": {
    "commands": [{
      "command": "xss-is-fsi.sensinfo",
      "title": "Found somthing interesting"
    }]
  },

Теперь можно нажать F5, чтобы запустить расширение в режиме Debug. Откроется новое окно VS Code, в котором будет доступно наше расширение. Жмем “Ctrl+Shift+P” и вводим начало нашей команды. Если что-то пошло не так, проверяйте package.json и extension.js на предмет совпадения команд, а так же проверьте Title команды. По-умолчанию, мастер пилит везде helloworld.

1739905754385.png


В результате мы видим, что все прекрасно работает:

1739905774999.png


Прежде чем идти дальше, давайте разберем код, который уже написали, чтобы понимать из чего состоит расширение VS Code.

JavaScript:
const vscode = require('vscode');

Первым делом, импортируем библиотеку vscode, которая предоставляет доступ к API VS Code. В примере мы видим обращение к двум классам:

  1. vscode.commands - позволяет регистрировать и выполнять команды в окне VS Code. Например, если хотите закрыть активную вкладку, можно вызывать:

    JavaScript:
    vscode.commands.executeCommand('workbench.action.closeActiveEditor');

    Команды бывают, как встроенные, так и команды расширений. В примере, мы регистрировали свою команду и она стала доступна для выполнения через vscode.commands. Любое расширение может выполнить эту команду. Ровно как и вы, зная об установленном расширении можете прописать его выполнение из собственного.

  2. vscode.window - очень широкий класс, который дает доступ к самому окну VS Code. Можно отправить стандартное диалоговое окно пользователю: предупреждение, ошибка, подтверждение и т.п. Можно открывать файлы в окне редактора. Можно управлять статусбарами и т.д. В примере, мы выводим информационное сообщение пользователю:

    JavaScript:
    vscode.window.showInformationMessage('Hello World from Find something interesting!');

Думаю, что уже на этих двух примерах понятно, что это полноценная библиотека с почти полным контролем над VS Code. Всего доступно около 50 интерфейсов!!! В том числе, интерфейсы для управления терминалами, отслеживания файловой системы (например, добавления файлов), даже класс для управления Jupyter Notebook и т.д.

Активация и деактивация​

Здесь все просто. При активации мы выполняем подготовительную работу, объявляем команды, вешаем обработчики событий и т.п. При деактивации, мы прибираем за собой, сохраняем какие-то критические данные и т.п. Соответственно, мы видим две функции:

JavaScript:
function activate(context) {
    ...
}

function deactivate() {
    ...
}

В примере, во время активации мы создаем свою команду, привязывая к ней асинхронную функцию. В завершении, пушим эту команду в подписки, чтобы VS Code понимал что нужно делать при вызове функции через Ctrl+Shift+P.

Поиск чувствительных данных​

Вернемся к обозначенным расширениям. Чтобы пройтись по всем файлам папки, нужно знать в какой папке лежат эти файлы. Для этого отлично подойдет массив workspaceFolders, который относится к объекту workspace пакета vscode. Сделаем небольшой набросок:

JavaScript:
const vscode = require('vscode');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

    console.log('Congratulations, your extension "xss-is-fsi" is now active!');

    const disposable = vscode.commands.registerCommand('xss-is-fsi.sensinfo', async function () {
        const workspaceFolders = vscode.workspace.workspaceFolders;

        if (!workspaceFolders) {
            vscode.window.showErrorMessage('No workspace is opened.');
            return;
        }
     
        console.log(workspaceFolders);      
    });

    context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
    activate,
    deactivate
}

В результате мы видим, что нам доступно:

1739905871002.png


Если хотите искать за рамками директории проекта, можно заменить получение директории диалогом выбора для пользователя:

JavaScript:
        const selectedFolder = await vscode.window.showOpenDialog({
            canSelectFiles: false,
            canSelectFolders: true,
            canSelectMany: false,
            openLabel: 'Please, select dir’
        });

В итоге, у нас получится точно такой же массив, состоящий из одного элемента, в котором будет точно такой же объект, как и в случае с рабочей папкой.

Имея путь к папке, можно обойти её рекурсивной функцией и просканировать каждый файл на предмет наличия искомых данных.

JavaScript:
function checkFiles(dir) {
            const files = fs.readdirSync(dir);

            for (const file of files) {
                const filePath = path.join(dir, file);
                const info = fs.statSync(filePath);

                if (info.isDirectory()) {
                    searchFiles(filePath);
                } else if (info.isFile()) {
                    const data = fs.readFileSync(filePath, 'utf8');
                    ...
                }
            }
        }

Получив данные, мы можем производить поиск. Наиболее удобным, вижу использование регулярных выражений. Я пользуюсь ими, но назвать себя прям мастером RegEx не назову, поэтом позволил себе слегка потырить в интернет. В итоге, получился вот такой объект:
JavaScript:
const patterns = {
            "Cloudinary"  : /cloudinary:\/\/.*/gi,
            "Firebase URL": /.*firebaseio\.com/gi,
            "Slack Token": /(xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})/gi,
            "RSA private key": /-----BEGIN RSA PRIVATE KEY-----/gi,
            "SSH (DSA) private key": /-----BEGIN DSA PRIVATE KEY-----/gi,
            "SSH (EC) private key": /-----BEGIN EC PRIVATE KEY-----/gi,
            "PGP private key block": /-----BEGIN PGP PRIVATE KEY BLOCK-----/gi,
            "Amazon AWS Access Key ID": /AKIA[0-9A-Z]{16}/gi,
            "Amazon MWS Auth Token": /amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
            "AWS API Key": /AKIA[0-9A-Z]{16}/gi,
            "Facebook Access Token": /EAACEdEose0cBA[0-9A-Za-z]+/gi,
            "Facebook OAuth": /[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]/gi,
            "GitHub": /[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]/gi,
            "Generic API Key": /[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]/gi,
            "Generic Secret": /[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]/gi,
            "Google API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
            "Google Cloud Platform API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
            "Google Cloud Platform OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
            "Google Drive API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
            "Google Drive OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
            "Google (GCP) Service-account": /\"type\": \"service_account\"/gi,
            "Google Gmail API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
            "Google Gmail OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
            "Google OAuth Access Token": /ya29\\.[0-9A-Za-z\\-_]+/gi,
            "Google YouTube API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
            "Google YouTube OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
            "Heroku API Key": /[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/gi,
            "MailChimp API Key": "[0-9a-f]{32}-us[0-9]{1,2}",
            "Mailgun API Key": /key-[0-9a-zA-Z]{32}/gi,
            "Password in URL": /[a-zA-Z]{3,10}:\/\/[^\/\\s:@]{3,20}:[^\/\\s:@]{3,20}@.{1,100}[\"'\\s]/gi,
            "PayPal Braintree Access Token": /access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}/gi,
            "Picatic API Key": /sk_live_[0-9a-z]{32}/gi,
            "Stripe API Key": /sk_live_[0-9a-zA-Z]{24}/gi,
            "Stripe Restricted API Key": /rk_live_[0-9a-zA-Z]{24}/gi,
            "Square Access Token": /sq0atp-[0-9A-Za-z\\-_]{22}/gi,
            "Square OAuth Secret": /sq0csp-[0-9A-Za-z\\-_]{43}/gi,
            "Twilio API Key": /SK[0-9a-fA-F]{32}/gi,
            "Twitter Access Token": /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*[1-9][0-9]+-[0-9a-zA-Z]{40}/gi,
            "Twitter OAuth": /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]/gi,      
            "API Keys": /(api[_-]?key|access[_-]?token|secret[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi,
            "Passwords": /(password|passwd|pwd)\s*[:=]\s*["']?([a-zA-Z0-9!@#$%^&*()_-]{8,})["']?/gi,
            "IP Addresses": /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
            "urls": /(https?:\/\/[^\s]+)/gi,
            "emails": /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi
        };

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

Нам остается добавить проверку регуляркой через exec() и формирование выходного массива, который позже распечатаем. Итоговый код функции поиска выглядит так:

JavaScript:
function checkFiles(dir, results) {

    const patterns = {
        "Cloudinary"  : /cloudinary:\/\/.*/gi,
        "Firebase URL": /.*firebaseio\.com/gi,
        "Slack Token": /(xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})/gi,
        "RSA private key": /-----BEGIN RSA PRIVATE KEY-----/gi,
        "SSH (DSA) private key": /-----BEGIN DSA PRIVATE KEY-----/gi,
        "SSH (EC) private key": /-----BEGIN EC PRIVATE KEY-----/gi,
        "PGP private key block": /-----BEGIN PGP PRIVATE KEY BLOCK-----/gi,
        "Amazon AWS Access Key ID": /AKIA[0-9A-Z]{16}/gi,
        "Amazon MWS Auth Token": /amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
        "AWS API Key": /AKIA[0-9A-Z]{16}/gi,
        "Facebook Access Token": /EAACEdEose0cBA[0-9A-Za-z]+/gi,
        "Facebook OAuth": /[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]/gi,
        "GitHub": /[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]/gi,
        "Generic API Key": /[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]/gi,
        "Generic Secret": /[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]/gi,
        "Google API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
        "Google Cloud Platform API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
        "Google Cloud Platform OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
        "Google Drive API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
        "Google Drive OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
        "Google (GCP) Service-account": /\"type\": \"service_account\"/gi,
        "Google Gmail API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
        "Google Gmail OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
        "Google OAuth Access Token": /ya29\\.[0-9A-Za-z\\-_]+/gi,
        "Google YouTube API Key": /AIza[0-9A-Za-z\\-_]{35}/gi,
        "Google YouTube OAuth": /[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/gi,
        "Heroku API Key": /[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/gi,
        "MailChimp API Key": /[0-9a-f]{32}-us[0-9]{1,2}/gi,
        "Mailgun API Key": /key-[0-9a-zA-Z]{32}/gi,
        "Password in URL": /[a-zA-Z]{3,10}:\/\/[^\/\\s:@]{3,20}:[^\/\\s:@]{3,20}@.{1,100}[\"'\\s]/gi,
        "PayPal Braintree Access Token": /access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}/gi,
        "Picatic API Key": /sk_live_[0-9a-z]{32}/gi,
        "Stripe API Key": /sk_live_[0-9a-zA-Z]{24}/gi,
        "Stripe Restricted API Key": /rk_live_[0-9a-zA-Z]{24}/gi,
        "Square Access Token": /sq0atp-[0-9A-Za-z\\-_]{22}/gi,
        "Square OAuth Secret": /sq0csp-[0-9A-Za-z\\-_]{43}/gi,
        "Twilio API Key": /SK[0-9a-fA-F]{32}/gi,
        "Twitter Access Token": /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*[1-9][0-9]+-[0-9a-zA-Z]{40}/gi,
        "Twitter OAuth": /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]/gi,      
        "API Keys": /(api[_-]?key|access[_-]?token|secret[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi,
        "Passwords": /(password|passwd|pwd)\s*[:=]\s*["']?([a-zA-Z0-9!@#$%^&*()_-]{8,})["']?/gi,
        "IP Addresses": /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
        "urls": /(https?:\/\/[^\s]+)/gi,
        "emails": /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi
    };

    const files = fs.readdirSync(dir);

    for (const file of files) {
        const filePath = path.join(dir, file);
        const info = fs.statSync(filePath);

        if (info.isDirectory()) {
            checkFiles(filePath);
        } else if (info.isFile()) {
            const data = fs.readFileSync(filePath, 'utf8');
            const matches = [];

            for (const [type, pattern] of Object.entries(patterns)) {
                console.log(type, pattern);
                let match;
                while ((match = pattern.exec(data)) !== null) {
                    matches.push({ type, value: match[0] });
                }
            }

            if (matches.length > 0) {
                results.push({ file: filePath, matches });
            }
        }
    }
}

Завершает композицию функция печати. Для вывода, у VS Code есть специализированный интерфейс “OutputChannel”. Перед его использованием, создадим его экземпляр и выведем, используям метод show():

JavaScript:
function printOutput(results) {
    const output = vscode.window.createOutputChannel('Sensitive Data Search');
    output.show();

}

Сам вывод можно выполнить при помощи метода appendLine(), который полностью соответствует названию и выводит строку с переносом каретки.

JavaScript:
        output.appendLine('Any text here');

Завершающий штрих, обернуть все в два цикла. Первый обходит элементы массива, распечатывая имя файла в котором найдены совпадения. Второй проходит по конкретным совпадениям, выводя тип совпадения и совпадающую строку:

JavaScript:
function printOutput(results) {
    const output = vscode.window.createOutputChannel('Sensitive Data Search');
    output.show();

    for (const result of results) {
        output.appendLine(`File: ${result.file}`);
        for (const match of result.matches) {
            output.appendLine(`  [${match.type}] ${match.value}`);
        }
        output.appendLine('');
        output.appendLine('');
    }
}

В итоге работы расширения, мы получаем вот такую красоту:

1739905891533.png


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

JavaScript:
const vscode = require('vscode');
const searchSensData = require('./sensdata');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

    console.log('Congratulations, your extension "xss-is-fsi" is now active!');

    const disposable = vscode.commands.registerCommand('xss-is-fsi.sensinfo', searchSensData);

    context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
    activate,
    deactivate
}

Все остальное я просто вынес в файл sensdata.js.

Поиск потенциальных угроз​

Нам вовсе не обязательно добавлять совершенно новое расширение, достаточно добавить еще одну команду VS Code. Причем, нетрудно догадаться, что код будет очень и очень похожим. Если точнее, отличаться он будет только набором регулярных выражений. Можно сколько угодно проверок сделать, создав разные наборы данных.

Чтобы не затягивать, остановимся на поиске запуска системных команд в PHP-коде. Если память мне не изменяет, то нас интересует всего три команды:

  1. system()
  2. exec()
  3. shell_exec()
  4. passthru()

Включаю свои крутые мега-навыки составления регулярок:

JavaScript:
const pattern = /(system|exec|shell_exec|passthru)\(.*?\);/gi;

Следующим действием, упрощаю добавление найденных элементов. Вместо этого цикла:

JavaScript:
            for (const [type, pattern] of Object.entries(patterns)) {
                console.log(type, pattern);
                let match;
                while ((match = pattern.exec(data)) !== null) {
                    matches.push({ type, value: match[0] });
                }
            }

Оставляю простое:

JavaScript:
            let match;
            while ((match = pattern.exec(data)) !== null) {
                matches.push({ type: "System Execution", value: match[0] });
            }

Можно добавить фильтрацию по расширению файла, но в целом можно оставить и без нее. Тестовый запуск прекрасно нашел мне два вхождения в файле:

1739905907967.png


Возможно у вас появится желание объединить две команды в одну. Добавить вариант последовательно автоматического запуска. Для этого нам почти ничего не нужно. Можно, конечно, выстроить цепочку вызовов классическим способом принятым в JS, но предлагаю попробовать возможность запуска скриптов средствами VS Code.

JavaScript:
    const disposable3 = vscode.commands.registerCommand('xss-is-fsi.run-all', () => {
        vscode.commands.executeCommand('xss-is-fsi.sensinfo');
        vscode.commands.executeCommand('xss-is-fsi.php-code-exec');
    });

Не забываем добавить команду в package.json:

JSON:
    "commands": [{
      "command": "xss-is-fsi.sensinfo",
      "title": "Found something interesting"
    },
    {
      "command": "xss-is-fsi.php-code-exec",
      "title": "PHP System Execute Command"
    },
    {
      "command": "xss-is-fsi.run-all",
      "title": "Found all interesting"
    }]

Единственный минус совмещения в том, что каждая команда создаст свое окно вывода. Можно это исправить, но в целом это уже мелкие детали, которые только отвлекут и сильно затянут статью.

1739905921789.png


Добавляем универсальности​

Мне очень не нравится тот факт, что регулярные выражения жестко прописаны в расширении. Почему бы не предоставить пользователю возможность настраивать этот параметр? Указал в настройках путь к файлу и забыл. Нужен нестандартный скан, указал другой файл.

Для этого потребуется покопаться во множестве мест, но в целом ничего экстраординарного нет. Первым делом, нужно добавить информацию о наличии настроек в package.json:

JSON:
"contributes": {
    "configuration":{
      "title": "Find Interesting Information",
      "properties": {
        "xss-is-fsi.sensetiveWordlist": {
          "type": "string",
          "default": "",
          "description": "Specify the path to the JSON file with sensitive data patterns"
        }
      }
    },
    "commands": [{
      "command": "xss-is-fsi.sensinfo",
      "title": "Found something interesting"
    },
    {
      "command": "xss-is-fsi.php-code-exec",
      "title": "PHP System Execute Command"
    }]
  },

Как видно из кода, у нас добавилось свойство “configuration”. Причем, оно указывается одно для всего расширения, а конкретные настройки уже добавляются как отдельные properties и имеют три стандартных свойства: тип, значение по-умолчанию и описание. К сожалению, отсутствует тип “файл”, из самых близких это “строка”. Но в целом, мы избавим пользователя от необходимости вбивать путь, дав возможность выбрать файл если строка пустая.

Для справки, вариант с двумя настройками выглядел бы так:

JSON:
"contributes": {
    "configuration":{
      "title": "Find Interesting Information",
      "properties": {
        "xss-is-fsi.sensetiveWordlist": {
          "type": "string",
          "default": "",
          "description": "Specify the path to the JSON file with sensitive data patterns"
        },
        "xss-is-fsi.phpCodeExecWordlist": {
          "type": "string",
          "default": "",
          "description": "Specify the path to the JSON file with PHP threat patterns"
        }
      }
    },
    "commands": [{
      "command": "xss-is-fsi.sensinfo",
      "title": "Found something interesting"
    },
    {
      "command": "xss-is-fsi.php-code-exec",
      "title": "PHP System Execute Command"
    }]
  },

Подгрузка данных из конфигурации происходит в два движения:

JavaScript:
    const config = vscode.workspace.getConfiguration('xss-is-fsi');
    let configSensInfo = config.get('sensitiveWordlist');

Сначала получаем объект конфигурации, который хранит в себе все предлагаемые настройки. После, через get() вытаскиваем уже конкретное конечное свойство.

Чтобы не захламлять основной файл расширения, весь последующий функционал вынесет в дополнительный файл file.functions.js и файл с функционалом sensdata.js. Функция активации расширения примет такой вид:

JavaScript:
function activate(context) {

    console.log('Congratulations, your extension "xss-is-fsi" is now active!');

    const config = vscode.workspace.getConfiguration('xss-is-fsi');
    let configSensInfo = config.get('sensitiveWordlist');

    const disposable = vscode.commands.registerCommand('xss-is-fsi.sensinfo', runSearch.bind(null, configSensInfo, config));
    const disposable2 = vscode.commands.registerCommand('xss-is-fsi.php-code-exec', searchPHPCodeExecution);

    context.subscriptions.push(disposable);
    context.subscriptions.push(disposable2);
}

Кроме подгрузки конфигурации, поменялся вызов функции поиска данных. Во-первых, заменил её на промежуточную, но об этом ниже. Соответственно, не забываем обновить импорты функций. Во-вторых, прикрутил к ней через bind два аргумента.

JavaScript:
const defaultPatterns = {
    "Cloudinary"  : /cloudinary:\/\/.*/gi,
    "Firebase URL": /.*firebaseio\.com/gi
    ...
}

async function runSearch(filePath, config) {

    console.log('Run search', filePath);

    if (!filePath) {
        filePath = await askUserToSelectWordlist();
        console.log('File selected', filePath);

        if (!filePath)
            return searchSensData();
         
        config.update('sensitiveWordlist', filePath, vscode.ConfigurationTarget.Global);
        console.log('Updated settings');
    }

    patternsSensInfo = loadDataFromFile(filePath);
 
    searchSensData(patternsSensInfo);
}

Не забываем, что строка конфигурации может быть пустой. Если файл не выбран, то спрашиваем у пользователя нужно ли выбрать файл или проигнорировать и использовать зашитый в код объект с паттернами (объект .defaultPatterns). При согласии пользователя на выбор файла, дожидаемся ответа, повторно проверяем появился ли путь (пользователь может нажать отмену) и вызываем функцию поиска чувствительной информации searchSensData(). Соответственно, нам нужно поправить саму функцию searchSensData(), чтобы была возможность использовать заранее объявленный объект:

JavaScript:
let workPatterns;

function searchSensData(patterns = defaultPatterns) {
    workPatterns = patterns;

Соответственно, в цикле поиска надо patterns заменить на workPatterns

JavaScript:
            for (const [type, pattern] of Object.entries(workPatterns)) {
                console.log(type, pattern);
                let match;
                while ((match = pattern.exec(data)) !== null) {
                    matches.push({ type, value: match[0] });
                }
            }

Функция вызова диалога:

JavaScript:
async function askUserToSelectWordlist() {
    console.log('Ask to select file');
    const answer = await vscode.window.showInformationMessage(
        'You must select the search pattern file to proceed. Otherwise, the default pattern set will be used. Would you like to select a file now?',
        { modal: true },
        'Yes',
        'No'
    );

    if (answer == 'Yes') {
        let filePath = await selectFile();
        return filePath;
    }

    return false;
}

1739905943557.png


Переходим к функционалу связанному с файлами file.functions.js. Функция выбора файла:

JavaScript:
async function selectFile() {
    const fileUri = await vscode.window.showOpenDialog({
        canSelectFiles: true,
        canSelectFolders: false,
        canSelectMany: false,
        filters: { 'JSON Files': ['json'] }
    });

    if (fileUri && fileUri[0]) {
        const filePath = fileUri[0].fsPath;      
        return filePath;
    }
 
    return false;
}

Учитывая, что у нас на выходе должен быть объект с паттернами RegEx, обойтись простым словарем не получится. Поэтому потребуется написать функцию десериализации, которая обойдет полученный JSON и преобразует в наш формат. На входе предполагается вот такая структура:

JSON:
{
  "Cloudinary": {
    "pattern": "cloudinary:\\/\\/.*",
    "flags": "gi"
  },
  "Firebase URL": {
    "pattern": ".*firebaseio\\.com",
    "flags": "gi"
  }
}

Напомню, что далее это должно превратиться в такое:

JavaScript:
const patterns = {
        "Cloudinary"  : /cloudinary:\/\/.*/gi,
        "Firebase URL": /.*firebaseio\.com/gi,
};

Функция преобразования получилась такая:

JavaScript:
function deserializePatterns(json) {
    const data = JSON.parse(json);
    const patterns = {};
    for (const [key, { pattern, flags }] of Object.entries(data)) {
        patterns[key] = new RegExp(pattern, flags);
    }
    return patterns;
}

Чтобы два раза не вставать, добавил попутно функцию сохранения и сериализации. Понятное дело, что в рамках работы расширения, сохранять и сериализовать ничего не нужно. Но! Например, перегнать уже существующие объекты было бы неплохо.

JavaScript:
function serializePatterns(patterns) {
    const serialized = {};
    for (const [key, regex] of Object.entries(patterns)) {
        serialized[key] = {
            pattern: regex.source,
            flags: regex.flags
        };
    }
    return JSON.stringify(serialized, null, 2);
}

Чтобы добраться к настройкам, жмем Ctrl+, или Cmd+, и вбиваем в поиск “Find Interesting Information”:

1739905959750.png


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

Восстановление структуры проекта​

Когда у тебя в руках оказывается LFI без явной возможности развить атаку, бывает полезно восстановить структуру проекта. Поискать файлы конфигурации, файлы с логинами, паролями и другими данными, возможно намеками на использование какого-то ПО и т.п. Можно пытаться фазить, но это слишком явно и маломальская синяя команда быстро увидит вашу активность. Кроме того, разработчик может такое название файла завернуть, что хрен угадаешь. Разумнее обойти структуру файлов и папок по “следам” в виде include, require, file_get_contents, путей прописанных в переменных и т.п. История обычная, но достаточно нудная и рутинная. На мой взгляд гораздо интереснее, когда можно запустить команду и увидеть недостающие файлы. Тем более, все необходимое у нас есть, нужен только адекватный алгоритм.

Понятное дело, что история достаточно утопичная и не факт, что на 100% закрою её, но постараюсь максимально приблизиться.

Для теста собрал небольшую структуру проекта, абсолютно выдуманную. Причем, часть файлов удалю, оставив их только в импортах и прочих местах, намекающих на существование файла. Тестовый проект приложу отдельным архивом.

1739905974814.png


Перечеркнутые файлы алгоритм должен будет “увидеть” и сообщить нам об их существовании. Например, о существовании трех логов мы узнаем из команд в правой части скрина.

Попытался более-менее понятно изобразить, откуда о существовании каких файлов и папок должен узнать алгоритм. Намерянно, слегка запутал путь поиска. Получилось что-то вроде такого:

1739905993806.png


Алгоритм​

Нам потребуется несколько этапов:

  1. Сначала нужно собрать информацию по существующим файлам, т.е. тем файлам которые уже выкачали.
  2. Вторым действом нужно проанализировать каждый доступный файл и выделить потенциальные названия файлов. Причем, обработать нужно не только функции взаимодействия с другими файлами, но и переменные, константы.
  3. Третьим этапом нужно произвести замены констант и переменных конкретными частями путей. Чтобы вместо $template.”/file.php” было “/templates/file.php”. Так же отработать относительные пути и стандартные возможности языка программирования в отношении фалов и папок (DIR, FILE, dirbase, etc.).
  4. На данном этапе все почти готово, остается только обеспечить красивый вывод.
Я попытался отобразить тот самый граф, который нам нужно построить... сложно сказать, получилось ли))) Смысл в том, чтобы отстроить взаимосвязи, после эти взаимосвязи привести к виду нескольких уровней папок.

1739907413551.png


Звучит муторно, но решаемо.

Пишем расширение​

По инструкции выше создаем расширение, я назвал его “restore-structure”. После создаем команду “restore-structure.run”:

JSON:
"contributes": {
    "commands": [{
      "command": "restore-structure.run",
      "title": "Restore Project Structure"
    }]
  },

Как и ранее, сам функционал предлагаю вынести за пределы основного запускающего файла:

JavaScript:
const vscode = require('vscode');
const startRestore = require('./restore');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
    const disposable = vscode.commands.registerCommand('restore-structure.run', startRestore);
    context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
    activate,
    deactivate
}

Пойдем поэтапно. Первым делом напишем сканер существующих файлов, который будет задавать основное направление для расширения:

JavaScript:
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');

function scanFolder(dir, map = {}) {
    const items = fs.readdirSync(dir);
    items.forEach((item) => {
        const itemPath = path.join(dir, item);
        const stat = fs.statSync(itemPath);
        if (stat.isDirectory()) {
            map[itemPath] = { type: 'directory', exists: true, children: [] };
            scanFolder(itemPath, map);
        } else if (item.endsWith('.php')) {
            if (!map[dir]) {
                map[dir] = { type: 'directory', exists: true, children: [] };
            }
            map[dir].children.push({ path: itemPath, type: 'file', exists: true });
        }
    });
    return map;
}

function startRestore() {
    const workspaceFolders = vscode.workspace.workspaceFolders;


    if (!workspaceFolders) {
        vscode.window.showErrorMessage('No workspace is opened.');
        return;
    }

    const rootDir = workspaceFolders[0].uri._fsPath;
    const fileMap = scanFolder(rootDir);
    console.log('fileMap', fileMap);
}

module.exports = startRestore;

Это рекурсивная функция, которая строит многоступенчатый объект. Ключом выступает конкретная папка. Соответственно, если это корневая папка, она будет равняться папке проекта открытого в VS Code, т.к. мы её передаем как стартовую точку скана. У каждого объекта два обязательных и одно не обязательное свойство:

  1. type - директория или файл (о)
  2. exists - существует ли она на диске или это объект на который есть ссылки (о)
  3. children - соответственно, если что-то есть в папке, оно попадет сюда как такой же объект (не о)
1739906011204.png


В данном варианте, работа происходит исключительно с php-файлами. Это компромиссное решение для статьи. Можно использовать isFile(), можно проверять набор расширений, можно вынести в параметры расширения и т.д.

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

JavaScript:
function analyzeFile(filePath, variables, constants) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const dependencies = [];

    const includeRegex = /(include|include_once|require|require_once|file_get_contents|file_put_contents)\s*\((.*?)\);/g;
    const variableRegex = /\$(\w+)\s*=\s*['"](.*?)['"];/g;
    const constantRegex = /define\s*\(\s*['"](.*?)['"]\s*,\s*['"](.*?)['"]\s*\);/g;

    let match;
    while ((match = variableRegex.exec(content)) !== null) {
        const [_, name, value] = match;
        variables[name] = value;
    }

    while ((match = constantRegex.exec(content)) !== null) {
        const [_, name, value] = match;
        constants[name] = value;
    }

    while ((match = includeRegex.exec(content)) !== null) {
        const depPath = match[2].trim().replace(/['"]/g, '');
        const resolvedPath = resolvePath(depPath, filePath, variables, constants);
        dependencies.push({ path: resolvedPath, exists: fs.existsSync(resolvedPath) });
    }

    return dependencies;
}

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

Но сама подмена пути происходит дальше… Вы, наверное, обратили внимание, что есть некая функция resolvePath. В ней скрыт функционал, который должен разбирать строки использующие стандартные константы, типа DIR и FILE, а также функции путей. Это нужно, чтобы на выходе у нас был полноценный четкий путь:

JavaScript:
function resolvePath(depPath, currentFilePath, variables, constants) {

    if (depPath.includes('__DIR__')) {
        const dir = path.dirname(currentFilePath);
        depPath = depPath.replace('__DIR__', dir);
    }

    if (depPath.includes('__FILE__')) {
        depPath = depPath.replace('__FILE__', currentFilePath);
    }

    if (depPath.includes('dirname(')) {
        const dir = path.dirname(currentFilePath);
        depPath = depPath.replace(/dirname\(.*?\)/, dir);
    }

    if (depPath.includes('realpath(')) {
        const realPath = path.resolve(currentFilePath);
        depPath = depPath.replace(/realpath\(.*?\)/, realPath);
    }

    if (depPath.includes('basename(')) {
        const baseName = path.basename(currentFilePath);
        depPath = depPath.replace(/basename\(.*?\)/, baseName);
    }

    if (depPath.startsWith('$')) {
        const varName = depPath.split('.')[0].replace('$', '');
        if (variables[varName]) {
            depPath = depPath.replace(`$${varName}`, variables[varName]);
        }
    }

    if (depPath.startsWith('define(')) {
        const constName = depPath.split("'")[1];
        if (constants[constName]) {
            depPath = depPath.replace(`define('${constName}'`, constants[constName]);
        }
    }

    if (!path.isAbsolute(depPath)) {
        depPath = path.resolve(path.dirname(currentFilePath), depPath);
    }

    return depPath;
}

Теперь можно запустить расширение заново и увидеть готовый граф. Специально выделил тот, в котором есть зависимости:

1739906023072.png


Следующим этапом нам нужно “доготовить” структуру, приведя её к финальному варианту. Делать это будем при помощи функции restoreStructure:

JavaScript:
function restoreStructure(graph, rootDir) {
    const structure = {};
    for (const file in graph) {
        const dir = path.dirname(file);
        const cleanDir = dir.replace(rootDir, '');
        const cleanFile = file.replace(rootDir, '');
        if (!structure[cleanDir]) {
            structure[cleanDir] = { type: 'directory', exists: fs.existsSync(dir), children: [] };
        }
        structure[cleanDir].children.push({ path: cleanFile, type: 'file', exists: graph[file].exists });
        graph[file].dependencies.forEach((dep) => {
            const depDir = path.dirname(dep.path);
            const cleanDepDir = depDir.replace(rootDir, '') ;
            const cleanDepPath = dep.path.replace(rootDir, '');
            if (!structure[cleanDepDir]) {
                structure[cleanDepDir] = { type: 'directory', exists: fs.existsSync(depDir), children: [] };
            }
            structure[cleanDepDir].children.push({ path: cleanDepPath, type: 'file', exists: dep.exists });
        });
    }
    return structure;
}

В целом, мы просто снова переобходим наш объек, перестраивая структуру. Разве что дополнительно, произвожу замену, удаляя из пути путь к папке проекта. Логика здесь у меня простая, путь к локальной папке не имеет никакого отношения. Результатом работы будет такой объект с “чистыми путями”:

1739906034472.png


Готово почти все, кроме вывода результата в редактор Visual Studio Code, Давайте исправлять это недоразумение. Но перед самим выводом, есть смысл снова поработать над видом объекта. Да, мы его неоднократно уже преобразовывали и наполняли данными. И полученная структура хороша для программной обработки. Например, если мы говорим об LFI, можно пользователю дать возможность подставить запрос с макросом подмены, чтобы расширение в полностью автоматическом режиме выкачивало все недостающие файлы. Но для вывода структура не лучшая, поэтому прогоним объект через еще одну функцию:

JavaScript:
function createTree(structure) {
    const root = {
        name: 'Project Structure Tree',
        exists: true,
        children: []
    };

    for (const dir in structure) {
        const dirNode = {
            name: path.basename(dir),
            exists: structure[dir].exists,
            children: []
        };

        structure[dir].children.forEach((file) => {
            const fileNode = {
                name: path.basename(file.path),
                exists: file.exists,
                type: file.type
            };
            dirNode.children.push(fileNode);
        });

        root.children.push(dirNode);
    }

    return root;
}

Комментировать в функции нечего, все достаточно прозаично. Теперь структура выглядит так:

1739906045434.png


Займемся непосредственным выводом:

JavaScript:
function printOutput(structure) {
    const outputChannel = vscode.window.createOutputChannel('Restored Project Structure');
    outputChannel.show();

    function printTree(node, prefix = '', isLast = true) {
        const connector = isLast ? '└── ' : '├── ';
        const newPrefix = isLast ? '    ' : '│   ';
        const label = `${prefix}${connector}${node.name} ${node.exists ? '(+)' : '(-)'}`;
        outputChannel.appendLine(label);
        if (node.children) {
            node.children.forEach((child, index) => {
                printTree(child, prefix + newPrefix, index === node.children.length - 1);
            });
        }
    }

    printTree(structure);
}

Создаем и выводим окно вывода (ну да, тавтология), после чего рекурсивная функция делает красивый вывод.

Запустив на тестовом проекте я получил близкий к идеалу результат,

1739906055399.png


Как видно на скрине, в вывод попало “, $info”. Попало благодаря тому, что в функции file_put_contents стоит пробел после запятой. Не идеальный алгоритм не понял, что нужно отбросить эту часть. Есть и другие “шероховатости” алгоритма. Например, если части пути соединены точкой и между ними пробел, алгоритм не разберется и не подменит переменную. И это далеко не предел.

Конкретно с данной ситуацией можно справиться легко, сплитанув запись по запятой и взяв нулевое значение. Но, как я и писал вначале, не было цели разработать идеальные универсальные расширения. Мы здесь учимся, а доработать каждый сможет самостоятельно.

JavaScript:
const label = `${prefix}${connector}${node.name.split(',')[0]} ${node.exists ? '(+)' : '(-)'}`;

Завершить расширение предлагаю возможностью сохранить полученную структуру в файл:

JavaScript:
async function askAndSave(treeStructure) {

    const answer = await vscode.window.showInformationMessage(
        'Wanna to save structure to file?',
        { modal: true },
        'Yes',
        'No'
    );

    if (answer != 'Yes') return

    const output = JSON.stringify(treeStructure, null, 2);

    const fileUri = await vscode.window.showSaveDialog({
        title: 'Save Project Structure',
        filters: {
            'JSON Files': ['json'],
            'Text Files': ['txt']          
        }
    });

    if (fileUri) {
        try {
            fs.writeFileSync(fileUri.fsPath, output, 'utf-8');
            vscode.window.showInformationMessage(`Success save: ${fileUri.fsPath}`);
        } catch (err) {
            vscode.window.showErrorMessage(`Error: ${err.message}`);
        }
    }
}

Как и раньше, сначала вызываем диалоговое окно, где предлагаем пользователю сохранить результа. Для записи доступны два формата, json-предпочтительный. Записанный подобным образом файл будет выглядеть так:

1739906067799.png


Если хочется, как в окне редактора Visual Studio Code, можно слегка схитрить и поправить функцию вывода окно. Просто объявив массив, в который помещается значение каждый раз при выводе в окно редактора:

function printOutput(structure) {
...

const outputText = [];

function buildTree(node, prefix = '', isLast = true) {
...
outputChannel.appendLine(label);
outputText.push(label)
...
}

buildTree(structure);
return outputText;
}

Теперь массив можно спокойно записать в текстовый файл, заджоинить через “\n” и получив точную копию вывода. Этот механизм в статье приводить не буду, а в исходниках он полностью реализован. Вот пример вывода в текстовый файл:

1739906079743.png


Упаковываем расширение​

Разработка это хорошо, но хотелось бы и попользоваться? Я не буду писать на тему публикации расширения, кому интересно, без проблем разберется. Но вот как превратить расширение в устанавливаемое, расскажу. Для начала нужно внести небольшие правки в package.json:

JSON:
{
  "name": "xss-is-fsi",
  "displayName": "Find something interesting",
  "description": "Search for sensitive information: logins, passwords, tokens, keys, etc.",
  "version": "0.0.1",
  "publisher": "petrihn1981@xss.pro",
  "engines": {
    "vscode": "^1.97.0"
  }
...
}
[CODE=javascript]

Обязательные поля:

[LIST=1]
[*]name — уникальное имя расширения.
[*]version — версия расширения.
[*]publisher — имя издателя (ваше имя или псевдоним).
[*]engines.vscode — минимальная версия VS Code, с которой работает расширение.
[/LIST]

Далее надо установить пакет vsce

[CODE=bash]
npm install -g vsce

После переходим в корневую папку расширения и выполняем команду:

Bash:
vsce package

Если выскочит такая ошибка:

1739906547880.png


Это значит, что нужно поправить README.md. Vsce просто не хочет кушать тестовый шаблон. Пример моего файла лежит в архиве с проектом. В целом там ничего сверхъестественного. После внесения правок все должно прекрасно работать. Разве что могут возникнуть предупреждения по поводу того, что не указан адрес репозитория или нет лицензии… но это все же фигня.

1739906555501.png


После этого, появится файл расширения с расширением .vsix. Чтобы установить, переходим в раздел “Extensions”, кликаем вверху на три точки и выбираем “Install From VSIX”

1739906564489.png


Выбираем файлик расширения и можно свободно пользоваться:

1739906576715.png


Заключение​

Всю информацию о разработке расширений для Visual Studio Code, вряд ли получится уместить и в десяток статей. Но в целом, если вы разобрались с информацией в этой статье, сможете написать практически любое расширение. Ничего сложного в этом нет, а возможности открываются просто невероятные. У меня был небольшой опыт написания расширений, но как-то мимо проскакивала идея расширений для пентеста и т.п. Пока не надоело руками все делать…

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

Другие два расширения тоже вполне практичные. У меня скопилось огромное количество проектов, в которых может быть много интересного. Сидеть и руками вводить каждую регулярку отдельно, а потом ждать чуть ли не по часу на отработку, это крайне неприятный процесс. Совсем другое дело нажать пару кнопок и дождаться результата.
 

Вложения

  • restore-structure.zip
    44.2 КБ · Просмотры: 23
  • xssis-fsi.zip
    52.3 КБ · Просмотры: 21
Последнее редактирование модератором:
Последнее редактирование:


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