JTopCMS审计之目录穿越漏洞
一、环境部署
1.1、官方网站
1.2、下载地址
https://gitee.com/mjtop/JTopCMSV3/releases/tag/JTopCMSV3.0.2-OP
1.3、所需环境
名称 | 版本 |
---|---|
JTopCMS | V3版本(开源版本) |
Java版本 | JDK1.7+即可 |
mysql | 5.5.29 |
Tomcat | Tomcat7+即可 |
1.4、修改配置
在JTopCMSV3-JTopCMSV3.0.2-OP\WebContent\WEB-INF\config\cs.properties配置文件中修改端口信息(与配置的Tomcat保持一致)与数据库连接信息
1.5、启动项目
启动项目之后,来到后端管理登录页面:
http://127.0.0.1:7089/login_page
登录账号密码为 admin/jtopcms
二、代码审计
今天我们只针对这个应用的目录穿越漏洞进行审计,而目录穿越漏洞一般伴随文件上传/下载/删除等功能,所以我们先查找一下该程序是否存在文件上传/下载/删除功能
那审计一个项目时如何快速定义到系统的下载功能呢?
1、项目中的使用文档或用户手册。
2、部署环境后通过抓取下载数据包确定接口名后全局搜索接口名
3、使用经典关键字download等进行全局搜索即可
这里通过关键字检索,我们发现了两个疑似文件下载的接口
下面我们分别来对这两个下载功能的接口进行追踪与漏洞测试
1、/downloadback.do
(1)功能点定位
首先定位到映射代码位于BackupSystemController类中,如下
再定位到请求该接口的jsp文件中,如下
通过方法名—downloadBak和该jsp文件名ManageBackup.jsp猜测该功能是一个备份文件管理功能,并且在备份下载功能中调用了该接口
所以来到后台寻找该功能点,成功定位到下载功能点,并且确实存在备份文件下载功能,功能点位于
- 后台->系统配置->系统备份管理
抓个包再次验证
进一步确认了该功能点调用了**/downloadback.do接口**
(2)源代码审计
1、Controller层
根据请求包来看,target,pw,downFileInfo,这三个参数都有可能与文件下载功能有关,
我们给下载备份这个功能点下一个断点,再调试项目,看看程序的执行流程
简化后的流程代码如下
@RequestMapping( value = "/downloadBak.do", method = { RequestMethod.POST } )
public String downloadBak( HttpServletRequest request, HttpServletResponse response )
throws UnsupportedEncodingException
{
// 将请求传入的参数存储在Map对象
Map params = ServletUtil.getRequestInfo( request );
// 通过session判断用户身份是否合法
.....
// 从请求中获取target参数的值,赋值为targetbak
String targetBak = ( String ) params.get( "target" );
// 获取系统的真实路径并赋值给base变量
String base = SystemConfiguration.getInstance().getSystemConfig().getSystemRealPath();
// 构建一个如下的字符串,并赋值testbakroot
String testBakRoot = base + "__sys_bak__" + File.separator + targetBak;
// 检查了名为testBakRoot的路径是否指向的是一个文件,并为其创建File对象
......
// 开始下载File对应的文件
......
// 关闭流
......
// 抛出异常
......
分析上面的代码,可以看我们传入的target会被直接拼接到如下路径,在这段代码中并没有防范
{base}/_sys_bak_/{target}
base在我的项目中的值如下
E:/WorkSpace/Javawork/JTopCMSV3-JTopCMSV3.0.2-OP/target/ROOT
即拼接后的备份完整路径为(假设备份文件名为bak.zip,即target为bak.zip)
E:/WorkSpace/Javawork/JTopCMSV3-JTopCMSV3.0.2-OP/target/ROOT/_sys_bak_/bak.zip
于是我在
- E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT
目录下新建了一个a.txt,再请求**/downloadbak接口**,传入的target的值改为..\a.txt,尝试拼接为如下的接口进行读取a.txt,从而验证任意文件下载漏洞
E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\_sys_bak_\..\a.txt
结果如下
可以看到被拦截了,并且给出提示:包含非法字符
控制台打印如下
这里我们思考一下,刚才我们Controller层代码中并没有检测target的合法性,但现在我们传入恶意的target又被拦截了,说明了什么?说明这个downloadbak请求到达Controller类代码之前被拦截并执行了检测,第一时间就想到了去看看Spring的配置文件中是否配置了拦截器
2、interceptor层
果不其然,在其中找到了这样一段Spring MVC的拦截器配置代码,如下
<mvc:interceptors>
// 第一个拦截器
<mvc:interceptor>
<!-- 拦截所有的请求-->
<mvc:mapping path="/**" />
<bean class="cn.com.mjsoft.cms.common.interceptor.SpringMVCFlowExeTokenAndTraceInterceptor" />
</mvc:interceptor>
// 第二个拦截器
<mvc:interceptor>
<mvc:mapping path="/survey/clientVote.do" />
<mvc:mapping path="/guestbook/clientAddGb.do" />
<mvc:mapping path="/content/clientAddContent.do" />
<mvc:mapping path="/content/clientEditContent.do" />
<mvc:mapping path="/content/deleteContentToTrash.do" />
<mvc:mapping path="/clientAddComment/clientAddComment.do" />
<mvc:mapping path="/deleteComment/deleteComment.do" />
<mvc:mapping path="/resources/clientDf.do" />
<mvc:mapping path="/member/*.do" />
<bean class="cn.com.mjsoft.cms.member.interceptor.MemberActScoreInterceptor" />
</mvc:interceptor>
// 第三个拦截器
<mvc:interceptor>
<mvc:mapping path="/member/*.do" />
<bean class="cn.com.mjsoft.cms.member.interceptor.MemberSendMessageInterceptor" />
</mvc:interceptor>
// 第四个拦截器
<mvc:interceptor>
<mvc:mapping path="/content/addContent.do" />
<mvc:mapping path="/content/editContent.do" />
<mvc:mapping path="/content/clientAddContent.do" />
<mvc:mapping path="/content/clientEditContent.do" />
<bean class="cn.com.mjsoft.cms.content.interceptor.DeleteTempFileWhenUploadErrorInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
可以看到配置了四个拦截器,这里能够匹配到/resources/donloadbak.do请求的只有第一个拦截器
这里提一下以下两种情况
1、如果有多个拦截器都能匹配,则拦截顺序默认按照配置拦截器的顺序进行
2、Spring MVC中,可以通过在配置文件中使用mvc:interceptor-ref标签来引用并配置拦截器,并使用order属性来指定它们的顺序,这样的话拦截顺序则变为指定顺序
回到正题,经过上面的分析,我们的/resources/downloadbak.do请求会先被第一个拦截器处理,所以我们跟过去看看第一个拦截器类(即SpringMVCFlowExeTokenAndTraceInterceptor类)的内容,如下
可以看到这个类实现了Spring的HandlerInterceptor接口,用于拦截请求并执行一些前置和后置的处理,这里我们要分析它对拦截的请求的检测逻辑,所以主要分析前置处理代码,即preHandle方法
简单看了下preHandle方法,只是进行了权限校验,也没有对参数特殊字符的处理
Controller类中没有处理,Controller类之前的拦截器也没有处理,那还有什么能够在Controller类和拦截器之前对请求进行处理呢?答案就是过滤器(Filter)
3、Filter层
过滤器的配置可以到Web应用的配置文件web.xml文件中查看,如下
可以看到这里配置了两个过滤器,第一个拦截的是所有请求,第二个拦截的是.do结尾的,我们刚才被拦截的请求是
/sources/downloadbak
所以会被第一个Filter拦截,我们定位到到第一个Filter对应的Filter实现类中,即InterceptFilter类
可以看到InterceptFilte类虽然名字叫InterceptFilte(拦截器),但其实实现的是Filter接口的一个自定义过滤器,因为刚才控制台打印了如下两句话
[2023-12-09 15:54:27,756] FATAL - danger char->..
[2023-12-09 15:54:27,757] FATAL - IP->172.24.86.134,非法动作->target=../a.txt&pw=,URL->http://172.24.86.134:8090/resources/downloadBak.do
说明是我们传入的target=../a.txt中带了**”..”被识别为危险字符了,所以先搜索一下“..”**字符,如下
在静态代码块中成功找到定义,而且这个_$6的内容看着也像黑名单字符,所以我们先试着追踪一下_$6的流转
可以看到288行调用当前类的_$1方法时,将_$6作为参数传入了方法
我们分析一下_$1方法,如下
经过分析,发现确实就是它导致了我们的..被拦截,分析流程如下
private boolean _$1(String var1, String[] var2, String var3, boolean var4) {
_$13.debug("{SYSTEM Adjudgement} 将验证参数:" + var1);
// 判断var1是否为空,为空就退出
......
//不为空就继续执行
if (var1.startsWith("http://")) {
//如果URL以 "http://..." 开头,检查它是否与已知站点的URL匹配,或者是否包含特定危险字符。
//如果匹配或包含危险字符,记录错误日志,返回 false 表示不安全。
}
// 如果var4=true
// 就检查var1是否包含危险字符
// 包含的话就打印错误信息并退出返回false
// 如果var4=false
// 就检查var1是否包含危险字符和“=”号
// 包含的话就打印错误信息并退出返回false
return true;
}
简单来说这段代码就是用于检测var1是否包含提前定义好的危险字
所以刚才我们的控制台上会出现 **danger char->..**这个信息
因为刚才的判断代码返回false,即我们的输入不安全,所以这里的288内的代码块会被执行
导致var55被赋值为false,从而执行第299行代码,即调用_$1方法,_$1方法打印出本次导致错误的动作信息,如下
这也就解释了我们控制台的第二句打印
IP->172.24.86.134,非法动作->target=../a.txt&pw=,URL->http://172.24.86.134:8090/resources/downloadBak.do
所以这个**/downloadbak.do**下载接口是有特殊字符过滤的,我在这里试了很久,但还是没有办法绕过,即没有办法配合目录穿越 ../ 来读取任意位置的敏感文件,所以我个人认为这里暂时是没有漏洞的。
进入下一个下载功能点。
/downloadresfile.do
(1)功能点定位
首先定位到关键代码
可以看到关键代码位于ManageSiteFileAndCheckController类中,再请求到定位该接口的jsp文件中,如下
成功定位到ManageTemplate.jsp文件中,并且还传入了一个entry参数
根据文件名和文件信息确认该功能点为模板管理功能的下载功能,如下
(2)源代码审计
根据上一个下载的功能点,我们知道了请求参数的值中不能包含指定的危险字符,基于这个前提,我们再来审计一下这个下载的功能点
审计上一个下载点的时候,我们已经把过滤器和拦截器分析过了,这里就不分析了,直接看Controller类的代码,如下
- 1、先分析211-219行,从请求中获取mode、entry、downFileInfo参数的值,并且entry和downFileInfo两个参数还经过了SystemSafeCharUtil.decodeFromWeb方法处理
- 2、校验用户身份是否合法
- 3、根据日期随机生成一串字符,拼接.zip,形成一个类似下面这样的压缩文件名
*sys_template_temp*demo_2023_465461.zip
发现在267行拼接了压缩文件名,跟进getFullPathByManager方法来到ResourcesService.java类中
主要代码如上,简单来说该方法就是把zipName中的替换为路径分隔符(”"或者”/“),再过滤掉一些敏感字符,再与根路径进行拼接,最终得到类似如下的*fullZipPath
E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\demo\sys_template_temp\demo_2023_465461.zip
\\ 其中*sys_template_temp*demo_2023_465461.zip为传入的zipName(entry)
后面的代码简单的说就是基于根目录,压缩template(entry参数的值)文件夹下的block(downloadfileinfo参数的值)文件夹下的内容并复制到sys_template_temp目录下,同时供用户下载
所以就从头梳理了一下,想起了最开始获取请求的entry、downFileInfo参数的值时,获取后调用了SystemSafeCharUtil.decodeFromWeb方法,所以我们追踪SystemSafeCharUtil.decodeFromWeb方法,如下
可以看到该方法将传入的input参数进行了一次url解码后,再调用decodeDangerChar()方法,我们继续追踪该方法
通过代码可以得知该方法主要用来完成替换危险字符的操作,如果路径中存在一些危险字符的话就会被该方法替换为指定字符,同时我观察到
- *!4* 则会被替换成**”..”**
- *!11* 会被替换成**”\“**
这样说的话路径中存在 ****!4****!11*的话就会变为“..\“*,这样是不是就又可以向上遍历目录了?于是我们来尝试读取一下电脑上的其他文件,例如
E:\info.txt
经过我们上面的分析,得出下载的文件根目录在
E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\demo
根据传入的entry和downFileInfo
最后构成
E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\demo\{entry}\{downFileInfo}
所以我们把downFileInfo修改为要读取的info.txt,entry也利用多个****!4****!11****穿越到E盘的根目录,
尝试形成如下的文件链接
E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\demo\template\..\..\..\..\..\..\..\..\info.txt
从而读取到
成功读取到目标文件
三、总结
到这里,JTopCMS的下载功能的目录穿越漏洞就审计完毕,看到Power7089师傅在讲解目录漏洞时讲了这个CMS的下载功能存在任意文件下载漏,就想着试一试自己能不能找到,在第一个下载点的审计花了很久,因为一直没找到“../”是怎么被拦截下来的,并且找到拦截代码之后尝试绕过也试了很久,还是没绕过去,审计第二个就比较容易了,直接在Controller类中就跟踪到了问题代码,审计完之后再和Power7089师傅的审计过程比对一下,发现确实是恶意字符替换导致的问题,这里贴一下Power7089师傅审计该漏洞的文章链接
https://power7089.github.io/2022/11/29/JavaWeb代码审计实战之配合JtopCMS实战讲解目录穿越漏洞/
最后,真的想吐槽一下,开发人员添加这个替换恶意字符的操作的意义是什么?明明Filter中都已经做了很好的拦截操作了,比如第一个下载的功能点,全靠Filter拦截,第二个下载点非要多此一举,替换后的字符和Filter中拦截的字符有些都是重复的,替换的意义是什么,真有点没搞明白。