题目
function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}
解题思路
前置知识
相当难的一道综合题型,考察对 Javascript 原理的理解程度,相关知识点如下:
- Object getter/setter 访问器(accessor):Object.prototype.__proto__
- String 正则替换:String.prototype.replace()
代码分析
首先需要清楚代码逻辑,我们逐行分析下。
这里告知了我们输入的 input
格式只能为 JSON(不过 data
这个变量在本题是毫无用处的):
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
然后 input
会与一个固定的 JSON { 'source' : 'http://placehold.it/350x150' }
执行 extend
操作。
这个 extend
函数看似复杂,但其实做的事情很简单:
检测 input
的 JSON 顶层是否具有属性 source
,若有则不对 input
做任何修改。否则则在 input
的 JSON 顶层 添加属性 source
,且取默认值为 http://placehold.it/350x150
。实际上这个函数是没什么用的。
处理后的 input
JSON 对象存储到 config
变量中:
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
继而利用 test
函数正则校验 config
JSON 对象的顶层属性 source
的值,若其值含有 0-9
、 a-z
、 A-Z
、 _
、 :
、 /
、 .
、 以外的字符,则删除 source
属性。
换言之这里是避免我们在顶层属性 source
编写 payload 。
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
即使 config
JSON 对象的顶层属性 source
得以保留,也会把其中的双引号 "
全部过滤。
换言之这行代码是避免我们闭合 JSON 属性。
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
最后把 source
的值作为 <img>
标签的 src
属性值输出到前端。
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
大致的代码逻辑分析完毕,接下来可以开始寻找逻辑缺陷解题。
replace 正则绕过
由于过程略复杂,我们不妨从最终期望的目标开始,反向推导 payload 。
先不管前面代码逻辑如何,最后两行代码是:
var source = config.source.replace(/"/g, '');
return '<img src="{{source}}">'.replace('{{source}}', source);
即我们所构造的 source
值,必须不含双引号 "
,且能够触发 prompt(1)
事件。
当 source
是正常的图片 URL 时,不妨在浏览器控制台调试一下代码,看一下效果:
正常情况下,如果要在 <img>
标签注入 JS ,一般是可以通过诸如 <img src="0" onerror=prompt(1) >
的方式。
但因为双引号 "
被过滤了,我们无法通过闭合 src
的双引号再增加 onerror
属性。
但是根据 JS 的 replace('{{source}}', source)
函数的语法,第二个由我们控制的参数 source
是可以插入特殊变量名以达到某些效果的(详见 这里 ):
而我们要使用的特殊变量名,就是
$` // 这个变量名的效果是 【插入当前匹配的子串左边的内容】。
就这题而言,因为 <img src="{{source}}">'.replace('{{source}}', source)
第一个参数 {{source}}
匹配了原字符串,而所匹配部分的左边内容是 <img src="
,因此若第二个参数 source
含有特殊变量,就会把 <img src="
插入到该特殊变量位置。 注意所插入的到 <img src="
最右侧刚好有一个双引号,那么我们就可以用来闭合 src
属性的双引号了。
于是我们可以构造 source
的值为 :
$` onerror=prompt(1) >
当特殊变量被替换后,实际就等价于 <img src=" onerror=prompt(1) >
,再将其通过 replace
替换到原串的 {{source}}
,就可以得到:
<img src="<img src=" onerror=prompt(1) >">
即 src
属性值等于 "<img src="
,被成功闭合了,同时因为是一个无效值,会触发到 onerror
的 JS 。
JSON 欺骗
那么接下来的问题就是,怎么保留我们所构造的 source
值到最后。
根据前面的分析知道, source
值就是源于我们输入的 json 的 source
属性值。
但是在此之前有这样的一段 test
代码,当 source
值含有 0-9
、 a-z
、 A-Z
、 _
、 :
、 /
、 .
、 以外的字符,则删除 json 的 source
属性:
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
很不幸地,我们构造的 source
值是满足删除标准的。
换言之,若直接 input 的 JSON 如下,是无法把 source
属性值保留到最后的 :
{ "source" : "$` onerror=prompt(1)" }
最直接的想法是,能不能在 JSON 构造两个 source
属性骗过正则校验,使得其中一个没用的 source
被删除,而我们构造的 source
则得以保留。
不过问题是,JSON 是具备 hash 特性的,若直接在同级构造两个同名属性 source
,后者是会覆盖前者的。
在浏览器控制台测试了一下,同名属性果然是会覆盖的:
不过也并非一无所获,从控制台里面注意到,所构造的 JSON 对象具有一个隐藏属性 __proto__
。
特意去查了一下这个属性的作用(详见 这里),得知在 JS 代码中,每个 JSON 对象都具有一个隐藏属性 __proto__
,而这个属性本质上是一个访问器,其作用是当我们需要访问 JSON 对象中的某个属性值时,可以提供类似于 getter
/ setter
访问方法的语法糖。
例如若在 JS 代码中定义一个这样的 JSON 变量 var json = {"source": "exp"}
:
- 当需要访问
source
的属性值时,如:var src = json.source
,实际上是__proto__
的getter
在起作用 - 当需要修改
source
的属性值时,如:json.source = "EXP"
,实际上是__proto__
的setter
在起作用
虽然 __proto__
是一个访问器,不过默认情况下,我们是不可以 json.__proto__.source
这样访问属性的。
但有趣的是,假如在 JSON 中显式设置了 __proto__
属性,例如这样:{"__proto__": {"source": "exp"}}
那么就会给 JS 解析器造成某些“混乱”,使得诸如 json.__proto__.source
的访问属性方式变成可能。
不但如此,此时 JSON 还同时支持 json.source
和 json.__proto__.source
两种访问属性方式,且他们是等价的:
利用这个特点,我们就可以在 JSON 的同级构造两个同名属性。
例如在 JS 中定义这样的一个 JSON 变量 var json = {"source": "EXP", "__proto__": {"source": "M02"}}
当 "source": "EXP"
属性存在时:
json.source
会优先得到EXP
的值json.__proto__.source
会得到全路径M02
的值
当 "source": "EXP"
属性不存在时:
json.source
会通过__proto__
访问器得到M02
的值json.__proto__.source
依旧会得到全路径M02
的值
回到这题,我们可以利用这个 JSON 特性进行欺骗,在 input 构造一个类似这样的 JSON :
input = {"source": "--delete me--", "__proto__": {"source": "payload"}}
其中第一个 source
只需要满足代码中 test
的正则条件使之被删除即可,这样第二个用于 payload 的 source
则可以保留到最后。
完成挑战
结合前面所有分析,最终可以构造 payload 如下,完成挑战:
{"source": "--EXP : Delete Me--", "__proto__": {"source": "$` onerror=prompt(1) >"}}
答案下载
- payload.json : 下载