目录穿越漏洞 CVE-2022-29804
2023-5-16 12:44:43 Author: 白帽子左一(查看原文) 阅读量:21 收藏

扫码领资料

获网安教程

免费&进群

一天,我看完了番剧后,闲着无聊审计了一下我用来做内网共享的小工具——"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到最新版本 重新编译发布二进制文件

来源:https://tttang.com/archive/1884/

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247594746&idx=1&sn=ea739166f269333d46b04e5426705dfa&chksm=ebeb3bd7dc9cb2c1ce04aa7ffe37d9f2fe28c2ff7aadf1e20d7a75d63660ad1ccacd316d8613#rd
如有侵权请联系:admin#unsafe.sh