Posts TokyoWesterns CTF Writeups
Post
Cancel

TokyoWesterns CTF Writeups

urlcheck v1

源码分析

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import os, re, requests, flask
from urllib.parse import urlparse

app = flask.Flask(__name__)
app.flag = '***CENSORED***'
app.re_ip = re.compile('\A(\d+)\.(\d+)\.(\d+)\.(\d+)\Z')

def valid_ip(ip):
	matches = app.re_ip.match(ip)
	if matches == None:
		return False

	ip = list(map(int, matches.groups()))
	if any(i > 255 for i in ip) == True:
		return False
	# Stay out of my private!
	if ip[0] in [0, 10, 127] \
		or (ip[0] == 172 and (ip[1] > 15 or ip[1] < 32)) \
		or (ip[0] == 169 and ip[1] == 254) \
		or (ip[0] == 192 and ip[1] == 168):
		return False
	return True

def get(url, recursive_count=0):
	r = requests.get(url, allow_redirects=False)
	if 'location' in r.headers:
		if recursive_count > 2:
			return '&#x1f914;'
		url = r.headers.get('location')
		if valid_ip(urlparse(url).netloc) == False:
			return '&#x1f914;'
		return get(url, recursive_count + 1) 
	return r.text

@app.route('/admin-status')
def admin_status():
	if flask.request.remote_addr != '127.0.0.1':
		return '&#x1f97a;'
	return app.flag

@app.route('/check-status')
def check_status():
	url = flask.request.args.get('url', '')
	if valid_ip(urlparse(url).netloc) == False:
		return '&#x1f97a;'
	return get(url)

@app.route('/')
def index():
	return '''
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
	<div id="app" class="container">
	  <h1>urlcheck v1</h1>
	  <div class="input-group input-group-lg mb-3">
		<input v-model="url" placeholder="e.g.) http://11.45.148.93/" class="form-control">
		<div class="input-group-append">
		  <button v-on:click="checkStatus" class="btn btn-primary">check</button>
		</div>
	  </div>
	  <div v-if="status" class="alert alert-info">{{ d(status) }}</div>
	</div>
	<script>
	  new Vue({
		el: '#app',
		data: {url: '', status: ''},
		methods: {
		  d: function (s) {
			let t = document.createElement('textarea')
			t.innerHTML = s
			return t.value
		  },
		  checkStatus: function () {
			fetch('/check-status?url=' + this.url)
			  .then((r) => r.text())
			  .then((r) => {
				this.status = r
			  })
		  }
		}
	  })
	</script>
</body>
</html>
'''

经过分析,用户输入框输入的 url 传递给 /check-status?url= 参数,服务端接收到 url ,进行 valid_ip(urlparse(url).netloc) == False 判断,因此只需要绕过 valid_ip() 函数,使得执行后面的 get() 函数即可获得 flag。此外,valid_ip() 函数中对 IP 地址进行了严格过滤,并且在 admin-status() 函数中明确 IP 地址为 127.0.0.1,因此使用IP地址的八进制表示法即可绕过

payload

3

urlcheck v2

源码分析

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import os, re, time, ipaddress, socket, requests, flask
from urllib.parse import urlparse

app = flask.Flask(__name__)
app.flag = '***CENSORED***'

def valid_ip(ip):
	try:
		result = ipaddress.ip_address(ip)
		# Stay out of my private!
		return result.is_global
	except:
		return False

def valid_fqdn(fqdn):
	return valid_ip(socket.gethostbyname(fqdn))

def get(url, recursive_count=0):
	r = requests.get(url, allow_redirects=False)
	if 'location' in r.headers:
		if recursive_count > 2:
			return '&#x1f914;'
		url = r.headers.get('location')
		if valid_fqdn(urlparse(url).netloc) == False:
			return '&#x1f914;'
		return get(url, recursive_count + 1)
	return r.text

@app.route('/admin-status')
def admin_status():
	print('Remote Address:', flask.request.remote_addr)
	if flask.request.remote_addr != '127.0.0.1':
		return '&#x1f97a;'
	return app.flag

@app.route('/check-status')
def check_status():
	url = flask.request.args.get('url', '')
	if valid_fqdn(urlparse(url).netloc) == False:
		return '&#x1f97a;'
	return get(url)

@app.route('/')
def index():
	return '''
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
	<div id="app" class="container">
	  <h1>urlcheck v2</h1>
	  <div class="input-group input-group-lg mb-3">
		<input v-model="url" placeholder="e.g.) http://westerns.tokyo/" class="form-control">
		<div class="input-group-append">
		  <button v-on:click="checkStatus" class="btn btn-primary">check</button>
		</div>
	  </div>
	  <div v-if="status" class="alert alert-info">{{ d(status) }}</div>
	</div>
	<script>
	  new Vue({
		el: '#app',
		data: {url: '', status: ''},
		methods: {
		  d: function (s) {
			let t = document.createElement('textarea')
			t.innerHTML = s
			return t.value
		  },
		  checkStatus: function () {
			fetch('/check-status?url=' + this.url)
			  .then((r) => r.text())
			  .then((r) => {
				this.status = r
			  })
		  }
		}
	  })
	</script>
</body>
</html>
'''

前面部分与 urlcheck v1 相同,不同的地方在于服务器端做了 DNS 查询,并且只支持公网地址,另外想要获取 flag,必须访问 127.0.0.1/admin-status 私有地址。因此该题主要考查的是 DNS Rebinding(DNS重绑定)攻击

Google 查询找到已经部署好的地址,将题目地址的IP地址填在 B

1

payload

2

easy-hash

源码分析

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
import struct
import os

MSG = b'twctf: please give me the flag of 2020'

assert os.environ['FLAG']

def easy_hash(x):
    m = 0
    for i in range(len(x) - 3):
        m += struct.unpack('<I', x[i:i + 4])[0]
        m = m & 0xffffffff

    return m

def index(request):
    message = request.get_data()
    if message[0:7] != b'twctf: ' or message[-4:] != b'2020':
        return b'invalid message format: ' + message

    if message == MSG:
        return b'dont cheet'

    msg_hash = easy_hash(message)
    expected_hash = easy_hash(MSG)
    if msg_hash == expected_hash:
        return 'Congrats! The flag is ' + "HERE IS THE FLAG"
    return 'Failed: easy_hash({}) is {}. but expected value is {}.'.format(message, msg_hash, expected_hash)

分析源码可以知道,只要 easy_hash(message) == easy_hash(MSG) 即可,但是 message 不能和 MSG(twctf: please give me the flag of 2020)字符串相同

此外,easy_hash() 函数将字符串分割为每4个字符一组,循环相加每4个字符的十进制值,最后再判断是否相等。因此修改其中两个字符,使得相加结果不变即可

payload

curl https://crypto01.chal.ctf.westerns.tokyo -d 'twctf: oleate give me the flag of 2020'

This post is licensed under CC BY 4.0 by the author.