####################
免责声明:工具本身并无好坏,希望大家以遵守《网络安全法》相关法律为前提来使用该工具,支持研究学习,切勿用于非法犯罪活动,对于恶意使用该工具造成的损失,和本人及开发者无关。
####################
这篇文章的目的不是解释原型污染漏洞是什么,但总的来说,能够编辑对象的原型或Object
原型(通过它们的属性)可以让攻击者污染它并可能恶意地改变受影响代码的目标。
小工具
我们可以将 [在此处插入漏洞] 小工具理解为帮助漏洞发生的代码片段或行为。在这种情况下,原型污染小工具是未定义的对象属性读取,它流向 JS 执行函数(例如eval
或Function
)。
-
不需要定义小工具,因为对象的属性读取使用对象的原型属性读取作为后备。
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 }
复制
进化只是专注于获得正确的结果,就像tainted
在tainted + 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 查找原型污染小工具
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论