Привет, Хабр!
В статье я хочу рассказать о нашем опыте создания своих запросов в Checkmarx SAST.
При первом знакомстве с этим анализатором может сложиться впечатление, что кроме поиска слабых алгоритмов шифрования/хеширования и кучи false positive, он ничего больше не выдает. Но при правильной настройке, это супермощный инструмент, который умеет искать серьезные баги.
Мы разберемся в тонкостях языка запросов Checkmarx SAST и напишем 2 запроса для поиска SQL-инъекций и Insecure Direct Object References.
Вступление
После долгих поисков каких-либо гайдов или статей по Checkmarx мне стало ясно, что кроме официальной документации, полезной информации мало. Да и в официальной документации не сказать, что все становится очень ясно и понятно. Например, я не смог найти каких либо best practices, как правильно организовать override запросов, как писать query «для чайников», и т. д. Да, есть документация по функциям CMx Query Language, но вот как объединять эти функции в единый запрос, в документации не написано.
Возможно, отсутствие статей и гайдов от сообщества Checkmarx связано с высокой стоимостью инструмента и, как следствие, небольшой аудиторией. А может быть просто мало кто заморачивается тонкой настройкой и используют решение как есть, из коробки.
На моем опыте я больше вижу, что SAST используется скорее для соблюдения формальностей, связанных с различными требованиями со стороны заказчиков, чем для поиска реальных багов. При таком подходе, в результате, имеем в лучшем случае, относительно небольшое количество «уязвимостей», которые чуть ли не автоматически прокликиваются как «not exploitable» (потому что таковыми и являются в 99.9% случаев).
Надо отметить, что сами Checkmarx стараются обновлять свои queries, чтобы они выдавали наилучший результат «из коробки». Но запросы CMx Query Language заточены под «общий случай». Первичный поиск токенов основан на названии. Например, CMx SAST предполагает, что все запросы в базу будут выглядеть так: *createQuery* или *createSQLQuery*. Но если для работы c БД используются внутренние разработки, и метод для запроса в базу называется по-другому, например *driveMyQuery*, то все SQL методы будут пропущены. Например, наш заказчик использует custom ORM для SQL DB. В этом случае CMx запросы «из коробки» пропустили все SQL-инъекции.
Сокращения и определения
CMx — Checkmarx SAST.
CMxQL – Checkmarx SAST query language
Токен – строка, имеющая определенное значение, является результатом работы лексического анализатора (который также называют токенизацией)
Тестовое приложение
Для написания статьи я набросал немного Java кода, маленькое тестовое приложение. Этот код – приближенная копия маленькой части реальной системы. Хотя в целом код тестового приложения не сильно отличается от любого другого кода HTTP-бекенда. Ключевые участки кода тестового приложения будут видны на скриншотах.
Тестовое приложение имеет следующую структуру
Класс WebRouter для обработки входящих HTTP запросов, внтури 4 метода для обработки URL:
- /getTransaction – на вход принимает id транзакции и выдает инфу по ней, id принимает как строку, и передает ее в getTransactionInfo(transactionId) => getTransactionInfo(transactoinId) – делает конкат transactionId к SQL запросу (то есть получается SQL инъекция);
- /getSecureTransaction – на вход принимает id транзакции и выдает инфу по ней, id принимает как строку, и передает ее getTransactionInfoSecured() => getTransactionInfoSecured(transactoinId) – сначала приводит строку transactionId к типу Long, а затем конкатит его к SQL запросу (в этом случае инъекция не эксплуатируется);
- /getSettings — на вход принимает userId и mailboxId – и выдает настройки мейлбокса. Не проверяет что mailboxid принадлежит пользователю;
- /getSecureSettings — на вход принимает также userId и mailboxId и выдает настройки мейлбокса. НО проверяет что mailboxid принадлежит пользователю.
CMx: Общая информация и базовые определения
Перед началом разработки запросов
Разработка запросов ведется в отдельной программе CxAuditor. В CxAuditor нужно просканировать весь код (create local project), для которого мы будем писать запросы. После этого можно писать и запускать новые запросы. При большой кодовой базе первичное сканирование может занять часы времени и гигабайты памяти. После этого каждый запрос будет выполняться не достаточно быстро. Это совершенно не подходит для разработки.
Поэтому можно взять небольшой набор файлов из проекта, в идеале с заранее найденным в коде багом того типа, под который пишем запрос(или заложить баг туда руками) и просканировать только этот набор файлов. При этом не обязательно соблюдать файловую структуру проекта. То есть, если у вас есть Java package A и B, и классы в пакете B используют классы и методы пакета А, можно все это свалить в одну директорию, и CMx все равно поймет взаимосвязи и построит цепочки вызовов между файлами корректно (ну или почти всегда корректно, хотя ошибки вряд ли связаны с файловой структурой проекта).
Базовые определения
CxList
Основной тип данных в CMx. Результатом выполнения почти всех CMxQL функций будет CxList. Это множество элементов с определенными свойствами. Наиболее полезные для разработки свойства мы рассмотрим далее.
result
CMxQL имеет встроенную переменную result. Множество, которое содержит переменная result, после выполнения всего запроса будет выведено как результат.
То есть конечной операцией любого запроса должна быть строка result=WHATEVER, например:
result = All.FindByName("anyname");
flow и code element
Большинство CMxQL Функции по типу возвращаемых значений делятся на 2, те, что возвращают «code elements» и те, что возвращают Flow. В обоих случаях результатом будет CxList. Но его содержание будет немного различаться для «Flow» и «code elements».
- Сode element — токен — например переменная, вызов метода, присваивание и т. д.;
- Flow — взаимосвязь между заданными токенами.
All и «sub» All
Каждую CMxQL функцию можно выполнять или над множеством All(содержит все токены всего сканируемого кода, мы уже видели пример с result) или над множеством CxList, которое в свою очередь было получено в результате каких-то операций в запросе, например запрос:
CxList newList = CxList.New();
создаст пустое множество, которое в дальнейшем мы можем заполнить элементами с помощью метода Add(), а затем выполнить поиск уже по элементам нового множества:
CxList newFind = newList.FindByName("narrowedScope");
Свойства найденных элементов
Каждый элемент множества CxList имеет несколько свойств. При анализе результатов для написания запросов самыми полезными являются:
- SourceFile — имя файла, который содержит данный элемент;
- Source Line — номер строки с токеном;
- Source Name — имя токена. Эквивалентно токену, тоесть если переменная называется var1, то Source Name = var1;
- Source Type — тип токена. Например, если это строка, то будет StringLiteral, если вызов метода, то MethodInvokeExpr, и еще много других;
- Destination File
- Destination Line;
- Destination Name;
- Destination Type.
Source и Destination будут разные, если элементами результирующего множества являются Flow, и наоборот будут совпадать, если результатом являются code elements.
Начинаем создавать запросы
Все CMxQL функции можно разделить на несколько типов. Тут, на мой взгляд, можно отметить основной недостаток документации по CMxQL, все функции в доке описаны просто в алфавитном порядке, в то время как было бы гораздо удобнее структурировать их по функционалу и уже потом по алфавиту.
- Функции поиска — почти все CMxQL функции с названием FindBy* и GetBy*;
- Функции операций над множествами – сложение, вычитание, пересечение, итерация по элементам, и т. д.;
- Функции анализа — в основном это функции *InfluencedBy* *InfluencingOn*.
Основной принцип запросов – чередование этих типов функций. Сначала с помощью функций поиска мы выбираем только интересующие нас токены по определенным свойствам. С помощью операций над множествами мы можем объединять разные множества с разными свойствами токенов в одно, или наоборот вычитать из одного другое. Затем с помощью функций анализа мы строим Code Flow и пытаемся понять, зависят ли потенциально уязвимые места от параметров в точках входа.
Выбор места, с которого начинать искать, и вообще весь путь поиска – зависит от конкретного кода, а если быть точнее, даже от «текста». В каких-то случаях удобно искать от точки входа пользовательских запросов, в каких-то удобней начинать с «конца» или вовсе с середины. Все сильно зависит от конкретного кода и нужно индивидуально подходить к каждому репозиторию.
Пример: Поиск SQL инъекций
План поиска, в скобках я указал название множеств(переменных в запросе):
- Определить исключения – токены, которые можно сразу выбросить из скоупа поиска(exclusionList);
- Определить места санитизации/секьюрити проверок (sanitization);
- Найти все низкоуровневые места с выполнением запросов в БД (runSuperSecureSQLQuery);
- Найти все параметры вызываемых методов runSuperSecureSQLQuery (runSSSQParams);
- Найти точки входа(родительские методы и их параметры) для мест выполнения запросов в БД (entryPointsParameters );
- Найти зависимости параметров runSSSQParams от entryPoints , при этом только те места, где отсутствует сантитизация sanitization ввода.
В результате мы получим низкоуровневые методы с SQL запросами, где параметры SQL запроса:
- зависят от параметров метода;
- параметры принимаются как строки;
- параметры конкатятся к запросу.
При это не будем проверять, можем ли мы контролировать эти параметры, т.к. мы полагаем, что есть механизм маппинга переменных в запрос и есть приведение к числовому типу для чисел, а конкатенацию строк всегда считаем опасной. Даже если сейчас контроля над строкой нет, в новом релизе он вполне может появится.
SQLi: Шаг 1. Определяем Исключения
В исключения нужно добавить те классы или файлы, где названия токенов могут совпадать с искомыми, т.к. эти токены приведут к невалидным нахождениям.
Например, метод для обращений в БД называется runSuperSecureSQLquery. Мы предполагаем, что метод runSuperSecureSQLquery внутри реализован безопасно. И наша задача – найти места не безопасного использования самого метода. Для SQL инъекции не безопасными местами будут места конкатенации параметров, контролируемых пользователем. А безопасными – места маппинга параметров в структуру ORM или, например, для числовых параметров это приведение к соответствующему типу. Весь код, который лежит “глубже” runSuperSecureSQLquery, нам сканировать не нужно, а значит лучше его исключить, чтобы избежать бесполезных нахождений.
Для поиска таких исключений удобно использовать CMxQL функции:
- FindByFileName() – найдет множество всех токенов в конкретном файле;
- GetByClass()– найдет множество всех токенов в классе с заданным названием.
Для тестового приложения таким исключением является класс Session, в котором находится реализация метода runSuperSecureSQLquery.
Пример запроса для исключения кода в классе Session (метод GetByClass() проверит, какой из переданных на вход токенов имеет CMx тип ClassDecl, и выдаст множество токенов этого класса)
CxList exclusionList = All.GetByClass(All.FindByName("*Session*"));
result = exclusionList;
Или другой способ — исключение кода во всем файле Session.java:
CxList exclusionList = All.FindByFileName("*Session.java");
result = exclusionList;
Астериск перед названием важен, т. к. в название файла входит весь путь.
Теперь у нас есть множество токенов, которые можно вычесть в следующих шагах из скоупа поиска.
Результат поиска токенов внутри класса Session:
SQLi: Шаг 2. Определяем места санитизации
В тестовом приложении есть 2 API метода (см. краткое описание тестового приложения). Различие двух API методов в том, что getTransactionInfo() делает конкатенацию параметра transactionId в SQL запрос, а getTransactionInfoSecured() сначала приводит transactionId к типу Long, и уже затем передает его как строку. Уязвимость(конкатенация параметра) заложена в оба метода. Но благодаря приведению к Long в getTransactionInfoSecured(), последний метод не уязвим к инъекции, т. к. при попытке передать инъекцию(строку) мы получим Java Exception.
В данном примере мы будем считать приведение к Long местом санитизации. Чтобы найти такие токены:
CxList sanitization = All.FindByName("*Long*");
result = sanitization;
Пример результата:
В результат попали токены с ЯП типом Long и методы getValueAsLong, которые внутри приводят значение к типу Long. Результат нужно внимательно просмотреть, чтобы убедиться что не попало ничего лишнего.
SQLi: Шаг 3. Найти все низкоуровневые места с выполнением запросов в БД
Следующий запрос найдет все места с использованием токена runSuperSecureSQLQuery(который используется для обращений в БД):
result = All.FindByName("*runSuperSecureSQLQuery*")
Результат поиска по имени токена runSuperSecureSQLQuery:
Причем для мест, где этот метод вызывается (класс Billing), будут найдены только токены вызова метода (тип MethodInvokeExpr), а для места объявления метода (класс Session), будут найдены также все токены – переменные.
Отфильтруем только токены вызова метода:
CxList runSuperSecureSQLQuery = All.FindByName("*runSuperSecureSQLQuery*").FindByType(typeof(MethodInvokeExpr));
result = runSuperSecureSQLQuery;
Результат:
В результате получили 7 мест, 4 из них искомые обращения к методу runSuperSecureSQLQuery() (Классы Billing и User). 2 – обращения к внутреннему методу runSuperSecureSQLQuery() внутри класса Session, и еще один – метод add, который скорее является некой странностью поиска CMxQL. Скажем так, я не ожидал, что он будет в списке =) Токены в классе Session, как мы выяснили в шаге 1, нам не интересны, поэтому дальше мы их просто вычтем из результата:
CxList runSuperSecureSQLQuery = All.FindByName("*runSuperSecureSQLQuery*").FindByType(typeof(MethodInvokeExpr));
result = runSuperSecureSQLQuery - exclusionList;
Получаем валидный список обращений к искомому методу:
Обратите внимание на функции FindByType() и typeof() в предыдущем запросе. Если мы хотим произвести поиск по CMx типу, то есть по CxList свойству “Source Type” – то мы используем typeof(Source Type). Если же мы хотим сделать поиск по типу данных ЯП, то нужно передать параметр просто как строку. Например:
result = All.FindByType("String");
найдет все java-токены с типом String.
SQLi: Шаг 4. Найти все параметры вызываемых методов runSuperSecureSQLQuery
Для поиска параметров метода используется CMxQL функция GetParameters():
CxList runSSSQParams = All.GetParameters(runSuperSecureSQLQuery);
result = runSSSQParams;
Результат:
SQLi: Шаг 5. Найти точки входа для мест выполнения запросов в БД
Для этого сначала получим названия родительских методов, внутри которых находятся вызовы к БД runSuperSecureSQLQuery, а затем получим их параметры. Для поиска родительских токенов используется CMxQL функция GetAncOfType():
CxList entryPoints = runSuperSecureSQLQuery.GetAncOfType(typeof(MethodDecl));
result = entryPoints;
В этом запросе для множества runSuperSecureSQLQuery вернуться все родительские токены с типом MethodDecl — это предыдущий метод в стеке вызовов:
Для поиска параметров метода также используем GetParameters():
CxList entryPointsParameters = All.GetParameters(entryPoints).FindByType("String");
Запрос вернет параметры подмножества entryPoints с Java-типом String:
SQLi: Шаг 6. Найти зависимости параметров runSSSQParams от entryPointsParameters, при этом только те места, где отсутствует сантитизация sanitization ввода
На этом шаге мы используем функции анализа. Для анализа Flow кода используются следующие функции:
- InfluencedBy()
- InfluencedByAndNotSanitized()
- InfluencingOn()
- InfluencingOnAndNotSanitized()
- NotInfluencedBy()
- NotInfluencingOn()
Чтобы найти Flow зависимости параметров запроса runSSSQParams от параметров родительского метода entryPointsParameters и исключить токены санитизации:
CxList dataInflOnTable = runSSSQParams.InfluencedByAndNotSanitized(entryPointsParameters, sanitization);
При этом я не уверен, что функции *AndNotSanitized внутри делают какую-то магию, и больше похоже на то, что метод просто вычитает множество sanitized из своего результата. То есть, если сделать:
CxList dataInflOnTable = runSSSQParams.InfluencedBy(entryPointsParameters) - sanitization;
получается тоже самое. Хотя может быть я просто не нашел варианта, когда отличия все же есть.
Результат запроса выдает нам корректно построенный Flow:
Получили Flow с потенциальной SQL-инъекцией. Как видно из скриншота, Checkmarx вернул 3 Flow. Flow на скриншоте самый короткий, он начинается и заканчивается в одном файле и одном методе. Следующий Flow уходит уже в класс Session. Обратите внимание на Source/Destination. И последний — это еще один метод в классе Session. Flow внутри Session будет выглядеть вот так:
Чтобы отобрать какой-то один Flow, используется метод ReduceFlow(CxList.ReduceFlowType flowType), где flowType может быть:
- CxList.ReduceFlowType.ReduceBigFlow — отобрать самые короткие Flow
- CxList.ReduceFlowType.ReduceSmallFlow — отобрать самые длинные Flow
SQLi: Итоговый запрос для поиска SQL инъекций
// 1. Поиск исключений
CxList exclusionList = All.GetByClass(All.FindByName("*Session*"));
// 2. Поиск мест санитизации
CxList sanitization = All.FindByName("*Long*");
// 3. Поиск обращений к runSuperSecureSQLQuery()
CxList runSuperSecureSQLQuery = All.FindByName("*runSuperSecureSQLQuery*").FindByType(typeof(MethodInvokeExpr));
runSuperSecureSQLQuery -= exclusionList;
// 4. Поиск параметров вызываемого метода runSuperSecureSQLQuery()
CxList runSSSQParams = All.GetParameters(runSuperSecureSQLQuery);
// 5. Поиск параметров методов, родительских по отношению к runSuperSecureSQLQuery()
CxList entryPoints = runSuperSecureSQLQuery.GetAncOfType(typeof(MethodDecl));
CxList entryPointsParameters = All.GetParameters(entryPoints).FindByType("String");
// 6. Поиск зависимости параметров запроса в базу (runSuperSecureSQLQuery) от параметров родительского метода
CxList dataInflOnTable = runSSSQParams.InfluencedByAndNotSanitized(entryPointsParameters, sanitization);
// 7. Вывод результата
result = dataInflOnTable.ReduceFlow(CxList.ReduceFlowType.ReduceBigFlow);
Пример 2: Поиск Insecure Direct Object References
В этом запросе мы будем искать все места, где происходит работа с объектами без проверки владельца объекта. При этом могут использоваться разные названия HTTP-параметров для mailboxid (предполагаем, что это легаси), также сама проверка может происходить на разных этапах: где-то прямо в точке HTTP API входа, где-то — перед запросом в базу, а иногда и в промежуточных методах.
План поиска
- Определить исключения (exclusionList);
- Определить места проверок авторизации (idorSanitizer);
- Найти точки входа – места первичной обработки HTTP-запросов (webRemoteMethods);
- Только по токенам точек входа найти места извлечения HTTP-параметра mailboxid (mailboxidInit);
- Найти все вызовы из webRemoteMethods к методам middleware и параметры этих вызовов (middlewareMethods);
- Найти middleware-методы, которые зависят от mailboxid (apiPotentialIDOR);
- Найти все места определения методов middleware(middlewareDecl);
- Пройти по всем apiPotentialIDOR и отобрать только те middlewareDecl, в которых нет проверки владельца объекта mailboxid.
IDOR: Шаг 1. Определить исключения
В этом случае исключим все токены в определенном файле:
CxList exclusionList = All.FindByFileName("*WebMethodContext.java");
result = exclusionList;
WebMethodContext.java содержит реализацию таких методов как getMailboxId и getUserId, а также строку «mailboxid». Т. к. название токенов будет совпадать c нужными нам для поиска уязвимости, этот файл будет выдавать ложные нахождения.
IDOR: Шаг 2. Определить места проверок авторизации
В тестовом приложении для определения, принадлежит ли запрашиваемый объект пользователю, используется метод validateMailbox():
CxList idorSanitizer = All.FindByName("*validateMailbox*");
result = idorSanitizer;
Результат:
IDOR: Шаг 3. Найти точки входа пользовательских запросов HTTP API
Обработчики HTTP-запросов имеют спец аннотацию, по которой их легко найти. В моем случае это «WebRemote», для поиска аннотаций используется CMxQL функция FindByCustomAttribute(). Для FindByCustomAttribute(), функция поиска родительского токена GetAncOfType() вернет метод под аннотацией:
CxList webRemoteMethods = All.FindByCustomAttribute("WebRemote")
.GetAncOfType(typeof(MethodDecl));
result = webRemoteMethods;
Результат запроса:
IDOR: Шаг 4. Только по токенам точек входа найти места извлечения HTTP параметра mailboxid
Для того, чтобы найти токены, относящиеся к обработке HTTP-параметра mailboxid:
CxList getMailboxId = All.FindByName("\"mailboxId\"")
+ All.FindByName("\"mid\"")
+ All.FindByName("\"boxid\"");
result = getMailboxId;
мы сложили 3 множества с 3-мя разными строками, т.к. по легенде, в разных частях системы название HTTP-параметра может отличаться.
Запрос найдет все места, где mailboxid/mid/boxid написан как строка(в двойных кавычках). Но этот запрос вернет очень много нахождений, т.к. такая строка может встречаться не только в местах извлечения HTTP параметров. Если дальше мы будем работать с этим множеством, то получим огромное количество ложных нахождений.
Поэтому мы сделаем поиск только по токенам точек входа (webRemoteMethods). Для нахождения всех дочерних токенов используется CMxQL функция GetByAncs():
result = All.GetByAncs(webRemoteMethods);
Запрос вернет все токены, принадлежащие методам, аннотированным как WebRemote. Уже на этом этапе мы можем отфильтровать токены тех методов, в которых делается проверка owner объекта. Поэтому перепишем предыдущий запрос для поиска дочерних токенов так, чтобы отобрать только дочерние токены WebRemote методов, где нет секьюрити-проверки владельца объекта. Для этого используем цикл с условием:
// создаем пустое множество для дочерних токенов потенциально опасных методов
CxList entry_point_tokens = All.NewCxList();
// идем по множеству родительских токенов webRemoteMethods
foreach (CxList method in webRemoteMethods) {
// достаем все дочерние токены текущего родительского токена
CxList method_tokens = All.GetByAncs(method);
// проверяем, есть ли в списке дочерних токенов токены, используемые для проверки owner
if (method_tokens.FindByName(idorSanitizer).Count > 0) {
// если да, то ничего делать не будем, считаем, что этот метод безопасен
} else {
// если нет, то вносим токены метода в список потенциально опасных
entry_point_tokens.Add(method_tokens);
}
}
Теперь мы можем сделать более точную выборку по HTTP параметрам mailboxid:
CxList getMailboxHTTPParams = entry_point_tokens.FindByName("\"mailboxid\"")
+ entry_point_tokens.FindByName("\"mid\"")
+ entry_point_tokens.FindByName("\"boxid\"");
result = getMailboxHTTPParams;
Но нас интересуют не сами места, где достаются HTTP-параметры, а переменные, которым в итоге присваиваются значения HTTP-параметров. Т. к. дальше надежнее искать Flow именно по токенам переменных.
CMxQL функция FindByInitialization() найдет места инициализации переменных для заданных токенов:
CxList mailboxidInit = entry_point_tokens.FindByInitialization(getMailboxHTTPParams);
result = mailboxidInit;
Результат:
IDOR: Шаг 5. Найти все вызовы из webRemoteMethods к методам middleware и параметры этих вызовов
Под middleware я подразумеваю код, который уходит глубже методов обработки HTTP API запросов, то есть глубже точек входа пользовательских запросов. Например, для скриншота выше, это методы класса User, вызовы user.getSettings() и user.getSecureSettings():
CxList middlewareMethods = All.FindByShortName("user").GetRightmostMember();
CxList middlewareMethodsParams = entry_point_tokens.GetParameters(middlewareMethods);
result = middlewareMethodsParams;
Cначала отберем все токены с именем user, и затем с помощью GetRightmostMember() отберем токены вызовов к middleware. GetRightmostMember() в цепочке вызовов методов вернет самый правый. После чего выведем параметры найденного метода с помощью GetParameters().
Результат:
IDOR: Шаг 6. Найти middleware-методы, которые зависят от mailboxid
Для анализа Flow используются методы *InfluencedBy* и *InfluncingOn*. Различие между ними понятно по названию.
Например:
All.InfluencedBy(getMailboxHTTPParams)
пройдет по множеству All и найдет все токены, которые зависят от getMailboxHTTPParams.
Тоже самое можно написать другим способом:
getMailboxHTTPParams.InfluencingOn(All)
Для поиска токенов, зависимых от mailboxidInit:
CxList apiPotentialIDOR = entry_point_tokens.InfluencedByAndNotSanitized(mailboxidInit, idorSanitizer);
result = apiPotentialIDOR;
Результат:
IDOR: Шаг 7. Найти все места определения методов middleware
Найдем определения всех промежуточных методов, которые могут использоваться в местах обработки пользовательских запросов. Для этого выделим их общее свойство, например во всех таких методах есть создание объекта Request(), создание объекта имеет CMx тип ObjectCreateExpr:
CxList requests = (All - exclusionList).FindByType(typeof(ObjectCreateExpr)).FindByName("*Request*");
CxList middlewareDecl = requests.GetAncOfType(typeof(MethodDecl));
result = middlewareDecl;
(All — exclusionList) — можно сделать такое вычитание множеств, после чего вызвать нужную CMxQL функцию от получившегося результата. Теперь requests содержит все токены с именем Request и типом, соответствующим созданию объекта.
Далее с помощью уже знакомого GetAncOfType() находим родительский токен с типом MethodDecl.
Результат:
IDOR: Шаг 8. Пройти по всем apiPotentialIDOR и отобрать только те middlewareDecl, в которых нет проверки владельца объкта mailboxid
В заключительной части запроса мы определим, какие из middleware-методов вызываются напрямую из методов точек входа и не проверяют, кому принадлежит mailboxid. После чего объединим Flow для более удобного анализа результатов.
Новые функции, которые мы еще не исопльзовали:
GetCxListByPath() – эта функция нужна для итерации по Flow, если ее НЕ использовать то CMx сожмет Flow в Code Element(в первую ноду flow)
Concatenate*() — ряд функций, необходимых для объединения нескольких flow в одно
FindByParameters() – найти метод по конкретному токену-параметру
GetName() – вернет строку с именем токена, если в CxList больше одного элемента, то вернет первый. Метод используется только при итерации по элементам множества.
Заключительная часть запроса:
// создаем пустое множество
CxList vulns = All.NewCxList();
// итерируемся по Flow множества apiPotentialIDOR
foreach(CxList cxFlow in apiPotentialIDOR.GetCxListByPath()) {
// извлекаем последнюю ноду Flow
CxList endNode = cxFlow.GetStartAndEndNodes(CxList.GetStartEndNodesType.EndNodesOnly);
// находим вызов метода по параметру из flow (mailboxid)
CxList method_call = entry_point_tokens.FindByParameters(endNode);
// находим определение этого метода
CxList method_decl = middlewareDecl.FindByShortName(method_call.GetName());
// если определение метода найдено
if (method_decl.Count > 0) {
// извлекам все токены внутри этого метода
CxList _all = (All - exclusionList).GetByAncs(method_decl);
// проверяем есть ли в методе санитизация
if (_all.FindByName(idorSanitizer).Count > 0) {
// если есть, то просто добавляем запись в лог
cxLog.WriteDebugMessage("find sanitized in method: " + method_call.GetName());
// если нет, то добавляем Flow в множество потенциально опасных vulns
} else {
// при этом добавляем к Flow токены вызова метода и его определения
vulns.Add(cxFlow.ConcatenatePath(method_call).ConcatenatePath(method_decl));
cxLog.WriteDebugMessage("find NOT sanitized in method: " + method_call.GetName());
}
}
}
Результат:
CocatenatePath мы использовали, чтобы при анализе всех нахождений было удобно перемещаться по коду. Этот метод прицепляет один Code Element к Flow
IDOR: Итоговый запрос для поиска IDOR
// 1. Определить исключения
CxList exclusionList = All.FindByFileName("*WebMethodContext.java");
// 2. Определить места проверок авторизации
CxList idorSanitizer = All.FindByName("*validateMailbox*");
// 3. Найти точки входа – места первичной обработки HTTP запросов
CxList webRemoteMethods = All.FindByCustomAttribute("WebRemote").GetAncOfType(typeof(MethodDecl));
// 4. Только по токенам точек входа найти места извлечения HTTP параметра mailboxid
// Поиск токенов точек входа
CxList entry_point_tokens = All.NewCxList();
foreach (CxList method in webRemoteMethods) {
CxList method_tokens = All.GetByAncs(method);
if (method_tokens.FindByName(idorSanitizer).Count > 0) {
} else {
entry_point_tokens.Add(method_tokens);
}
}
// Поиск мест извлечения HTTP параметра и инициализации соотв-ей переменной
CxList getMailboxHTTPParams = entry_point_tokens.FindByName("\"mailboxId\"")
+ entry_point_tokens.FindByName("\"mid\"")
+ entry_point_tokens.FindByName("\"boxid\"");
CxList mailboxidInit = entry_point_tokens.FindByInitialization(getMailboxHTTPParams);
// 5. Найти все вызовы к методам middleware и параметры этих вызовов
CxList middlewareMethods = All.FindByShortName("user").GetRightmostMember();
CxList middlewareMethodsParams = entry_point_tokens.GetParameters(middlewareMethods);
// 6. Найти middleware методы, на которые влияет переменая mailboxid
CxList apiPotentialIDOR = entry_point_tokens.InfluencedByAndNotSanitized(mailboxidInit, idorSanitizer);
// 7. Найти все места определения методов middleware и все токены этих методов
CxList requests = (All - exclusionList).FindByType(typeof(ObjectCreateExpr)).FindByName("*Request*");
CxList middlewareDecl = requests.GetAncOfType(typeof(MethodDecl));
// 8. Пройти по всем apiPotentialIDOR и отобрать только те middlewareDecl, которые используются на точках входа
CxList vulns = All.NewCxList();
foreach(CxList cxFlow in apiPotentialIDOR.GetCxListByPath()) {
CxList endNode = cxFlow.GetStartAndEndNodes(CxList.GetStartEndNodesType.EndNodesOnly);
CxList method_call = entry_point_tokens.FindByParameters(endNode);
CxList method_decl = middlewareDecl.FindByShortName(method_call.GetName());
if (method_decl.Count > 0) {
CxList _all = (All - exclusionList).GetByAncs(method_decl);
if (_all.FindByName(idorSanitizer).Count > 0) {
cxLog.WriteDebugMessage("find sanitized in method: " + method_call.GetName());
} else {
vulns.Add(cxFlow.ConcatenatePath(method_call).ConcatenatePath(method_decl));
cxLog.WriteDebugMessage("find NOT sanitized in method: " + method_call.GetName());
}
}
}
result = vulns;
Заключение
Checkmarx без проблем разбирает код на токены, при этом определяя их типы. Также статический анализатор хорошо выполняет простой поиск токенов, например найти родительский токен к текущему, найти инициализацию переменной, найти параметры методов и т.д. Почти так же хорошо строит Flow (но иногда все же промахивается). Всё это дает возможность работать с кодом как с любой БД, с той разницей что структура кода заранее не определена, и ее приходится «подгонять» самому.
Чтобы сильно уменьшить количество false positive, стоит обратить внимание на следующее:
- Добавление исключений в настройках проектов, чтобы исключить лишние файлы и директории (например директории с тестами).
- Просмротреть сами запросы из коробки, и отключить ненужные (или изменить запрос). Например, есть такой тип бага «Privacy Violation», он ищет места, где сенситив данные могут быть либо записаны в файл, либо посланы в Web UI. Последнее не особо полезно, т.к. чаще всего отображение сенситив данных в UI это часть бизнес логики. И защита в данном случае обеспечивается TLS для защиты канала и аудитом на предмет XSS для защиты данных в браузере.
- Иногда какой-то баг может не найтись, потому что в запросе из коробки не оказалось дефолтного токена (да, это конечно косяк). Например, для поиска XXE стоит проверить, все ли токены, соотв-е названиям библиотек по умолчанию, есть в запросе.
- Если много false positive, которые совсем мимо, нужно смотреть запрос из коробки и сравнивать названия токенов в CMxQL функциях вида FindBy/GetBy. Скорее всего названия ожидаемых токенов будут отличаться от тех, что используются в коде (в самом начале я приводил пример с пропущенными SQLинъекциями).
- Если много false positives, которые нашли правильные места, но в итоге там бага нет, то вам повезло, скорей всего нужно совсем немного подправить запрос, чтобы указать CMx, что является санитизацией для этого запроса. Например, у нас нашлись правильные места с LDAP запросами, где поля запросов частично контролируются пользователями. Но по факту в каждом методе c LDAP-запросом был другой метода, который проводил санитизацию, и вот его чекмаркс не понял.
Надеюсь данный how-to будет полезен в качестве «hello world» для тех, кто только начинает писать свои запросы в Checkmarx.
Комментарии (4)
o_nix
12.12.2019 15:01Если мы руками написали кастомный AST-процессор, то зачем нам Checkmarx за много денег? Почему не просто опенсорсный AST-парсер с хуками?
focuz Автор
13.12.2019 11:56не знаю что вам ответить, если мы можем руками написать такой же функционал как в комерческом продукте, то действительно, зачем покупать комерческий продукт за много денег? =)
y4ppieflu
Эм, я правильно понимаю, что Вы взяли конкретное приложение, вручную проанализировали что оно делает и как называются потенциально опасные методы и под это все подогнали запрос? Это супер неуниверсально и, следовательно, не имеет практической пользы — проще уж тогда ручным security code review заниматься.
В каждом языке/фреймворке есть вполне стандартные методы для доступа к БД — их и ищет Checkmarx, а затем строит дерево до вызывающей «пользовательской» функции. Кастомные костыли для стандартных действий, это, пожалуй, единственный кейс для описанного подхода, но как по мне, если они используются это уже само по себе плохо.
focuz Автор
Практическая польза в том, что тюнинг запросов требуется только в начальной стадии проекта, дальше в процессе жизни проекта запрос будет просто работать и отлавливать баги. Это универсально для конкретного репозитория.