一、环境部署

1.1、官方网站

https://www.jtopcms.com/

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保持一致)与数据库连接信息

img

1.5、启动项目

启动项目之后,来到后端管理登录页面:

http://127.0.0.1:7089/login_page

img

登录账号密码为 admin/jtopcms

二、代码审计

今天我们只针对这个应用的目录穿越漏洞进行审计,而目录穿越漏洞一般伴随文件上传/下载/删除等功能,所以我们先查找一下该程序是否存在文件上传/下载/删除功能

那审计一个项目时如何快速定义到系统的下载功能呢?

1、项目中的使用文档用户手册

2、部署环境后通过抓取下载数据包确定接口名后全局搜索接口名

3、使用经典关键字download等进行全局搜索即可

这里通过关键字检索,我们发现了两个疑似文件下载的接口

img

下面我们分别来对这两个下载功能的接口进行追踪与漏洞测试

1、/downloadback.do

(1)功能点定位

首先定位到映射代码位于BackupSystemController类中,如下

img

再定位到请求该接口的jsp文件中,如下

img

通过方法名—downloadBak和该jsp文件名ManageBackup.jsp猜测该功能是一个备份文件管理功能,并且在备份下载功能中调用了该接口

所以来到后台寻找该功能点,成功定位到下载功能点,并且确实存在备份文件下载功能,功能点位于

  • 后台->系统配置->系统备份管理

img

抓个包再次验证

img

进一步确认了该功能点调用了**/downloadback.do接口**

(2)源代码审计

1、Controller层

根据请求包来看,targetpwdownFileInfo,这三个参数都有可能与文件下载功能有关,

我们给下载备份这个功能点下一个断点,再调试项目,看看程序的执行流程

img

简化后的流程代码如下

@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

img

结果如下

img

可以看到被拦截了,并且给出提示:包含非法字符

控制台打印如下

img

这里我们思考一下,刚才我们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类)的内容,如下

img

可以看到这个类实现了Spring的HandlerInterceptor接口,用于拦截请求并执行一些前置和后置的处理,这里我们要分析它对拦截的请求的检测逻辑,所以主要分析前置处理代码,即preHandle方法

img

简单看了下preHandle方法,只是进行了权限校验,也没有对参数特殊字符的处理

Controller类中没有处理,Controller类之前的拦截器也没有处理,那还有什么能够在Controller类和拦截器之前对请求进行处理呢?答案就是过滤器(Filter)

3、Filter层

过滤器的配置可以到Web应用的配置文件web.xml文件中查看,如下

img

可以看到这里配置了两个过滤器,第一个拦截的是所有请求,第二个拦截的是.do结尾的,我们刚才被拦截的请求是

/sources/downloadbak

所以会被第一个Filter拦截,我们定位到到第一个Filter对应的Filter实现类中,即InterceptFilter类

img

可以看到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中带了**”..”被识别为危险字符了,所以先搜索一下“..”**字符,如下

img

在静态代码块中成功找到定义,而且这个_$6的内容看着也像黑名单字符,所以我们先试着追踪一下_$6的流转

可以看到288行调用当前类的_$1方法时,将_$6作为参数传入了方法

img

我们分析一下_$1方法,如下

img

经过分析,发现确实就是它导致了我们的..被拦截,分析流程如下

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是否包含提前定义好的危险字

img

所以刚才我们的控制台上会出现 **danger char->..**这个信息

因为刚才的判断代码返回false,即我们的输入不安全,所以这里的288内的代码块会被执行

img

导致var55被赋值为false,从而执行第299行代码,即调用_$1方法,_$1方法打印出本次导致错误的动作信息,如下

img

这也就解释了我们控制台的第二句打印

IP->172.24.86.134,非法动作->target=../a.txt&pw=,URL->http://172.24.86.134:8090/resources/downloadBak.do

所以这个**/downloadbak.do**下载接口是有特殊字符过滤的,我在这里试了很久,但还是没有办法绕过,即没有办法配合目录穿越 ../ 来读取任意位置的敏感文件,所以我个人认为这里暂时是没有漏洞的。

进入下一个下载功能点。

/downloadresfile.do

(1)功能点定位

首先定位到关键代码

img

可以看到关键代码位于ManageSiteFileAndCheckController类中,再请求到定位该接口的jsp文件中,如下

img

成功定位到ManageTemplate.jsp文件中,并且还传入了一个entry参数

img

根据文件名和文件信息确认该功能点为模板管理功能的下载功能,如下

img

(2)源代码审计

根据上一个下载的功能点,我们知道了请求参数的值中不能包含指定的危险字符,基于这个前提,我们再来审计一下这个下载的功能点

审计上一个下载点的时候,我们已经把过滤器拦截器分析过了,这里就不分析了,直接看Controller类的代码,如下

img

  • 1、先分析211-219行,从请求中获取mode、entry、downFileInfo参数的值,并且entry和downFileInfo两个参数还经过了SystemSafeCharUtil.decodeFromWeb方法处理
  • 2、校验用户身份是否合法
  • 3、根据日期随机生成一串字符,拼接.zip,形成一个类似下面这样的压缩文件名
*sys_template_temp*demo_2023_465461.zip

img

发现在267行拼接了压缩文件名,跟进getFullPathByManager方法来到ResourcesService.java类

img

主要代码如上,简单来说该方法就是把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方法,如下

img

可以看到该方法将传入的input参数进行了一次url解码后,再调用decodeDangerChar()方法,我们继续追踪该方法

img

img

通过代码可以得知该方法主要用来完成替换危险字符的操作,如果路径中存在一些危险字符的话就会被该方法替换为指定字符,同时我观察到

  • *!4* 则会被替换成**”..”**
  • *!11* 会被替换成**”\“**

这样说的话路径中存在 ****!4****!11*的话就会变为“..\“*,这样是不是就又可以向上遍历目录了?于是我们来尝试读取一下电脑上的其他文件,例如

E:\info.txt

经过我们上面的分析,得出下载的文件根目录

E:\WorkSpace\Javawork\JTopCMSV3-JTopCMSV3.0.2-OP\target\ROOT\demo

根据传入的entrydownFileInfo

最后构成

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

从而读取到

img

成功读取到目标文件

img

三、总结

到这里,JTopCMS的下载功能的目录穿越漏洞就审计完毕,看到Power7089师傅在讲解目录漏洞时讲了这个CMS的下载功能存在任意文件下载漏,就想着试一试自己能不能找到,在第一个下载点的审计花了很久,因为一直没找到“../”是怎么被拦截下来的,并且找到拦截代码之后尝试绕过也试了很久,还是没绕过去,审计第二个就比较容易了,直接在Controller类中就跟踪到了问题代码,审计完之后再和Power7089师傅的审计过程比对一下,发现确实是恶意字符替换导致的问题,这里贴一下Power7089师傅审计该漏洞的文章链接

https://power7089.github.io/2022/11/29/JavaWeb代码审计实战之配合JtopCMS实战讲解目录穿越漏洞/

最后,真的想吐槽一下,开发人员添加这个替换恶意字符的操作的意义是什么?明明Filter中都已经做了很好的拦截操作了,比如第一个下载的功能点,全靠Filter拦截,第二个下载点非要多此一举,替换后的字符和Filter中拦截的字符有些都是重复的,替换的意义是什么,真有点没搞明白。