百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

一个隐藏在Go语言标准库中的目录穿越漏洞 CVE-2022-29804

toyiye 2024-06-21 12:19 10 浏览 0 评论

前言

一天,我看完了番剧后,闲着无聊审计了一下我用来做内网共享的小工具——"Go HTTP File Server"。
这是一个文件服务器,可以快速搭建http服务器共享文件.
启动的默认路径为当前路径(./)

代码审计

非常安全的代码?

这个工具默认是没有鉴权的 所以我们直接看文件浏览的部分

func (s *HTTPStaticServer) getRealPath(r *http.Request) string {
    path := mux.Vars(r)["path"]
    if !strings.HasPrefix(path, "/") {
        path = "/" + path
    }
    path = filepath.Clean(path) // prevent .. for safe issues
    relativePath, err := filepath.Rel(s.Prefix, path)
    if err != nil {
        relativePath = path
    }
    realPath := filepath.Join(s.Root, relativePath)
    return filepath.ToSlash(realPath)
}


func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
    path := mux.Vars(r)["path"]
    realPath := s.getRealPath(r)
    if r.FormValue("json") == "true" {
        s.hJSONList(w, r)
        return
    }

    if r.FormValue("op") == "info" {
        s.hInfo(w, r)
        return
    }

    if r.FormValue("op") == "archive" {
        s.hZip(w, r)
        return
    }

    log.Println("GET", path, realPath)
    if r.FormValue("raw") == "false" || isDir(realPath) {
        if r.Method == "HEAD" {
            return
        }
        renderHTML(w, "assets/index.html", s)
    } else {
        if filepath.Base(path) == YAMLCONF {
            auth := s.readAccessConf(realPath)
            if !auth.Delete {
                http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
                return
            }
        }
        if r.FormValue("download") == "true" {
            w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
        }
        http.ServeFile(w, r, realPath)
    }
}

乍一看上去,这段代码好像没有什么问题。它使用了 Go 标准库中的 filepath.Clean (去除 ..) 和 filepath.Join(合并路径) 函数,来防止目录穿越。

标准库中的漏洞

我刚好还有些空余时间,所以我又开始检查 Go 标准库中的函数实现。

filepath.Clean

func Clean(path string) string {
    originalPath := path
    volLen := volumeNameLen(path)
    path = path[volLen:]
    if path == "" {
        if volLen > 1 && originalPath[1] != ':' {
            // should be UNC
            return FromSlash(originalPath)
        }
        return originalPath + "."
    }
    rooted := os.IsPathSeparator(path[0])

    // Invariants:
    //  reading from path; r is index of next byte to process.
    //  writing to buf; w is index of next byte to write.
    //  dotdot is index in buf where .. must stop, either because
    //      it is the leading slash or it is a leading ../../.. prefix.
    n := len(path)
    out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
    r, dotdot := 0, 0
    if rooted {
        out.append(Separator)
        r, dotdot = 1, 1
    }

    for r < n {
        switch {
        case os.IsPathSeparator(path[r]):
            // empty path element
            r++
        case path[r] == '.' && r+1 == n:
            // . element
            r++
        case path[r] == '.' && os.IsPathSeparator(path[r+1]):
            // ./ element
            r++

            for r < len(path) && os.IsPathSeparator(path[r]) {
                r++
            }
            if out.w == 0 && volumeNameLen(path[r:]) > 0 {
                // When joining prefix "." and an absolute path on Windows,
                // the prefix should not be removed.
                out.append('.')
            }
        case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
            // .. element: remove to last separator
            r += 2
            switch {
            case out.w > dotdot:
                // can backtrack
                out.w--
                for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) {
                    out.w--
                }
            case !rooted:
                // cannot backtrack, but not rooted, so append .. element.
                if out.w > 0 {
                    out.append(Separator)
                }
                out.append('.')
                out.append('.')
                dotdot = out.w
            }
        default:
            // real path element.
            // add slash if needed
            if rooted && out.w != 1 || !rooted && out.w != 0 {
                out.append(Separator)
            }
            // copy element
            for ; r < n && !os.IsPathSeparator(path[r]); r++ {
                out.append(path[r])
            }
        }
    }

    // Turn empty string into "."
    if out.w == 0 {
        out.append('.')
    }

    return FromSlash(out.string())
}

调试了一遍后,我发现 filepath.Clean 对路径处理非常完美。这个函数可以将路径中的冗余部分去除,同时可以处理不同操作系统下的路径分隔符问题.

filepath.Join

但是 filepath.Join 函数就不太一样了,这个函数在 Plan9、Unix 和 Windows 三个操作系统类型下有着不同的实现。

func join(elem []string) string {
    // If there's a bug here, fix the logic in ./path_plan9.go too.
    for i, e := range elem {
        if e != "" {
            return Clean(strings.Join(elem[i:], string(Separator)))
        }
    }
    return ""
}

在 Unix 系统下,filepath.Join 非常简单,它会在Clean之后直接拼接路径,没有任何问题。

func volumeNameLen(path string) int {
    if len(path) < 2 {
        return 0
    }
    // with drive letter
    c := path[0]
    if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
        return 2
    }
    // is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
    if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
        !isSlash(path[2]) && path[2] != '.' {
        // first, leading `\\` and next shouldn't be `\`. its server name.
        for n := 3; n < l-1; n++ {
            // second, next '\' shouldn't be repeated.
            if isSlash(path[n]) {
                n++
                // third, following something characters. its share name.
                if !isSlash(path[n]) {
                    if path[n] == '.' {
                        break
                    }
                    for ; n < l; n++ {
                        if isSlash(path[n]) {
                            break
                        }
                    }
                    return n
                }
                break
            }
        }
    }
    return 0
}
func join(elem []string) string {
    for i, e := range elem {
        if e != "" {
            return joinNonEmpty(elem[i:])
        }
    }
    return ""
}

// joinNonEmpty is like join, but it assumes that the first element is non-empty.
func joinNonEmpty(elem []string) string {
    if len(elem[0]) == 2 && elem[0][1] == ':' {
        // First element is drive letter without terminating slash.
        // Keep path relative to current directory on that drive.
        // Skip empty elements.
        i := 1
        for ; i < len(elem); i++ {
            if elem[i] != "" {
                break
            }
        }
        return Clean(elem[0] + strings.Join(elem[i:], string(Separator)))
    }
    // The following logic prevents Join from inadvertently creating a
    // UNC path on Windows. Unless the first element is a UNC path, Join
    // shouldn't create a UNC path. See golang.org/issue/9167.
    p := Clean(strings.Join(elem, string(Separator)))
    if !isUNC(p) {
        return p
    }
    // p == UNC only allowed when the first element is a UNC path.
    head := Clean(elem[0])
    if isUNC(head) {
        return p
    }
    // head + tail == UNC, but joining two non-UNC paths should not result
    // in a UNC path. Undo creation of UNC path.
    tail := Clean(strings.Join(elem[1:], string(Separator)))
    if head[len(head)-1] == Separator {
        return head + tail
    }
    return head + string(Separator) + tail
}

// isUNC reports whether path is a UNC path.
func isUNC(path string) bool {
    return volumeNameLen(path) > 2
}

func sameWord(a, b string) bool {
    return strings.EqualFold(a, b)
}

在 Windows 系统下,filepath.Join 函数的实现要复杂得多,因为需要处理路径分隔符和 UNC 路径等特殊情况。
到这里就变得有趣了一些 filepath.Join 的输入不完全是用户控制的 Clean函数会把用户输入和固定路径一起处理
这个工具刚好出现了一个非常特殊的情况
文件服务器本来想要限制访问当前目录下的文件
filepath.Join("./",'已经处理后的用户输入')
如果输入的路径是./ abc/1.txt
Clean处理后会变成 abc/1.txt Clean去除了开头的设定的./
这个处理在linux系统下没有问题
但是在windows 系统下 如果我们构造路径组./ c:/1.txt
Clean处理后会变成 c:/1.txt
显然从Clean处理后把当前目录下的路径变为了c盘根目录
在这里,filepath.Clean 函数的处理并没有避免目录穿越问题,反而造成了一个安全漏洞。
最终在http server 上复现成功

小插曲

提交给go 官方之后才发现这洞3个月前就被修复了. 我电脑上的go版本一直没更新 23333

漏洞issue
https://github.com/golang/go/issues/52476

漏洞影响&利用条件

  1. 使用 filepath.Clean/filepath.Join 处理路径
  2. 左侧被拼接路径为./
  3. 右侧路径可完全控制
  4. Go编译Windows二进制文件使用 Go 1.18 <1.18.3 Go 1.17 <1.17.11 (不在维护的版本应该不会修复)
  5. 目标二进制部署在windows 操作系统

标准库的漏洞会影响编译分发出的二进制文件

解决方法

更新go到最新版本 重新编译发布二进制文件

from https://tttang.com/archive/1884/

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码