10

代码审计--osroom

 3 years ago
source link: https://misakikata.github.io/2020/11/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1-osroom/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

osroom

这个cms很有意思,从漏洞和程序的写法上,很适合用来入门学习,漏洞的一些形式相比来说,也比较多一点。

apps\utils\format\obj_format.py

如下,文件中采用了eval来转换字符串对象,当json.loads转换失败的时候,则直接使用eval来转换。

  1. def json_to_pyseq(tjson):
  2. """
  3. json to python sequencer
  4. :param json:
  5. :return:
  6. """
  7. if tjson in [None, "None"]:
  8. return None
  9. elif not isinstance(tjson, (list, dict, tuple)) and tjson != "":
  10. if isinstance(tjson, (str, bytes)) and tjson[0] not in ["{", "[", "("]:
  11. return tjson
  12. elif isinstance(tjson, (int, float)):
  13. return tjson
  14. try:
  15. tjson = json.loads(tjson)
  16. except BaseException:
  17. tjson = eval(tjson)
  18. else:
  19. if isinstance(tjson, str):
  20. tjson = eval(tjson)
  21. return tjson

转到一个使用此方法的功能,例如apps\modules\audit\process\rules.py

删除规则处,传入一个ids参数,原参数值是一个hash值,但是可以修改为python代码。

  1. def audit_rule_delete():
  2. ids = json_to_pyseq(request.argget.all('ids', []))
  3. if not isinstance(ids, list):
  4. ids = json.loads(ids)
  5. for i, tid in enumerate(ids):
  6. ids[i] = ObjectId(tid)
  7. r = mdbs["sys"].db.audit_rules.delete_many({"_id": {"$in": ids}})
  8. if r.deleted_count > 0:
  9. data = {"msg": gettext("Delete the success,{}").format(
  10. r.deleted_count), "msg_type": "s", "custom_status": 204}
  11. else:
  12. data = {
  13. "msg": gettext("Delete failed"),
  14. "msg_type": "w",
  15. "custom_status": 400}
  16. return data

参数POC: {123:__import__('os').system('whoami')},查看终端输出。

image-20201112124711645
image-20201112121033287

只要涉及到ids参数的都存在此问题,比如另一个类别删除功能。

image-20201112133809406

在用户登陆的判断中,也对传入的参数code_url_obj执行了此方法,所以存在一个前台的RCE

apps\modules\user\process\online.py

  1. code_url_obj = json_to_pyseq(request.argget.all('code_url_obj', {}))
image-20201112162057637

apps\utils\upload\file_up.py

ps: 此问题没有复现,理论上存在。

代码描述了一种上传typroa图像base64后处理来保存写入文件的方式,其中后缀是解析typroa图像base64开头得到,例如

data:image/jpg;base64,获得后缀为jpg,在后续的文件明拼接中,文件名被以时间戳和UUID重写构造,但是后缀可控,可以写入..\..\形式的遍历data:image/jpg\..\..\..\..\tmp;base64

  1. def fileup_base_64(uploaded_files, file_name=None, prefix=""):
  2. """
  3. 文件以base64编码上传上传
  4. :param uploaded_files: 数组
  5. :param bucket_var: 保存typroa图像服务器空间名的变量名, 如AVA_B
  6. :param file_name:
  7. :return:
  8. """
  9. if not uploaded_files:
  10. return None
  11. keys = []
  12. for file_base in uploaded_files:
  13. if file_base:
  14. # data:image/jpeg
  15. file_format = file_base.split(";")[0].split("/")[-1]
  16. imgdata = base64.b64decode(file_base.split(",")[-1])
  17. if file_name:
  18. filename = '{}.{}'.format(file_name, file_format)
  19. else:
  20. filename = '{}_{}.{}'.format(
  21. time_to_utcdate(
  22. time_stamp=time.time(),
  23. tformat="%Y%m%d%H%M%S"),
  24. uuid1(),
  25. file_format)

传入后可以造成一种保存文件到其他目录的效果,这种遍历在Linux下是不允许的,但在Windows下可执行,win支持及../..\,还可以文件结尾的回退遍历,所以在Windows下可以造成覆写。

由于兼容性,Windows下有个别的包兼容有问题,并没有复现,附一张Linux的目录构造图

image-20201112155343443

上传文件覆盖

如果上面那个不是很清楚,这个就比较明显了,插件上传功能中。

apps\modules\plug_in_manager\process\manager.py

  1. def upload_plugin():
  2. """
  3. 插件上传
  4. :return:
  5. """
  6. file = request.files["upfile"]
  7. file_name = os.path.splitext(file.filename) #('123','.zip')
  8. filename = os.path.splitext(file.filename)[0] #123
  9. extension = file_name[1] #.zip
  10. if not extension.strip(".").lower() in ["zip"]:
  11. data = {"msg": gettext("File format error, please upload zip archive"),
  12. "msg_type": "w", "custom_status": 401}
  13. return data
  14. if not os.path.exists(PLUG_IN_FOLDER): #osroom/apps/plugins
  15. os.makedirs(PLUG_IN_FOLDER)
  16. fpath = os.path.join(PLUG_IN_FOLDER, filename) ##osroom/apps/plugins/123
  17. if os.path.isdir(fpath) or os.path.exists(fpath):
  18. if mdbs["sys"].db.plugin.find_one(
  19. {"plugin_name": filename, "is_deleted": {"$in": [0, False]}}):
  20. # 如果插件没有准备删除标志
  21. data = {"msg": gettext("The same name plugin already exists"),
  22. "msg_type": "w", "custom_status": 403}
  23. return data
  24. else:
  25. # 否则清除旧的插件
  26. shutil.rmtree(fpath)
  27. mdbs["sys"].db.plugin.update_one({"plugin_name": filename}, {
  28. "$set": {"is_deleted": 0}})
  29. # 保存主题
  30. save_file = os.path.join("{}/{}".format(PLUG_IN_FOLDER, file.filename)) ##osroom/apps/plugins/123.zip
  31. file.save(save_file)

上传文件后分割文件和后缀,判断插件是否存在以及是否清理就插件,在下面保存的时候,直接使用了上传的参数名做拼接,导致可以被跨目录保存,比如文件应该保存到osroom/apps/plugins/下,上传如下

image-20201113142316180

我们在系统查看

image-20201113142402087

apps\modules\user\process\sign_in.py

ps:此问题影响较小,当作分析即可

在代码中存在一个获取值的参数next,这个参数是登陆的时候默认没有存在,可能是为了跳转登陆留下的参数。参数值为任意值的时候,返回的to_url的值就为参数值。

  1. def p_sign_in(
  2. username,
  3. password,
  4. code_url_obj,
  5. code,
  6. remember_me,
  7. use_jwt_auth=0):
  8. """
  9. 用户登录函数
  10. :param adm:
  11. :return:
  12. """
  13. data = {}
  14. if current_user.is_authenticated and username in [current_user.username,
  15. current_user.email,
  16. current_user.mphone_num]:
  17. data['msg'] = gettext("Is logged in")
  18. data["msg_type"] = "s"
  19. data["custom_status"] = 201
  20. data['to_url'] = request.argget.all(
  21. 'next') or get_config("login_manager", "LOGIN_IN_TO")
  22. return data

然后在前端js中apps\admin_pages\pages\sign-in.html

直接获取响应的data的to_url进行跳转,类似于统一登陆中的任意域跳转的问题。

  1. var result = osrHttp("PUT","/api/sign-in", d);
  2. result.then(function (r) {
  3. if(r.data.msg_type=="s"){
  4. window.location.href = r.data/to_url;
  5. }else if(r.data.open_img_verif_code){
  6. get_imgcode();
  7. }
  8. }).catch(function (r) {
  9. if(r.data.open_img_verif_code){
  10. get_imgcode();
  11. }
  12. });

任意文件读取

apps\modules\theme_setting\process\static_file.py

读取静态文件模板的时候,直接使用了请求的参数进行拼接访问,导致可以任意读取文件

  1. def get_static_file_content():
  2. """
  3. 获取静态文件内容, 如html文件
  4. :return:
  5. """
  6. filename = request.argget.all('filename', "index").strip("/")
  7. file_path = request.argget.all('file_path', "").strip("/")
  8. theme_name = request.argget.all("theme_name")
  9. s, r = arg_verify([(gettext("theme name"), theme_name)], required=True)
  10. if not s:
  11. return r
  12. path = os.path.join(
  13. THEME_TEMPLATE_FOLDER, theme_name)
  14. file = "{}/{}/{}".format(path, file_path, filename)
  15. if not os.path.exists(file) or THEME_TEMPLATE_FOLDER not in file:
  16. data = {"msg": gettext("File not found,'{}'").format(file),
  17. "msg_type": "w", "custom_status": 404}
  18. else:
  19. with open(file) as wf:
  20. content = wf.read()
  21. data = {
  22. "content": content,
  23. "file_relative_path": file_path.replace(
  24. path,
  25. "").strip("/")}
  26. return data

构造POC:http://192.168.120.128:5000/api/admin/static/file?file_path=pages/account/settings/../../../../../../../../etc&filename=passwd&theme_name=osr-theme-w

image-20201112174207578

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK