CodeQL:GitHub Security Lab CTF 1 题目来源 XSS-unsafe jQuery plugins:https://securitylab.github.com/ctf/
题目内容 漏洞代码 fix:https://github.com/twbs/bootstrap/pull/27047
Four such vulnerabilities in Bootstrap jQuery plugins were fixed in this pull request . MITRE has issued the following CVEs for the vulnerabilities: CVE-2018-14040 , CVE-2018-14042 , CVE-2018-20676 , and CVE-2018-20677 .
修复前漏洞版本:3.3.7
1 2 3 4 > git clone <https://github.com/twbs/bootstrap.git> > cd bootstrap > git tag -l > git checkout v3.3.7
前置 - 漏洞分析 01:CVE-2018-14040
🛕 Bootstrap 的 collapse 插件在处理 data-parent 属性时不安全,导致 XSS。
Source :分析 collapse.js 文件中的 Plugin 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Plugin (option ) { return this .each (function ( ) { var $this = $(this ) var data = $this.data ('bs.collapse' ) var options = $.extend ( {}, Collapse .DEFAULTS , $this.data (), typeof option == 'object' && option ) if (!data && options.toggle && /show|hide/ .test (option)) options.toggle = false if (!data) $this.data ('bs.collapse' , (data = new Collapse (this , options))) if (typeof option == 'string' ) data[option]() }) }
其中 options 那张后面的 $this.data() 会获取所有 data-* 属性,包括 data-parent(这是易受 DOM 型 XSS攻击的 source)
$.extend 将这些属性合并到了 options 变量中。 所以此时,options.parent 就变成了 "攻击代码"。
Data Flow: 构造函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var Collapse = function (element, options ) { this .$element = $(element) this .options = $.extend ({}, Collapse .DEFAULTS , options) this .$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + '[data-toggle="collapse"][data-target="#' + element.id + '"]' ) this .transitioning = null if (this .options .parent ) { this .$parent = this .getParent () } else { this .addAriaAndCollapsedClass (this .$element , this .$trigger ) } if (this .options .toggle ) this .toggle () }
通过 extend,写进options.parent ,在下面的逻辑中进入 if ,然后调用this.getParent()。
Sink: getParent()
1 2 3 4 5 6 7 8 9 Collapse .prototype .getParent = function ( ) { return $(this .options .parent ) .find ('[data-toggle="collapse"][data-parent="' + this .options .parent + '"]' ) .each ($.proxy (function (i, element ) { var $element = $(element) this .addAriaAndCollapsedClass (getTargetFromTrigger ($element), $element) }, this )) .end () }
其中的 $(this.options.parent) 是关键漏洞所在
$() 是 jQuery 的核心函数。
jQuery $() 的特性 : 如果传入一个字符串,jQuery 会尝试判断它是 CSS 选择器 还是 HTML 代码 。
如果字符串是 #myId,它会查找 ID 为 myId 的元素。
如果字符串是 <img src=x onerror=alert(1)>(以 < 开头),jQuery 会 创建这个 DOM 元素 并在浏览器中渲染它!
可以发现整体链路为:
source :将 $this.data()【插件中作为可控变量,开发者开发不当可能导致用户可控】写进 $options 变量
data flow :$options 中如果存在 data-parent,就会调用 getparent 函数
sink :getparent 中存在$(this.options.parent),使得 data-parent 的值有可能被当做 JS,使得 jQuery 去创建 DOM 元素并渲染导致 DOM 型 XSS。
02:CVE-2018-14042
🛕 In Bootstrap before 4.1.2, XSS is possible in the data-container property of tooltip.
Source :依旧用户可控的 data-container。
Sink:
1 2 3 4 this .options .container ? $tip.appendTo (this .options .container ) : $tip.insertAfter (this .$element )
这里 this.options.container 是用户可控的字符串。 当它被传递给 jQuery 的 appendTo() 函数时,jQuery 内部会自动执行。
03:CVE-2018-20676 Sink: data-viewport
1 2 3 4 5 6 7 8 9 10 11 12 13 Tooltip .prototype .init = function (type, element, options ) { this .enabled = true this .type = type this .$element = $(element) this .options = this .getOptions (options) this .$viewport = this .options .viewport && $($.isFunction (this .options .viewport ) ? this .options .viewport .call (this , this .$element ) : (this .options .viewport .selector || this .options .viewport )) this .inState = { click : false , hover : false , focus : false }· }
其中处理 $viewport 变量的时候做了针对 this.options.viewport (即为用户可控输入的 source)的判断,如果不是 function,则三元判断进入 false,前面 selector 返回 undefined,逻辑或后面返回值,最后拼接为: $(this.options.viewport)
04:CVE-2018-20677
🛕 In Bootstrap before 3.4.0, XSS is possible in the affix configuration target property.(affix 配置目标属性)
Source & sink :
1 2 3 4 5 6 7 8 9 10 11 12 13 var Affix = function (element, options ) { this .options = $.extend ({}, Affix .DEFAULTS , options) this .$target = $(this .options .target ) .on ('scroll.bs.affix.data-api' , $.proxy (this .checkPosition , this )) .on ('click.bs.affix.data-api' , $.proxy (this .checkPositionWithEventLoop , this )) this .$element = $(element) this .affixed = null this .unpin = null this .pinnedOffset = null this .checkPosition () }
其中的 target 变量来源,直接由 $(this.options.target) 赋值。当变量为恶意 JS 代码时,会进行渲染从而被攻击。
漏洞总结 Source 在 jQuery 这个框架定义的 插件 中,由于 插件配置项 不安全导致的 XSS 作为 Source。也就是所有定义 JQuery 插件中传入的 options 参数(通常由开发者或用户可控的参数组成)。
jQuery 插件是通过为 $.fn 对象分配属性来定义的,如:$.fn.copyText = function() { ... }。为了查找形如这种的插件定义,查询语句应该寻找一个赋值,其中右侧是一个函数,左侧是一个链式属性访问。示例:
1 2 3 4 $.fn.collapse = function(options) { // <-- 我们认为这个 options 参数是 Source this.options = $.extend({}, defaults, options); $(this.options.parent); // 如果能流到这里,就是漏洞 }
查询代码如下:
1 2 3 4 5 6 7 override predicate isSource(DataFlow::Node source) { source = jquery() .getAPropertyRead("fn") // 读取 $.fn .getAPropertySource() // 获取被赋值的值 .(DataFlow::FunctionNode) // 确保它是函数 .getLastParameter() // 拿到最后一个参数 }
同时,还有一些高级插件定义,如 let fn = $.fn; let f = function() { ... }; fn.copyText = f;我们的查找应该也能找到这类写法的定义,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 import javascript from DataFlow::FunctionNode fn, DataFlow::PropWrite jqdotfndotx, DataFlow::PropRead jqdotfn, DataFlow::Node intermediate, DataFlow::SourceNode jq where jq = jquery() and jqdotfn = jq.getAPropertyRead("fn") and jqdotfn.flowsTo(intermediate) and jqdotfndotx.getBase() = intermediate and fn.flowsTo(jqdotfndotx.getRhs()) select jqdotfndotx, fn
Data Flow 可能在 Source 之后并不会直接触发存在危险的函数,需要调用一些其他函数然后触发,甚至可能在传输图中会经过一些过滤正则函数导致漏洞不生效,这里需要在 CodeQL 中把 Data Flow 考虑完备。在本例中导入污点追踪模块 TaintTracking,实现标准的 TaintTracking::Configuration 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import javascript import DataFlow::PathGraph class Configuration extends TaintTracking::Configuration { Configuration() { this = "XssUnsafeJQueryPlugin" } override predicate isSource(DataFlow::Node source) { ··· } // TODO override predicate isSink(DataFlow::Node sink) { ··· } // TODO } from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink where cfg.hasFlowPath(source, sink) select sink.getNode(), source, sink, "Potential XSS vulnerability in plugin."
Additional TaintStep 官方补充道:如果在一个类(或构造函数)的实例里,有人写了 this.options = tainted(属性写),然后后面又读了 this.options(属性读),那污点应该传过去。例如:
1 2 3 4 5 6 function Collapse (element, options ) { this .options = $.extend ({}, defaults, options); } Collapse .prototype .getParent = function ( ) { return $(this .options .parent ); }
在这种情况下,也就是分析时发现的通过 extend 将 options 传递过去的情况,如果直接通过污点传播,codeql 无法找到这类链路,需要重写 isAdditionalTaintStep
1 2 3 4 5 6 override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node sink) { exists(DataFlow::ClassNode cn, string p | cn.getAnInstanceReference().getAPropertyWrite(p).getRhs() = src and cn.getAnInstanceReference().getAPropertyRead(p) = sink ) }
Sink Sink 则是对应的 Source 传入的变量经过一系列 Data Flow 后传入一个危险的(会被 jQuery作为 JS 渲染、创建 DOM 等),对应的函数可能有:
HTML 解析器 :$()( 本例子中最核心)、jQuery()。在 codeql中可以直接用JQuery::MethodCall call 查找。
DOM 插入方法 (如果参数被作为 HTML 解析):appendTo(), prependTo(), insertAfter(), html(), append() 等。(text()不算,因为它只单纯处理文本)。codeql中存在call.interpretsArgumentAsHtml() 可以直接覆盖那些会把参数当做 HTML 解析的函数。
因此对应的代码:
1 2 3 override predicate isSink(DataFlow::Node sink) { exists(JQuery::MethodCall call | call.interpretsArgumentAsHtml(sink)) }
CodeQL 设计 Source Code by official 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /** * @name Cross-site scripting vulnerable plugin * @kind path-problem * @id js/xss-unsafe-plugin */ import javascript import DataFlow::PathGraph class Configuration extends TaintTracking::Configuration { Configuration() { this = "XssUnsafeJQueryPlugin" } override predicate isSource(DataFlow::Node source) { source = jquery() .getAPropertyRead("fn") .getAPropertySource() .(DataFlow::FunctionNode) .getLastParameter() } override predicate isSink(DataFlow::Node sink) { exists(JQuery::MethodCall call | call.interpretsArgumentAsHtml(sink)) } override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node sink) { exists(DataFlow::ClassNode cn, string p | cn.getAnInstanceReference().getAPropertyWrite(p).getRhs() = src and cn.getAnInstanceReference().getAPropertyRead(p) = sink ) } } from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink where cfg.hasFlowPath(source, sink) select sink.getNode(), source, sink, "Potential XSS vulnerability in plugin."
command
创建数据库
1 > codeql database create ./codeql-custom/db --language=javascript-typescript --source-root=./ --overwrite
配置依赖
针对自定义查询,codeql需要一个 qlpack.yml文件作为模块化包管理机制,给 codeql 读取并配置对应依赖。
cd codeql-custom然后创建 qlpack.yml 文件:
1 2 3 4 name: my-bootstrap-xss-queries version: 1.0 .0 dependencies: codeql/javascript-all: "*"
自动下载 JavaScript 的查询库依赖:
运行查询
1 2 3 4 5 6 7 > codeql query run bootstrap_xss.ql --database=./db --output=results.bqrs > codeql bqrs decode results.bqrs --format=csv --output=results.csv
参考
https://securitylab.github.com/ctf/jquery/
https://medium.com/@qazbnm456/the-journey-of-codeql-part-2-655cd3dbb60b
https://paraschetal.in/write-up/2020/01/28/github-security-lab-ctf.html