CodeQL_Girhub-Security-Lab-CTF-1

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)
// 重点解释:$.extend
// 这是一个“合并配置”的工具。
// -----------------------------------------------------------
var options = $.extend(
{},
Collapse.DEFAULTS, // 1. 先拿“默认配置” (比如默认是开启的)
$this.data(), // 2. 再拿“HTML标签上的配置” (也就是 data-* 属性) <--- 关键!
typeof option == 'object' && option // 3. 最后拿“代码里手动传的配置”
)

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
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================

var Collapse = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Collapse.DEFAULTS, options) // options.parent 被保存下来
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
this.transitioning = null

if (this.options.parent) { // data-parent 逻辑
this.$parent = this.getParent() // 调用 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 代码

  1. 如果字符串是 #myId,它会查找 ID 为 myId 的元素。
  2. 如果字符串是 <img src=x onerror=alert(1)>(以 < 开头),jQuery 会创建这个 DOM 元素并在浏览器中渲染它!

可以发现整体链路为:

  1. source:将 $this.data()【插件中作为可控变量,开发者开发不当可能导致用户可控】写进 $options 变量
  2. data flow$options 中如果存在 data-parent,就会调用 getparent 函数
  3. 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
// js/tooltip.js 第 207 行
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) ? // 不是function
this.options.viewport.call(this, this.$element) :
(this.options.viewport.selector || //字符串,为 undefined 或(||) this.options.viewport的值
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); // 写 this.options
}
Collapse.prototype.getParent = function() {
return $(this.options.parent); // 读 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 等),对应的函数可能有:

  1. HTML 解析器$()( 本例子中最核心)、jQuery()。在 codeql中可以直接用JQuery::MethodCall call 查找。
  2. 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. 创建数据库

    1
    > codeql database create ./codeql-custom/db --language=javascript-typescript --source-root=./ --overwrite
  2. 配置依赖

针对自定义查询,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
> codeql pack install
  1. 运行查询

    1
    2
    3
    4
    5
    6
    7
    # 运行编写好的 bootstrap_xss.ql
    # --database 指定刚才创建的 codeql_db
    # --output 指定结果输出文件 (.bqrs 格式)
    > codeql query run bootstrap_xss.ql --database=./db --output=results.bqrs

    # 将结果转换为 CSV
    > codeql bqrs decode results.bqrs --format=csv --output=results.csv

参考

  1. https://securitylab.github.com/ctf/jquery/
  2. https://medium.com/@qazbnm456/the-journey-of-codeql-part-2-655cd3dbb60b
  3. https://paraschetal.in/write-up/2020/01/28/github-security-lab-ctf.html