
哈Ha!
在本文中,我想谈谈我们在Checkmarx SAST中创建查询的经验。
初次熟悉此分析器时,您可能会得到的印象是,除了搜索弱加密/哈希算法和大量误报外,它不返回任何其他内容。 但是,如果配置正确,它是一种超级强大的工具,可以查找严重的错误。
我们将了解Checkmarx SAST查询语言的复杂性,并编写2个查询来搜索SQL注入和不安全的直接对象引用。
参赛作品
在长期搜索Checkmarx上的任何指南或文章之后,对我来说很清楚,除了正式文档之外,没有足够的有用信息。 官方文档并没有说一切都变得非常清晰易懂。 例如,我找不到任何最佳做法,如何正确组织覆盖查询,如何编写“傻瓜”查询等。是的,有关于CMx查询语言功能的文档,但这是将这些功能组合成一个查询的方法,该文档未编写。
Checkmarx社区缺少文章和指南,可能与该工具的高昂成本有关,因此受众不多。 或者,也许只有很少的人会费力地进行微调,并直接使用解决方案。
根据我的经验,我发现,更多地使用SAST来满足客户方面与各种要求有关的手续,而不是寻找真正的错误。 结果,通过这种方法,我们充其量只有很少数量的“漏洞”,几乎自动被称为“不可利用”(因为在99.9%的情况下都是这样)。
应当注意,Checkmarx本身正在尝试更新其查询,以使它们开箱即用地提供最佳结果。 但是CMx查询语言查询是针对“一般情况”量身定制的。 令牌的初始搜索基于名称。 例如,CMx SAST假定对数据库的所有查询将如下所示:* createQuery *或* createSQLQuery *。 但是,如果使用内部开发来处理数据库,并且以不同的方式调用查询数据库的方法(例如* driveMyQuery *),那么将跳过所有SQL方法。 例如,我们的客户对SQL DB使用定制的ORM。 在这种情况下,开箱即用的CMx查询将跳过所有SQL注入。
缩写和定义
CMx -Checkmarx SAST。
CMxQL -Checkmarx SAST查询语言
令牌 -具有特定值的字符串是词法分析器工作的结果(也称为
令牌化)
测试申请
为了写一篇文章,我画了一些Java代码,一个小的测试应用程序。 此代码是实际系统的一小部分的近似副本。 尽管一般而言,测试应用程序的代码与任何其他HTTP后端代码并没有太大区别。 屏幕截图中将显示测试应用程序代码的关键部分。
测试应用程序具有以下结构
WebRouter类,用于处理传入的HTTP请求;内部处理URL的4种方法:
- / getTransaction-在输入处接受交易ID 并返回有关其的信息 , ID将其作为字符串,并将其传递给getTransactionInfo(transactionId) => getTransactionInfo(transactoinId) -使transactionId与SQL查询连接(即获得SQL注入);
- / getSecureTransaction-在输入处接收交易ID , 并返回有关其的信息 , ID将其作为字符串并将其传递给getTransactionInfoSecured() => getTransactionInfoSecured(transactoinId) -首先将字符串transactionId转换为Long类型,然后将其连接到SQL查询(在此示例中)如果注射未被利用);
- / getSettings-接受userId和mailboxId作为输入 -并发布邮箱设置。 不验证邮箱标识是否属于该用户;
- / getSecureSettings-还接受userId和mailboxId作为输入并显示邮箱设置。 但是,将检查邮箱标识是否属于该用户。
CMx:一般信息和基本定义
在开始开发查询之前
查询开发是在单独的程序CxAuditor中进行的。 在CxAuditor中,您需要扫描所有代码(创建本地项目),我们将为此编写查询。 之后,您可以编写和运行新查询。 使用大型代码库,主扫描可能要花费数小时的时间和千兆字节的内存。 此后,每个请求将不会足够快地执行。 这完全不适合开发。
因此,您可以从项目中获取少量文件,理想情况下,可以在代码中找到一个错误,该错误要早于我们正在编写请求的类型(或将错误手动放置在此处)并仅扫描这组文件。 不必遵守项目的文件结构。 也就是说,如果您具有Java程序包A和B,并且程序包B中的类使用程序包A的类和方法,则可以将所有这些都放在一个目录中,并且CMx仍将理解关系并正确地建立文件之间的调用链(很好,或几乎总是正确的,尽管错误几乎与项目的文件结构无关)。
基本定义
列表
CMx中的主要数据类型。 几乎所有CMxQL函数的结果都是
CxList 。 这是许多具有某些属性的元素。 以下将考虑对开发最有用的属性。
结果
CMxQL具有内置的变量
结果 。 执行完整个查询后,包含
结果变量的集合将显示为结果。
也就是说,任何查询的最终操作都应该是字符串
result = WHATEVER ,例如:
result = All.FindByName("anyname");
流和代码元素
根据返回值的类型,大多数CMxQL函数分为2个,返回“代码元素”的函数和返回Flow的函数。 在这两种情况下,结果都是
CxList 。 但是对于Flow和代码元素,其内容将略有不同。
- 代码元素 -令牌-例如变量,方法调用,赋值等;
- 流 -给定令牌之间的关系。
全部和“子”全部
每个CMxQL函数都可以在
All集合(它包含整个扫描代码的所有标记,我们已经看到了带有
result的示例)上执行,也可以在
CxList集合上执行,而
CxList集合又是通过查询中某些操作(例如查询)而获得的:
CxList newList = CxList.New();
将创建一个空集合,然后我们可以使用
Add()方法填充元素,然后按新集合的元素进行搜索:
CxList newFind = newList.FindByName("narrowedScope");
找到的项目的属性
CxList集的每个元素都有几个属性。 分析写查询的结果时,最有用的是:
- SourceFile-包含此元素的文件的名称;
- 源行 -带令牌的行号;
- 源名称 -令牌的名称。 等同于令牌,即,如果变量名为var1,则源名称= var1;
- 源类型 -令牌的类型。 例如,如果它是一个字符串,则它将是StringLiteral,如果调用了该方法,则将调用MethodInvokeExpr,等等。
- 目标文件
- 目的行;
- 目的地名称;
- 目标类型。
如果结果集中的元素为Flow,则Source和Destination将有所不同;反之亦然,如果结果是代码元素,则它们将匹配。
开始创建查询
所有CMxQL功能都可以分为几种类型。 在我看来,这里可以注意到CMxQL文档的主要缺点,扩展坞中的所有功能都是按字母顺序简单描述的,而根据功能(然后仅按字母顺序)构建它们会更加方便。
- 搜索功能-几乎所有名称为FindBy *和GetBy *的 CMxQL函数;
- 集上运算的功能是加,减,交,元素上的迭代等。
- 分析函数-这些基本上是* ImpactdBy * * InfluencingOn *函数。
查询的基本原理是这些类型的功能的交替。 首先,使用搜索功能,我们仅通过某些属性选择我们感兴趣的令牌。 使用集合上的运算,我们可以将具有不同令牌属性的不同集合组合成一个集合,反之亦然,从集合中减去另一个集合。 然后,使用分析功能构建代码流,并尝试了解潜在漏洞是否取决于入口点的参数。
开始搜索的位置(通常是整个搜索路径)的选择取决于特定的代码,更确切地说,甚至取决于“文本”。 在某些情况下,从入口点搜索用户查询会很方便,在某些情况下,从“结尾”甚至中间位置开始会更方便。 所有这些都取决于特定的代码,您需要分别访问每个存储库。
示例:搜索SQL注入
搜索计划,在方括号中,我指出了集合的名称(查询中的变量):
- 定义例外-可以立即扔出搜索范围的令牌( exclusionList );
- 确定消毒/安全检查( 消毒 )的位置;
- 在数据库中查找所有具有查询执行的低级位置( runSuperSecureSQLQuery );
- 查找被调用方法的所有参数runSuperSecureSQLQuery ( runSSSQParams );
- 查找数据库中查询执行位置的入口点(父方法及其参数)( entryPointsParameters );
- 查找runSSSQParams参数对entryPoints的依赖关系,而仅查找那些没有清理输入清理的地方 。
结果,我们获得了带有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()将SQL查询中的transactionId参数连接起来,而
getTransactionInfoSecured()首先
将 transactionId
转换为Long,然后将其作为字符串传递。 两种方法都嵌入了漏洞(参数级联)。 但是,由于在
getTransactionInfoSecured()中强制转换为Long,所以最后一个方法不容易受到注入的影响,因为当我们尝试传递注入(字符串)时,我们会收到Java异常。
在此示例中,我们将对Long的演员表视为卫生站点。 要找到这些令牌:
CxList sanitization = All.FindByName("*Long*"); result = sanitization;
结果示例:

结果包括具有YP类型
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-调用
Session类内部的runSuperSecureSQLQuery()内部方法,还有一个是
add方法,这是某种CMxQL搜索奇数。 只是说我不希望它出现在列表中=)正如我们在步骤1中发现的那样,
Session类中的标记对我们来说并不有趣,因此我们将从结果中减去它们:
CxList runSuperSecureSQLQuery = All.FindByName("*runSuperSecureSQLQuery*").FindByType(typeof(MethodInvokeExpr)); result = runSuperSecureSQLQuery - exclusionList;
我们获得对所需方法的有效调用列表:

注意上一个查询中的
FindByType()和
typeof()函数。 如果
要按 CMx类型进行搜索,即按
CxList属性“源类型”进行搜索-那么我们使用
typeof(源类型) 。 如果要按数据类型进行搜索,则需要像字符串一样传递参数。 例如:
result = All.FindByType("String");
将找到所有类型为String的Java令牌。
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");
该查询将返回Java类型为String的
entryPoints子集的参数:

SQLi:步骤6。查找runSSSQParams参数对entryPointsParameters的依赖关系,而仅查找那些没有消毒输入的地方
在这一步中,我们使用分析功能。 以下功能用于分析流代码:
- 影响力dBy()
- ImpactdByAndNotSanitized()
- 影响()
- 影响OnAndNotSanitized()
- 不影响dBy()
- 不影响开启()
要根据
entryPointsParameters父方法的参数查找
runSSSQParams请求参数流,并排除卫生令牌:
CxList dataInflOnTable = runSSSQParams.InfluencedByAndNotSanitized(entryPointsParameters, sanitization);
但是,我不确定内部的
* AndNotSanitized函数是否起到某种作用,它看起来更像是该方法只是从结果中减去已清理的集合。 也就是说,如果您这样做:
CxList dataInflOnTable = runSSSQParams.InfluencedBy(entryPointsParameters) - sanitization;
结果也一样。 虽然也许我仍然没有选择,但是仍然存在差异。
查询结果为我们提供了正确构造的Flow:

通过潜在的SQL注入获得流程。 从屏幕截图可以看出,Checkmarx返回了3 Flow。 屏幕截图中的流程最短,它以一个文件和一种方法开始和结束。 下一个Flow已离开Session类。 注意源/目标。 最后一个是Session类中的另一种方法。
会话内的流程如下所示:

要选择一个Flow,请使用
ReduceFlow方法
(CxList.ReduceFlowType flowType) ,其中flowType可以是:
- CxList.ReduceFlowType.ReduceBigFlow-选择最短的Flow
- CxList.ReduceFlowType.ReduceSmallFlow-选择最长的流
SQLi:查找SQL注入的最终查询
示例2:搜索不安全的直接对象引用
在此请求中,我们将搜索与对象一起工作的所有位置,而无需检查对象的所有者。 在这种情况下,可以对mailboxid使用不同名称的HTTP参数(我们假设这是旧式的),并且验证本身可以在不同的阶段进行:在HTTP入口API点的某个位置,在数据库请求之前的某个位置,有时在中间方法中。
搜索计划
- 定义异常( exclusionList );
- 确定授权检查的位置( idorSanitizer );
- 查找入口点-HTTP请求主要处理的位置( webRemoteMethods );
- 只有通过入口点令牌才能找到HTTP参数的提取位置mailboxid ( mailboxidInit );
- 查找从webRemoteMethods到中间件方法的所有调用以及这些调用的参数( middlewareMethods );
- 查找依赖于mailboxid( apiPotentialIDOR )的中间件方法;
- 查找定义了中间件方法的所有位置( middlewareDecl );
- 遍历所有apiPotentialIDOR并仅选择其中没有对mailboxid对象的所有者进行验证的中间件 Decl 。
IDOR:步骤1.识别异常
在这种情况下,请排除特定文件中的所有令牌:
CxList exclusionList = All.FindByFileName("*WebMethodContext.java"); result = exclusionList;
WebMethodContext.java包含诸如
getMailboxId和
getUserId之类的方法的实现,以及字符串“ mailboxid”。 由于令牌的名称将与我们搜索漏洞所需的名称一致,因此该文件将发出错误的发现。
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。仅使用入口点令牌,找到mailboxid参数的HTTP提取位置
要查找与HTTP Mailboxid参数的处理有关的令牌:
CxList getMailboxId = All.FindByName("\"mailboxId\"") + All.FindByName("\"mid\"") + All.FindByName("\"boxid\""); result = getMailboxId;
我们添加了3组带有3条不同的线,因为 根据传说,HTTP参数的名称在系统的不同部分可能有所不同。
该查询将找到将
mailboxid / mid / boxid写为字符串(用双引号引起来)的所有位置。 但是此查询将返回很多结果tk。 这样的字符串不仅可以在提取HTTP参数的地方找到。 如果我们继续使用此集合,将会得到大量的错误发现。
因此,我们将仅搜索入口点的令牌(
webRemoteMethods )。 要查找所有子令牌,请使用CMBQL函数
GetByAncs() :
result = All.GetByAncs(webRemoteMethods);
该请求将返回属于标注为
WebRemote的方法的所有令牌。 在这个阶段,我们已经可以过滤那些检查对象所有者的方法的标记。 因此,我们重写前一个查询以搜索子令牌,以便仅选择
WebRemote方法的子令牌,而对对象所有者不进行安全检查。 为此,请使用条件循环:
现在,我们可以使用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到中间件方法的所有调用以及这些调用的参数
所谓中间件,是指代码比HTTP API请求的处理方法更深,即比用户请求的入口点更深。 例如,对于上面的屏幕截图,这些是
User类的方法,对
user.getSettings()和
user.getSecureSettings()的调用:
CxList middlewareMethods = All.FindByShortName("user").GetRightmostMember(); CxList middlewareMethodsParams = entry_point_tokens.GetParameters(middlewareMethods); result = middlewareMethodsParams;
首先,我们选择名称为user的所有令牌,然后使用
GetRightmostMember()选择中间件的调用令牌。 方法调用链中的
GetRightmostMember()将返回最右边的一个。 然后,使用
GetParameters()导出找到的方法的参数。
结果:

IDOR:步骤6。查找依赖于mailboxid的中间件方法
流量分析使用
* ImpactdBy *和
* InfluncingOn *方法 。 它们之间的区别在名称上很明显。
例如:
All.InfluencedBy(getMailboxHTTPParams)
将遍历集合All并找到所有依赖于
getMailboxHTTPParams的令牌。
可以用另一种方式写同样的东西:
getMailboxHTTPParams.InfluencingOn(All)
要搜索取决于
mailboxidInit的令牌:
CxList apiPotentialIDOR = entry_point_tokens.InfluencedByAndNotSanitized(mailboxidInit, idorSanitizer); result = apiPotentialIDOR;
结果:

IDOR:步骤7。查找所有位置以定义中间件方法
让我们找到可在处理用户请求的地方使用的所有中间方法的定义。 为此,我们突出显示它们的公共属性,例如,在所有此类方法中,都有一个request
()对象创建,该对象创建为CMx类型的
ObjectCreateExpr :
CxList requests = (All - exclusionList).FindByType(typeof(ObjectCreateExpr)).FindByName("*Request*"); CxList middlewareDecl = requests.GetAncOfType(typeof(MethodDecl)); result = middlewareDecl;
(全部 -exclusionList
) -您可以对集合进行此减法,然后从结果中调用所需的CMxQL函数。 现在,
请求包含名称为“
请求”的所有令牌以及与对象创建相对应的类型。
接下来,使用熟悉的
GetAncOfType(),找到类型为
MethodDecl的父标记。
结果:

IDOR:步骤8。遍历所有apiPotentialIDOR,并仅选择其中没有对mailboxid对象的所有者进行验证的中间件Decl
在请求的最后部分,我们将确定直接从入口点方法中调用哪些中间件方法,而不检查谁的
信箱ID属于谁。 然后结合Flow以更方便地分析结果。
我们尚未使用的新功能:
GetCxListByPath() -需要此函数来迭代Flow,如果不使用它,则CMx将压缩Code Element中的Flow(在第一个流节点中)
级联*() -将多个流合并为一个所需的许多函数
FindByParameters() -通过特定参数标记查找方法
GetName() -将返回带有令牌名称的字符串,如果CxList中有多个元素,则它将返回第一个。 该方法仅在迭代集合的元素时使用。
请求的最后一部分:
结果:
我们使用CocatenatePath,以便在分析所有位置时可以方便地浏览代码。此方法将一个代码元素附加到流。IDOR:对IDOR的最终搜索
结论
Checkmarx可以轻松地将代码解析为令牌,同时确定其类型。此外,静态分析器还可以很好地搜索令牌,例如,找到当前令牌的父令牌,查找变量的初始化,查找方法的参数等。Flow在构建方面几乎同样出色(但有时仍然会遗漏)。所有这些使得可以像处理任何数据库一样使用该代码,不同之处在于该代码的结构不是预定义的,您必须自己“自定义”它。为了大大减少误报的数量,请注意以下几点:- , ( ).
- , ( ). , «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.