JTopCMS审计之目录穿越漏洞

JTopCMS审计之目录穿越漏洞

原创 庆尘qc Daylight庆尘 2023-12-10 10:04

一、环境部署

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

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.d
ebug
(
“{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
:
\Work

Space\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中拦截的字符有些都是重复的,替换的意义是什么,真有点没搞明白。