比赛做题合集喵
[TOC]
青少年CTF S1 · 2026 公益赛
web
1.0 easy_php
题目➡
在这个简单的网页后端中,似乎没有任何危险的函数直接暴露。你能让它“发出声音”,拿到根目录下的 /flag 吗?
1 |
|
打开后审计php➡
看到Monitor类,如果将$status的值设为danger来满足if条件,再将$reporter设为Screen的一个对象,这样当__destruct()结束之后,
就会触发Screen类里的alert(),然后让它替我们格式化输出我们要的根目录下的flag
注:在 PHP 中,如果一个变量名后接括号,它会被当作函数调用
因此如果我们把$format设置为system,把$content设置为tac /fl*,就得到了flag
然后是以GET传参的方式输入,并且过滤了flag,我们可以考虑fl*
大概看懂了之后,我们来编写一下代码➡
1 |
|
得到O%3A7%3A%22Monitor%22%3A2%3A%7Bs%3A15%3A%22%00Monitor%00status%22%3Bs%3A6%3A%22danger%22%3Bs%3A17%3A%22%00Monitor%00reporter%22%3BO%3A6%3A%22Screen%22%3A2%3A%7Bs%3A7%3A%22content%22%3Bs%3A7%3A%22cat+%2Ff%2A%22%3Bs%3A6%3A%22format%22%3Bs%3A6%3A%22system%22%3B%7D%7D
然后GET传参即可
这里需要注意的是,Gloria一开始为了方便,把reporter之类的全设置成了public,结果服务端的源码是private,出现了键名对不上的问题
得到flag
2.0 Serialization
看起来又是一道反序列化的题目喵
题目➡
PHP Web 应用里的 FileCache 类被反序列化触发写文件时,非要在内容开头加句exit;搞破坏,导致代码没法执行,快想办法躲开这个 “拦路虎”
1 |
|
我们继续审计➡
这道题目换成了__toString()函数,我们看到echo unserialize($payload);这里,如果尝试echo一个对象的时候,php会自动调用该对象的__toString()魔术方法,当这个对象被当作字符串时,他会调用$this->handler->process()
然后看到return $this->handler->process();这段代码,我们可以把$handler换成任何拥有process()方法的类,这样就跳转到了FileCache类
来看FileCache类,他将$security_header和$this->content拼接,写到$filepath指定的文件中($path),但是他在$security_header里面加入了一段exit()代码,意味着木马写入也不会执行。
SystemStatus类里面如果./system_config.php文件存在,就将其包含
思路➡
设置一个AuditLog类的对象,然后将其中的属性$handler设置为FileCache 对象,这样就把火引到了FileCache 里面。
然后进行exit()的绕过,如果把$filePath设置为php://filter/write=convert.base64-decode/resource=shell.php那么file_put_contents 在写入的时候会把内容进行base64解码再保存
而exit部分被解码后,非base64字符(<、?、>、(、)、"、:、空格)会被忽略
这样就剩下了phpexitAccessDeniedProtectedCache32个字符,刚好可以被完整解码
然后我们只需要在$content开头补两个字符(对齐base64解码位数的补位),再接上base64编码后的木马,就能绕过
我们来编写脚本➡
1 |
|
阿巴阿巴……得到Cache Saved.了,但为什么一直是乱码😭
3.0 silent_logger
题目打开➡
沉默的日志……emmmm
拿到这道题目,在一个较为真实的环境中,首先去考虑它到底考察什么知识点
是SQL注入?SSTI?或者是日志注入/文件包含?
如果是SSTI的话➡我们尝试输入{{7*7}}以及{{config}}(如果是Flask)或者{{_self}}(如果是Twig)
如果是SQL注入的话➡我们尝试输入1'OR 1=1#及其他变体(比如这道题窝就用到了1'OR 1=1--)
因为测试其他的都没有响应,但是这个有,所以看起来很像是SQL注入
所以我们进一步使用联合查询尝试
再次输入1'UNION SELECT 1,2,3-- -(因为图片中回显了ID、用户名、邮箱,所以先尝试3列)
页面回显了123,因此确定这是一个联合注入的题目,接下来的操作就很熟悉了,就是查库,查表,查字段,拿flag
查数据库➡1'UNION SELECT 1,database(),3-- -
结果发现,没有database(),因此确定他不是mysql和posrgresql而是sqlite
我们再确认一手
写入:1'UNION SELECT 1,sqlite_version(),3-- -
得到了版本号,就此确定他是sqlite
接下来我们就要去查sqlite的元数据表(Meta-table)
知识点喵➡
sqlite没有information_schema,它所有的数据都存储在sqlite_master的核心表里面
| 表明列 | 类型列 | |
|---|---|---|
| mysql | table_name | name |
| sqlite | table_type | type |
payload:1'UNION SELECT 1,group_concat(name),3 FROM sqlite_master WHERE type='table'-- -
注:name字段储存的是表名
group_concat会把所有结果拼成一个字符串,这样我们可以一次看完所有表
然后我们很明显就看到了flags表
然后我们继续查列名
payload:1'UNION SELECT 1,sql,3 FROM sqlite_master WHERE type='table' AND name='flags'-- -
CREATE TABLE flags ( id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL ):在sqlite中,sqlite_master表里的sql字段记录了当初创建这个表的时候输入的完整SQL命令,因此这段代码的意思是➡
创建一个叫flags的表
括号里是定义了这个表有几列(字段),每一列叫什么,存什么类型的数据:第一列叫id,第二列叫value,而在一般的题目中id通常只是一个序号,真正的flag一般存放在另一个字段里,所以我们会去特别关注value这个字段
这里的关键字段名是value,现在只需要把flags表中的value这一列内容读取出来就可以
payload:1'UNION SELECT 1,value,3 FROM flags-- -
得到flag
(阿巴阿巴…wp写完之后记得再复现一遍喵…)
4.0 时间胶囊留言板
题目➡
你被分配到了一个神秘的 Web 系统测试任务——一个“时间胶囊留言板”。用户可以在这个留言板上留下自己的留言,但留言只有在未来的指定日期才能被解封查看。系统还隐藏了一条特殊信息(FLAG),只有当时间到达后才能显示。
不难看出,系统留言下面那一串*就是我们要得到的flag
emmmm……不知道是要考什么,我们先ctrl U看一下源码
我们能看到这一段代码
这段代码表明➡
网页先是在浏览器里面使用JS判断现在时间是否大于解封日期,如果时间到了,JS会去请求get_content.php?id=xxx这个接口
如果后端get_content.pgp没有好好检查时间,那么我们大概是可以绕过前端时间的检验的
因此我们直接尝试在URL后面访问get_content.php?id=1
发现完全可以绕过,并且他是下面系统留言的一个,所以我们再去尝试一下id=2,结果就得到了flag喵
使用控制台是一样的喵(不多赘述了)
复盘一下➡
这道题目的重点在于“未来日期才能解封”,因此这暗示存在了一个“判断是否解封”的逻辑(这个刚好在源码中就能看到了)
奥对,所以这道题的漏洞就在于它把校检全放在了前端,但因为浏览器对于用户来说都是完全透明的且可控的,因此用户可以直接绕过JS,而去访问它背后的API接口
但是如果它在后端也加上一个时间判断,比如
1 | // 伪代码示例:更安全的做法 |
那么我们直接访问接口就会失败,我们可能就要去考虑像SQL注入之类的,通过注入强制改变unlock_date的比较结果,或者考虑变量覆盖
修改本地系统时间来绕过前端JS判断(绕过前端)
源码中有一句:const now = new Date();,如果不传参数,默认获取的是用户电脑的当前系统时间,如果电脑显示的时间是2026年4月5日,那么JS就会认为now >= unlockDateObj成立,从而触发fetch请求
(我有尝试了一下这个绕过,emmm一尝试就崩……)
修改cookie中的时间(绕过后端)
这道题目是靠JS判断new Date(),但是很多题会将用户最后访问时间或解封权限存在cookie里面,所以有的时候会考虑修改cookie
(这个没尝试喵,因为这道题目根本没有cookie什么事……)
5.0 CallBack
题目➡
我们有一个简单的 PHP 脚本,负责处理用户输入,并通过回调函数对数组进行操作,然而,这个脚本并未对输入进行严格的过滤。你是否能发现某些细节并利用它来深入了解更多信息?
白盒代码审计喵
这里很明显注意到return array_map($callback, $someArray);,array_map()函数是一个数组函数,它的核心作用是把数组里的每一个元素,以此作为参数传递给前面那个函数去执行
阿巴阿巴……类似C语言中的
1 | //假设输入的callback是my_function |
如果我们传入?callback=system,服务区会依次执行system(0)、system(1)、system(2)、system(3),在linux系统里面,通常没有名为0、1的系统命令,所以这只会导致报错或者无响应,因此直接通过system执行如tac /fl*之类的命令肯定不行,因为参数被限制死了0123
but but but,题目说,利用它来深入了解更多信息
那么,有没有哪个php内置
函数,只要随便给他传入一个整数参数(或者不需要参数),就可以显示大量的系统配置、环境变量和敏感信息?
出来吧,皮卡丘!好🍭,对不起orz
这时我们就想到了➡phpinfo
phpinfo(1)、phpinfo(2)等在php中都是合法的调用,它会打印出php的完整环境信息页面
好吧,也是直接得到了flag
知识点喵➡回调注入
在php中,很多内置函数(比如array_map、filter_var、usort)支持传入另一个函数的名字作为参数,这个被传入的函数就叫做回调函数
如果开发者直接把用户的输入($_GET['callback'])作为回调函数的名称,没有进行白名单校检,境相当于给了攻击者调用任意函数的权力
那么,为什么选phpinfo?
在web审计题目中,如果发现自己可以控制函数名,但是无法控制参数内容,那么寻找无参或者参数无关的函数是唯一出路
| 函数 | |
|---|---|
phpinfo |
无论传什么参数,都会打印环境信息,flag经常藏在environment或者PHP Variables板块中 |
var_dump |
可以用来打印变量 |
print_r |
也是用来打印环境变量的 |
如果phpinfo没给flag的话,我们会去尝试
| 函数 | |
|---|---|
get_defined_vars |
获取所有定义的变量 |
scandir |
如果参数可控,可以用来列目录 |
assert |
在旧版本php中,assert甚至可以像eval一样执行任意php代码 |
6.0 preg_replace
题目➡
简简单单preg_replace
1 |
|
打开一看好简单的代码喵……
审计一下
也…注意到preg_replace()函数,以及/e漏洞
知识点喵➡preg_replace()函数
preg_replace(正则,替换内容,输入),这是php的正则替换函数
/(.*)/是正则表达式,其中.*匹配任意字符,()代表捕获组
\\1代表引用第一个捕获组的内容,也就是输入的$input
关键点在/e修饰符:在旧版本php中,/e代表eval,它的逻辑是先进行正则匹配,然后把替换后的内容当作php代码执行一遍,最后把执行结果替换回去
所以,这行代码的意思说白了就是执行$data变量里写入的内容,我们要做的就是RCE
所以我们下意识构造payload:?data=system('ls /');
但是发现报错了,原因是出现了\反斜杠。在preg_replace的/e模式中,它会自动给'单引号或者"双引号加上\转义符号
引号被转义的绕过➡
- 使用
${}动态调用
payload:?data=${system(ls)}(注意:这里的ls没加引号,php会把它当成一个未定义的常量,但在没开启严格模式的情况下,他会自动降级为字符串"ls")
- 使用反引号
在linux命令执行中,反引号也可以执行命令,且不需要引号包裹参数
payload:echo`ls /`;
- ⭐二次传参!!!
data会被转义,那么我们就再传一个参数啊!
payload:?data=system($_GET[a])&a=ls /
看到flag了喵
直接tac!
7.0 答案之书
题目➡
传闻世间有一本《答案之书》,能解众生心中困惑。你只需虔诚地递上你的疑问,它便会给予你命运的指引。
然而,书页之间似乎隐藏着某种古老的禁制,唯有避开那些“禁忌之语”,方能窥见真实的奥秘。
万物皆有裂痕,那是光照进来的地方。你能否在禁忌的边缘,寻得那最终的真相(Flag)?
很明显看到URL上面有一个GET传参(而且…有没有发现这道题目的背景很漂亮…?)
然后我一开始以为是RCE,因为我发现它ban掉了system和eval函数
然后我尝试了一堆绕过比如passthru或者反引号之类的(取反都试过了喵……),我发现这些绕过没有被禁用,但是也没有发挥它该有的作用
这道题的思路和3.0很像啊其实(
不是rce,也不是sql,那是……?
其实是SSTI喵
考虑到可能是SSTI,因此去尝试了{{7*7}},得到了49!
因此,这是典型的SSTI(虽然最后还是要执行RCE)
接下来我们就要去确定这到底是什么模板引擎(Jinja2, Twig, Smarty, Mako),来确定最终的payload
啊…SSTI我还没学……这个周学习一下喵(超小声)
我们输入payload:${7*7},发现并没有什么响应
但是,当我们输入:{{7*'7'}}的时候,会发现出现了7777777
在编程语言中,能把数组和字符串相乘,并且结果是把字符串重复N遍的,最经典的就是python
因此,这其实是一个跑在python flask框架上的网站,使用的模板引擎是Jinja2
因此我们现在就是要利用python的魔术方法/面向对象,来达到RCE的目的
找一个基础对象(比如一个空字符串''或者空元组())➡
找它的类(使用__class__,得到<class 'str'>)➡
找所有类的基类(使用__base__,爬到所有类的基类)➡
找出所有类(使用__sublasses__(),这会列出当前python环境里加载的所有类)➡
找到像os._wrap_close 或者 subprocess.Popen 这样能执行命令的类,然后调用它们
所以payload为:?question={{().__class__.__base__.__subclasses__()}}
但是发现被过滤了,所以这道题目依旧需要绕过WAF
- 敏感关键词:
class、base、subclasses、os、popen、import - 敏感符号:
.点好用于调用属性、_用于魔术方法、[或]
绕过➡
字符串拼接
如果waf只是单纯检查有没有class这个连续单纯,我们可以把它拆开
正常:
().__class__如果不用电好,可以用字典形式()['__class__']拼接:
()['__cla'+'ss__']十六进制/unicode编码(对抗下划线和字母)
在Jinja2模板的字符串里,我们可以直接使用\X加上十六进制编码来表示字符
_的十六进制是\x5fc是\x63所以
__class__可以写成'\x5f\x5fclass\x5f\x5f'
利用latter过滤器(对抗点号
.)如果点号
.被过滤了,Jinja2 提供了原生过滤器attr()来获取属性:正常:
().__class__绕过:
()|attr('__class__')或者()|attr('\x5f\x5fclass\x5f\x5f')
???怎么能一直在过滤啊???我已急哭我已急哭我已急哭
跑路了……做表去惹
Leave a comment