路径穿越漏洞的原理与防护

admin 2024年10月13日14:55:04评论49 views字数 8710阅读29分2秒阅读模式

一、基本介绍

路径穿越漏洞(Path Traversal Vulnerability)是一种安全漏洞,攻击者利用该漏洞可以访问系统中本不应被访问的文件或目录。通过构造特定的输入,攻击者可以绕过应用程序的安全机制,访问敏感文件或执行未授权的操作。

1.1 原理

路径穿越通常利用了文件路径的相对引用特性。攻击者可以在输入中使用特殊字符(如 ../..\)来导航到父目录,从而访问不应该被访问的文件。例如,如果一个Web应用程序允许用户下载文件,攻击者可以输入 ../../etc/passwd,试图获取系统的密码文件。

1.2 漏洞示例

假设有一个Web应用程序接受用户输入的文件名来读取文件并返回内容。如果没有对输入进行适当的验证,攻击者可以提交一个包含路径穿越字符的文件名,从而获取到服务器上的任意文件。

1.3 影响

路径穿越漏洞可能导致:

  • 敏感数据泄露,例如配置文件、密码文件等。
  • 服务器上的任意文件读取,可能导致系统被入侵。
  • 进一步的攻击,例如通过获取敏感信息进行身份盗用。

二、修复方案

在操作系统中".."代表的是向上级目录跳转,如果程序在处理到诸如用 ../../../../../etc/passwd 的文件名时没有进行防护,则会跳转出当前工作目录,跳转到到其他目录中;从而返回系统敏感文件给用户。其危害为泄漏源码、泄漏系统敏感文件。

再此,建议统一编写一个校验公共函数,统一命名为PathTraversalValidator, 其他项目默认使用该公共函数校验即可。示例代码

2.1 Go代码示例

// bad: 任意文件读取
func handler(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Query()["path"][0]
    // 未过滤文件路径,可能导致任意文件读取
    data, _ := ioutil.ReadFile(path)
    w.Write(data)
    // 对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
    data, _ = ioutil.ReadFile(filepath.Join("/home/user/", path))
    w.Write(data)
}

// bad: 任意文件写入
func unzip(f string) {
    r, _ := zip.OpenReader(f)
    for _, f := range r.File {
        p, _ := filepath.Abs(f.Name)
        // 未验证压缩文件名,可能导致../等路径穿越,任意文件路径写入
        ioutil.WriteFile(p, []byte("present"), 0640)
    }
}


// good: 检查压缩的文件名是否包含..路径穿越特征字符,防止任意写入
func unzipGood(f string) bool {
    r, err := zip.OpenReader(f)
    if err != nil {
        fmt.Println("read zip file fail")
        return false
    }
    for _, f := range r.File {
        if !strings.Contains(f.Name, "..") {
            p, _ := filepath.Abs(f.Name)
            ioutil.WriteFile(p, []byte("present"), 0640)
        } else {
            return false
        }
    }
    return true
}

2.2 Java代码示例

// bad
@RequestMapping("/path/delete")
public void delete(HttpServletRequest request, HttpServletResponse response) {
    String filePath = request.getParameter("path");
    File file = new File(filePath);   // 文件全路径由客户端传入,禁止
    file.delete();
}

// good
@RequestMapping("/path/download")
public void download(HttpServletRequest request, HttpServletResponse response) {
    String fileName = request.getParameter("name");
    String DIR = "/data/file/upload/";  //文件服务器映射目录,非web目录
    /*
    *  防护方法:判断用户输入文件名是否存在..,存在则可能导致跨目录
    */

    if(fileName.contains("..")) {
        return;
    }
    File file = new File(DIR + fileName);
    try {
        InputStream inputStream = new FileInputStream(file);
        OutputStream out = response.getOutputStream();
        byte[] b = new byte[100];
        int len;
        while ((len = inputStream.read(b)) > 0) {
            out.write(b, 0, len);
        }
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// good
@RequestMapping("/path/upload")
public void safe_upload(@RequestParam(value="file") MultipartFile file) throws  IOException {
    String fileName = file.getOriginalFilename();
    String DIR = "/data/file/upload/";  //文件服务器映射目录,非web目录
    String filePath = DIR + fileName;
    File tmpFile = new File(filePath);
    /*
    *  防护方法:File对象获取绝对路径,进行startwith判断
    */

    if(!tmpFile.getCanonicalPath().startsWith(DIR)) {
        throw new IOException();        // 这里return也可以
    }
    try {
        InputStream in = file.getInputStream();
        FileUtils.copyInputStreamToFile(in, tmpFile);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// good
@RequestMapping("/path/delete")
public void safe_delete(HttpServletRequest request) {
/*
    *  防护方法:判断用户输入的文件后缀名是否在白名单中,是的话执行下一步操作
    */

String webRootPath = request.getSession().getServletContext().getRealPath("/");
String fileName = request.getParameter("name");
if(fileName.contains("..")) {
    return;
}
int pos = fileName.lastIndexOf(".");
String ext = fileName.substring(pos);
String whiteExt = ".jpg.jpeg.png.gif.bmp";   // 文件类型白名单,根据具体情况而定
if(whiteExt.contains(ext)) {
    new File(webRootPath + fileName).delete();
}
}

// good
@RequestMapping("/path/upload")
public void safe_upload(@RequestParam(value="file") MultipartFile file) throws  IOException {
    String fileName = file.getOriginalFilename();
    String ext = fileName.substring(fileName.lastIndexOf("."));
    String DIR = "/data/file/upload/";  //文件服务器映射目录,非web目录
    String fileName = UUID.randomUUID().toString();  // 文件名为随机字符串
    String filePath = DIR + fileName + ext;
    File tmpFile = new File(filePath);
    try {
        InputStream in = file.getInputStream();
        FileUtils.copyInputStreamToFile(in, tmpFile);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.3 Node.js代码示例

const fs = require("fs");
const path = require("path");
let filename = req.query.ufile;
let root = '/data/ufile';


// bad:未检查文件名/路径
fs.readFile(root + filename, (err, data) => {
  if (err) {
    return console.error(err);
  }
  console.log(
    异步读取: ${data.toString()}
  );
});


// bad:使用path处理过后的路径参数值做校验,仍可能有路径穿越风险
filename = path.join(root, filename);
if (filename.indexOf("..") < 0) {
  fs.readFile(filename, (err, data) => {
    if (err) {
      return console.error(err);
    }
    console.log(data.toString());
  });
};


// good:检查了文件名/路径,是否包含路径穿越字符
if (filename.indexOf("..") < 0) {
  filename = path.join(root, filename);
  fs.readFile(filename, (err, data) => {
    if (err) {
      return console.error(err);
    }
    console.log(data.toString());
  });
};

2.4 PHP代码示例

// bad:未检查文件名/路径

if(isset(
  $_GET['filename'])){
  $
    path = "/var/www/html/" . 
    $_GET['filename'];
  echo file_get_contents($
                         path);
}


// good:检查了文件名/路径,是否包含路径穿越字符
if(isset(
  $_GET['filename'])){
  $
    path = "/var/www/html/" . 
    $_GET['filename'];
  if(strpos($
            path, '..') === false){
    echo file_get_contents($path);
  }else{
    echo "filename is not valid";
  }
}

2.5 Python代码示例

// good
import os

ALLOWED_EXTENSIONS = ['txt','jpg','png']

def allowed_file(filename):
    if ('.' in filename and
        '..' not in filename and
        os.path.splitext(filename)[1].lower() in ALLOWED_EXTENSIONS):

        return filename
    return None

// good
import os
upload_dir = '/tmp/upload/' # 预期的上传目录
file_name = '../../etc/hosts' # 用户传入的文件名
absolute_path = os.path.join(upload_dir, file_name) # /tmp/upload/../../etc/hosts
normalized_path = os.path.normpath(absolute_path) # /etc/hosts
if not normalized_path.startswith(upload_dir): # 检查最终路径是否在预期的上传目录中
    raise IOError()

三、延伸拓展

在大部分上传文件的业务场景中,部分后端会使用前端传入的文件名,这时就容易出现路径穿越风险。但是有些语言框架层在文件上传时,会对文件名做规范化处理,可以有效防止风险。

3.1 Java

如以下代码路径穿越漏洞的原理与防护

在Java Spring Boot中,默认情况下,并没有对MultipartFile的文件名进行路径过滤,包括像../..这样的相对路径。这是因为Spring框架通常更倾向于提供开发者更大的灵活性和自由度,以便他们根据实际需求来自定义和控制文件上传的行为。在文件上传的过程中,Spring框架不会默认执行严格的文件名过滤,因为这可能会限制一些合法的文件名,对于某些特定的业务场景可能不适用。相对路径的问题通常是由于不正确的文件路径处理而产生的,因此开发者需要自行确保文件上传的安全性。

正确写法,判断文件名是否包含..,或者直接对文件名重新命名做md5,不使用用户上传的文件名。

@RestController
public void safe_delete(HttpServletRequest request) {
/*
    *  防护方法:判断用户输入的文件后缀名是否在白名单中,是的话执行下一步操作
    */

String webRootPath = request.getSession().getServletContext().getRealPath("/");
String fileName = request.getParameter("name");
if(fileName.contains("..")) {
    return;
}
int pos = fileName.lastIndexOf(".");
String ext = fileName.substring(pos);
String whiteExt = ".jpg.jpeg.png.gif.bmp";   // 文件类型白名单,根据具体情况而定
if(whiteExt.contains(ext)) {
    new File(webRootPath + fileName).delete();
}
}
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>

CommonsMultipartFile 中的 getOriginalFilename 方法对文件名进行了处理

3.2 Go

对于Go语言标准库中的mime/multipart包,在处理文件上传时,会自动规范化文件路径,这意味着路径中的..会被解析并去除,以防止路径遍历攻击。

curl -X POST -F "imgFile=@/Users/admin/Desktop/test.txt;filename=./../xe.txt" http://localhost:9000/api/upload
func uploadFile(w http.ResponseWriter, r *http.Request) {

    file, handler, err := r.FormFile("imgFile")
    if err != nil {
        fmt.Println("Error retrieving the file")
        fmt.Println(err)
        return
    }
    defer file.Close()

    // 创建一个新文件来保存上传的文件
    f, err := os.OpenFile("uploadFile/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println("Error creating the file")
        fmt.Println(err)
        return
    }
    defer f.Close()

    // 将上传的文件内容复制到新文件中
    _, err = io.Copy(f, file)
    if err != nil {
        fmt.Println("Error copying file contents")
        fmt.Println(err)
        return
    }

    fmt.Fprintf(w, "File uploaded successfullyn")
}
路径穿越漏洞的原理与防护

类似的Gin框架会调用Go语言标准库中的mime/multipart包来解析form-data请求体,该包会自动处理文件名中的相对路径符号,确保最终的文件名只包含合法的字符。

路径穿越漏洞的原理与防护
// FileName returns the filename parameter of the Part's Content-Disposition
// header. If not empty, the filename is passed through filepath.Base (which is
// platform dependent) before being returned.
func (p *Part) FileName() string {
    if p.dispositionParams == nil {
        p.parseContentDisposition()
    }
    filename := p.dispositionParams["filename"]
    if filename == "" {
        return ""
    }
    // RFC 7578, Section 4.2 requires that if a filename is provided, the
    // directory path information must not be used.
    return filepath.Base(filename)
}

原文始发于微信公众号(SDL安全):路径穿越漏洞的原理与防护

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月13日14:55:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   路径穿越漏洞的原理与防护https://cn-sec.com/archives/3261653.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息