Не удалось прогрузить медиа
Первая часть - https://forum.LIMB.cc/threads/1516/ В этой части же мы рассмотрим извлечением JS-файлов из HAR и их обработку для последующего сравнения и анализа изменений.
Извлекаем JS файлы
Начнем с извлечения файлов из HAR, но сперва просто профильтруем HAR и сложим данные в отдельные JSON файлы, просто чтобы избавиться от лишнего мусора, которые есть в HAR файлах, и уберем дубликаты
Bash:
mkdir har-processor
touch har-processor/{blacklist.ts,cli.ts,scope.ts,types.d.ts,utils.ts}
- blacklist.ts - определяется массив строк, против которых будет проверять URL, и исключать
- scope.ts - префиксы для интересующих нас URL'ов
- utils.ts - врапперы над функциями для работы с файлами и хешом
- types.d.ts - типы
Начнем с определением типов:
JavaScript:
// файл types.d.ts
interface HAR {
log: {
entries: HARLogEntry[];
};
}
// поля, которые мы извлечем из HAR
interface HARLogEntry {
request: {
url: string;
};
response: {
content: {
mimeType?: string;
text?: string;
hash?: string;
};
};
}
interface ProcessedState {
// будем вычислять md5 хеш каждого файла, чтобы убрать дубликаты
hashes: string[];
js: Record<string, HARLogEntry>;
// время сохранения нашего снашпшота
timestamp: string;
}
export { HAR, HARLogEntry, ProcessedState };
Укажем и blacklist и scope:
JavaScript:
// file blacklist.ts
export const BLACKLIST = [
'/api/context/',
'/cdn-cgi/challenge-platform/',
'icon-spritemap.js',
'/zxcvbn-',
];
// file scope.ts
export const SCOPE = [
'https://www.udemy.com',
'https://frontends.udemycdn.com',
'https://my-org.udemy.com',
];
Перед тем как рассмотреть основной код, глянем на примеры использования:
Bash:
❯ bun run har-processor/cli.ts
Не удалось прогрузить медиа
process-hars обрабатывает HAR файл, сохраняя только интересующие нас ответы с телом JS файлов в *.js-state.json, а extract-files берет js-sate.json и сохраняет на диск сам отформатированный JS код под соответствующим именем.
Bash:
❯ bun run har-processor/cli.ts process-hars --hars ./revisions/current/*.har
Не удалось прогрузить медиа
Теперь сохраняем JS файлы в revisions/current/extracted-js директорию:
Bash:
❯ bun run har-processor/cli.ts extract-files --states ./revisions/current/*.js-state.json --dir revisions/current/extracted-js
Не удалось прогрузить медиа
И сам код с комментариями:
JavaScript:
// file cli.ts
import beautify from 'js-beautify';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { SCOPE } from './scope';
import { BLACKLIST } from './blacklist';
import { HAR, HARLogEntry, ProcessedState } from './types';
import { loadJSON, makeFilePath, md5, saveJSON } from './utils';
// фильтрация по типу, blacklist и scope
function extractJS(entries: HARLogEntry[]): HARLogEntry[] {
return entries.filter(({ request: { url }, response: { content: { mimeType } = {} } = {} }) => {
// иногда javascript код приходит в ответе от вызова API, поэтому добавлем mime
const isJS = mimeType?.includes('javascript') || /\.js$/.test(url);
return isJS && SCOPE.some(domain => url.startsWith(domain)) && BLACKLIST.every(ignore => !url.includes(ignore));
});
}
async function processHAR(harPath: string): Promise<ProcessedState> {
const har: HAR = await loadJSON(harPath);
const entries = extractJS(har.log.entries);
const hashes: string[] = [];
const jsMap: Record<string, HARLogEntry> = {};
// извлекаем нужные нам поля из HAR и убираем дубликаты
for (const entry of entries) {
const content = entry.response.content?.text;
if (!content) continue;
const hash = md5(content);
entry.response.content.hash = hash;
hashes.push(hash);
jsMap[hash] = entry;
}
const state: ProcessedState = {
hashes: [...new Set(hashes)],
js: jsMap,
timestamp: new Date().toString()
};
// сохраняем очищенный HAR
await saveJSON(harPath.replace('.har', '.js-state.json'), state);
const urls = Object.values(state.js)
.map(entry => entry.request.url)
.sort()
.join('\n');
// и так же отдельно сохраним список URL в отдельном файле для удобства
await Bun.write(harPath.replace('.har', '.js-state.txt'), urls);
return state;
}
async function extractJSFiles(state: ProcessedState, hashes: string[], dir: string) {
console.log(`Total unique js files: ${hashes.length}`);
let i = 0;
for (const hash of hashes) {
const entry = state.js[hash];
const content = entry.response.content?.text;
if (!content) continue;
let url = entry.request.url;
// форматируем код перед сохранением в файл
const beautified = beautify(content, { indent_size: 2, indent_char: ' ' });
const outputPath = makeFilePath(url, dir);
await Bun.write(outputPath, beautified);
console.log(`${++i}/${hashes.length} saved to ${outputPath}`);
}
}
yargs(hideBin(Bun.argv))
.command('process-hars', 'Extract js-state.json and js-state.txt from HAR files',
y => y.option('hars', { type: 'array' }),
async argv => {
for (const har of argv.hars as string[]) {
console.log(`Processing ${har}`);
await processHAR(har);
}
})
.command('extract-files', 'Extract JS files from JS states',
y => y.option('states', { type: 'array' })
.positional('dir', { type: 'string', default: 'extracted-js', optional: true }),
async argv => {
for (const path of argv.states as string[]) {
console.log(`// Processing ${path}`);
const state = await loadJSON(path) as ProcessedState;
const dir = argv.dir;
const hashes = Object.keys(state.js);
await extractJSFiles(state, hashes, dir);
}
})
.demandCommand()
.strict()
.parse();
Последнее - утилиты:
JavaScript:
// file utils.ts
import crypto from 'crypto';
export const md5 = (text: string): string =>
crypto.createHash('md5').update(text).digest('hex');
export const loadJSON = async (filePath: string): Promise<any> => {
const file = Bun.file(filePath);
return await file.json();
};
export const saveJSON = async (path: string, data: any) => {
await Bun.write(path, JSON.stringify(data, null, 2));
};
export const writeFile = async (path: string, content: string) => {
await Bun.write(path, content);
};
export function makeFilePath(url: string, dir: string): string {
url = url.endsWith('/') ? url.slice(0, -1) : url;
const fileName = url.split('/').pop();
// на случай одинаковых имен файлов
//return `${dir}/${hash}_${fileName}`;
return `${dir}/${fileName}`;
}
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Извлечение source maps
Файлы извлекли, теперь перед началом анализа осталось только вытащить source maps, тут нам как раз и пригодятся .js-state.txt файлы:
Bash:
❯ mkdir sourcemaps
❯ touch sourcemaps/extract-from-urls.sh
❯ chmod +x ./sourcemaps/extract-from-urls.sh
Файл extract-from-urls.sh
Bash:
#!/bin/bash
dir=snapshot
revision="${1:-current}"
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo "Usage: $0 [current|previous]"
echo "Current by default"
exit 0
fi
mkdir -p $dir
echo > $dir/js_urls.txt
echo > $dir/non_sourcemaps_urls.txt
echo > $dir/sourcemaps_urls.txt
# собираем все ссылки в одном файле
cat revisions/$revision/*.js-state.txt >> $dir/js_urls.txt
# frontends.udemycdn.com нас интересует только этот поддомен т.к. уже точно известно, что там включены source maps
cat $dir/js_urls.txt | sort -u | grep -vP '^\s*$' | grep -v 'buildManifest|ssgManifest' | grep 'https://frontends.udemycdn.com' > $dir/js_urls.txt.tmp
mv $dir/js_urls.txt.tmp $dir/js_urls.txt
echo "Found $(wc -l $dir/js_urls.txt) unique URLs"
while IFS= read -r line; do
echo "Processing $line"
(bunx js-source-extractor "$line" -o "$dir/src" >/dev/null 2>&1 && echo "$line" >> $dir/sourcemaps_urls.txt) || echo "No sourcemaps found" && echo "$line" >> "$dir/non_sourcemaps_urls.txt"
done < "$dir/js_urls.txt"
В данном случае JS файлы лежат на отдельном CDN и без проблем скачиваются программно, а так же сами соурсмапы лежат в отдельном файле по адресу $originalJsUrl.map В ином случае, мы могли бы вместе с JS файлами скачивать так же .map на этапе краулинга, а так же модифицировать скрипт, чтобы он работал не ссылкой, а с файлами - изменения минимальны, просто использовать протокол file://
Bash:
❯ ./sourcemaps/extract-from-urls.sh
Не удалось прогрузить медиа
Теперь посмотрим на восстановленную структуру проекта:
Bash:
❯ tree snapshot/src/src/ | less
Не удалось прогрузить медиа
На самом деле, файл с соурсмапами гораздо больше, но для этого нужно прописать более сложные сценарии краулинга. Udemy использует архитектуру микрофронтендов, где есть основное приложение на react (мы вытаскивали все JS оттуда с помощью сценария manifest) и несколько микрофронтов на next.js Часть от него мы можем увидеть выше. Сценарии вроде процесса покупки и просмотра курса, а так же создание собственной организации здесь опущены, чтобы не переусложнять изложение, но большая часть sourcemaps была бы восстановлена оттуда.
Так же отмечу что код мы восстановили в папку snapshot:
Bash:
❯ ls snapshot
js_urls.txt non_sourcemaps_urls.txt sourcemaps_urls.txt src
Напомню архитектуру нашего тулкита. Почти каждая команда имеет параметр revison: current или previous, то есть в какую папку сохранять файлы. Они не будут храниться в гите и используются для сравнения минифицированных JS файлов, о чем будет рассказано в следующей части. А все находки и извлеченные из соурсмапов + восстановленный код мы храним в папке snapshot, а версионируем и смотрим изменения через git.
Анализ JS файлов
Начнем с извлечения всех ссылок
Bash:
❯ mkdir -p extractors/URIs
❯ touch extractors/URIs/{urls,path}.sh
❯ chmod +x ./extractors/URIs/*.sh
File extractors/URIs/urls.sh
Bash:
#!/bin/bash
if [[ $# -eq 0 ]]; then
echo "Usage: $0 OUTPUT [JS-FILES...]"
exit 0
fi
output=$1
files=("${@:2}")
echo > "$output"
for file in "${files[@]}"; do
grep -rPo '\w+://.+' $file -h | grep -vP 'node_modules|svg|doctype|</html>' | perl -pe 's/^\s+//' | sort -u >> "$output"
done
cat "$output" | sort -u > "$output.tmp"
mv "$output.tmp" "$output"
Bash:
❯ ./extractors/URIs/urls.sh snapshot/uri_urls.txt revisions/current/extracted-js/* $(find snapshot/src -type f | grep -P '(ts)$')
Также могут пригодиться ссылки на внутренние сервисы, вот здесь например мы нашли ссылки на k8s сервисы внутри кластера. Если нашли SSRF, для демонстрации импакта такие домены могут очень пригодиться. Еще и что-то с MCP связанное, возможно стоит внимания.
Bash:
cat snapshot/uri_urls.txt
Не удалось прогрузить медиа
Теперь пути.
Файл extractors/URIs/path.sh
Bash:
#!/bin/bash
if [[ $# -eq 0 ]]; then
echo "Usage: $0 OUTPUT [JS-FILES...]"
exit 0
fi
output=$1
files=("${@:2}")
echo > "$output"
for file in "${files[@]}"; do
grep -rP "[\`'"'"](/\w+|\w+/)' $file -h | \
grep -Pv 'Africa/Abidjan|PARSER_MEDIA_TYPE|ace/\w|Core/|node_modules|uZG9tYnl0ZXMAYjY0X3BvcyA8PSBiNjRfbGVu|/staticx/udemy/|svg|video/mp4|text/csv|MM/DD|\d\.\d\.\d|application/|audio/' | \
perl -pe 's/const \w+ = //' | \
perl -pe 's/^\s+//' | \
perl -pe 's/[,;]$//' | \
perl -pe 's/ +$//' | \
perl -pe 's/.+?(get|post|patch|delete)/$1/' | \
grep -v '<!doctype html>' | \
sort -u >> "$output"
done
cat "$output" | sort -u > "$output.tmp"
mv "$output.tmp" "$output"
cat $output | perl -pe 's/ /\n/g' | sort -u | grep -P '[/"`'']' > "$output.cleaned"
запускаем
Bash:
❯ ./extractors/URIs/path.sh snapshot/uri_paths.txt revisions/current/extracted-js/* $(find snapshot/src -type f | grep -P '(ts)$')
Пути сохраняются в двух файлах: с контекстом snapshot/uri_paths.txt и без snapshot/uri_paths.txt.cleaned Измнениям будет смотреть в файле без контекста, потом находить в файле уже с контекстом, чтобы можно было найти уже строку в исходном жс файле. Нас в первую очередь интересуют новые эндпоинты и изменения в уже существующих. Так же видно, что летит много мусора, можно его дополнительно отфильтровать, расширив реглярку для grep выше, но по большей части это будет просто трата времени. Дополнительно, если почистить пути, можно собрать wordlist для последующего фаззинга.
Не удалось прогрузить медиа
Извлечение graphql query, фрагментов и мутаций
Здесь может быть два подход, в зависимости от того, доступны ли нам типы всей graphql схемы, полученные из соурсмапов. Если да, можем просто сравнивать их, а так же с помощью ts-morph обойти весь код на подобие того, как мы делали через babel для поиска ссылок внутри манифеста. В данном случае для простоты изложения рассмотрим только вариант извлечения из прямых api вызовов, без анализа типов.
Bash:
❯ mkdir -p extractors/graphql; touch extractors/graphql/queries.sh; chmod +x extractors/graphql/queries.sh
Bash:
#!/bin/bash
echo 'Extracting queries/mutations/fragments from JS files...'
echo > snapshot/queries.gql
for file in "$@"; do
grep -rP '^.+\\n.+(query|mutation|fragment) ' $file -h | \
perl -pe 's/^.+?(query|mutation|fragment)/$1/' | \
perl -pe 's/(`;| ["`],)//' | \
perl -pe "s/ ',//" | \
perl -pe 's/ +$/ /' | \
grep -v 'querySelector' | \
sort -u >> snapshot/queries.gql
done
cat snapshot/queries.gql | perl -pe 's/^\s*$//' | perl -pe 's/ +$//' | sort -u > snapshot/queries.gql.tmp
mv snapshot/queries.gql.tmp snapshot/queries.gql
echo 'Done.'
Bash:
❯ ./extractors/graphql/queries.sh ./revisions/current/extracted-js/*
Extracting queries/mutations/fragments from JS files...
Done.
Файл snapshot/queries.gql
Не удалось прогрузить медиа
Здесь так же как и с путями - ищем новые эндпоинты и изменения в существующих. Но если интроспекция отключена, дополнительно можно собрать wordlist для восстановления схемы через https://github.com/nikitastupin/clairvoyance но это отдельная тема и заслуживает отдельной статьи.
Dependency confusion
Так же проверим используемые npm пакеты на уязвимость dependency confusion. Коротко о том как это работает: npm при установке пакета ищет в первую очередь в публичном registry, если не настроено обратное. Это довольно частая мисконфигурация. Таким образом, если у Udemy есть приватный пакет во внутренним registry по пути @udemy/some-package, то мы можем пойти зарегистрировать на npmjs.com организацию и пакет с таким же именем и получить RCE в пайплайне сборки.
Bash:
# собираем зависимости
grep -rPizho 'dependencies[a-z\-_0-9]*["\047]:\{\K(.*?)(?=\})' revisions/current/extracted-js | tr "," "\n" | awk -F":" '{print $1}' | tr -d '"' > deps.tmp
grep -Pr 'node_modules/[\-@\w_]+/' revisions/current/extracted-js -oh | perl -pe 's/node_modules|\///g' >> deps.tmp
ls -1 ./snapshot/src/node_modules/ >> deps.tmp
cat deps.tmp | sort -u > snapshot/deps.txt
rm deps.tmp
Bash:
❯ mkdir js-tools; touch js-tools/check-deps.sh; chmod +x js-tools/check-deps.sh
Bash:
#!/bin/bash
total=$(wc -l < "$1")
count=0
while IFS= read -r dep; do
((count++))
printf "\r%d/%d" "$count" "$total" >&2
if echo $dep | grep -qP "^@"; then
curl -s -o /dev/null -w "%{http_code}" "https://www.npmjs.com/org/${dep:1}" | grep -q 404 && echo $dep
else
curl "https://registry.npmjs.org/$dep" 2>/dev/null | grep -q '{"error":"Not found"}' && echo $dep
fi
done < $1
Bash:
❯ ./js-tools/check-deps.sh snapshot/deps.txt
196/196
Что еще можно интеренсого найти в JS файлах? например комментарии имеющие слово TODO, если получилось восстановить source maps. Это может указывать на какую-то недоделанную фичу, а значит и потенциальные баги.
Не удалось прогрузить медиа
Так же стоит изучить feature флаги и A/B тесты - часто отключенный функционал отключен только на фронте, а еще тестируемый функционал содержит баги.
Не удалось прогрузить медиа
Заключение
Какие еще есть варианты анализа JS кода? Так как я предпочитаю DAST и баги из разряда BAC, BL и прочие серверные уязвимости, то другие средства автоматизации я не добавлял. Но вы можете рассмотреть интеграцию например semgrep - инструмент для статического анализа, подсвечивающий участки кода, набиолее подверженные уявзимостям, или же grep ручками на стандартные sink/gadgets для XSS -postMessage, innerHTML, unsafeDangerousHTML .А в следующей, финальной части, мы рассмотримпостроение пайплайна из всех созданных нами модулей, добавим сравнение изменений в минифицированных JS файлах, и увидим финальный проект на github с подробными примерами использования.
Stay tuned~
👍6
😮1
LIMB