Toc
  1. 作者:橙子酱
  • 前言
  • 代码审计
    1. 非常安全的代码?
    2. 标准库中的漏洞
      1. filepath.Clean
      2. filepath.Join
  • 小插曲
  • 漏洞影响&利用条件
  • 解决方法
  • Toc
    0 results found
    白帽酱
    一个隐藏在Go语言标准库中的目录穿越漏洞 CVE-2022-29804
    2023/03/25 漏洞分析 WEB CVE 0DAY
    作者:橙子酱

    前言

    这是半年前我在 Go 语言中发现的一个目录穿越漏洞(虽然被人抢先发现了)。
    Go 语言支持非常方便的交叉编译,但是在不同平台下,操作系统对某些功能的实现有所差异。这些差异可能会导致一些安全问题。

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

    图片.png

    代码审计

    非常安全的代码?

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

    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 上复现成功

    图片.png图片.png小插曲

    提交给go 官方之后才发现这洞3个月前就被修复了. 我电脑上的go版本一直没更新 23333
    图片.png漏洞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到最新版本 重新编译发布二进制文件

    本文作者:白帽酱
    版权声明:本文首发于白帽酱的博客,转载请注明出处!