项目介绍
MyBlog是由SpringBoot+Mybatis+Thymeleaf等技术实现的Java博客系统,页面美观、功能齐全、部署简单及完善的代码,适合个人开发者、小型团队或教育机构使用,其灵活性和易用性使得用户能够专注于内容创作,而将繁琐的技术细节留给系统来处理
环境搭建
首先下载源代码文件到本地并使用IDEA加载源代码文件:
创建数据库并导入数据库表数据文件
随后启动项目并在浏览器中访问Web系统
登录后台地址如下:
http://192.168.204.137:8081/admin/login
代码审计
本次代码审计我们的重点在于关注用户登录认证过程中的业务设计缺陷的挖掘,所以我们这里主要审计登录认证部分的代码,我们直接来到后台对应的登录认证的Controller一层看一下关于后台登录的认证逻辑,从下面可以看到这里首先是获取了用户登录认证的失败次数,随后获取了用户的用户名和密码进行身份认证
blog-mastersrcmainjavacommyblogwebsitecontrolleradminAuthController.java
随后跟踪login的实现类一直到blog-mastersrcmainjavacommyblogwebsiteserviceimplUserServiceImpl.java,从这里可以看到首先检查了从上层传递过来的用户名和密码是否为空,随后又创建了一个example的查询实例,随后增加了一个SQL查询的断言要求用户名为username,随后调用底层的SQL进行查询
countByExample的DAO层定义,随后继续向下查找齐对应的Mapper中的SQL语句的定义
随后跟踪来到UserVomapper.xml查看到对应的后端SQL语句定义,从下面可以看到这里如果参数不为空,那么会继续引用这里的Example_Where_Clause,随后在当前上下文进行查找
Example_Where_Clause的定义如下,在这里我们重点介绍一下这一部分,因为它是目前多数项目中会用到的一个点——动态生成SQL,在这里它主要用于根据入参来定义SQL语句的where部分,这里首先使用foreach标签遍历oredcriteria集合,item表示当前元素的名称(此处为criteria),separator指定项之间的分隔符(这里是or),这意味着针对每个criteria将生成一个以OR分隔的条件组,随后进行调节判断,只有当criteria.valid为真时才会执行下面的代码块,随后使用trim来去除特定前缀、关键词和后缀,随后再次使用foreach,这次遍历criteria.criteria集合中的每一个criterion,每个criterion表示具体的条件,例如:刚开始的"无值"检查,如果criterion没有值,则仅使用条件,例如:WHERE condition
<when test="criterion.noValue">
and ${criterion.condition}
</when>
如果criterion有一个值则使用条件和该值,例如:WHERE condition=value
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
如果criterion是一个区间值,则用于生成类似:WHERE condition BETWEEN valuel AND value2的条件,如果criterion是一个列表,用于生成类似WHERE condition IN(valuel,value2,...)的条件,同时从下面我们可以看看到这里的条件判断使用了${}进行条件的拼接,但是这里的条件部分是我们最开始的条件构建部分的构建的内容,而期间真正可控的是条件表达式的值,也就是这里的Value,我们从上层传递过来的用户名——username
下面我们再对上面的条件部分进行一个跟踪来规避大家认为的存在SQL注入的疑惑部分,跟踪andUsernameEqualTo
继续跟踪你会发现这里的criteria其实就是一个List,上面的调用add操作类似于直接就是构建了一个二维List数组List(List("username",value)),同时需要注意的是这里的username是之前预定义的,而不是用户入参,用户参部分为Value
而我们从上层传递有一个username的用户名传参后经过条件封装后到后端的SQL查询语句如下,所以到这里大家应该明白为啥不存在SQL注入了,说白了这里就是将我们传统的MySQL语句中的条件等部分按需进行了List封装,条件是条件(根据特定的场景按需定义),值是值(用户入参),实现了条件和值的分离,那现在我们解释清楚了哈,下面我们继续来分析上面的登录认证逻辑部分
selectcount(*) from t_users where username =
随后如果返回的count如果为零则说明不存在这样一个用户,直接抛出异常——"不存在该用户",如果返回的count不为零则说明存在这样一个用户,随后继续根据上面的方式来判断用户密码是否正确,如果用户名和密码正确则直接取返回List的第一项
从上面可以看到这里存在一个差异性回答,用户可以用于进行枚举用户名
随后可以看到这里如果认证失败则会直接进入到catch中,同时这里的一个error_count会进行递增,随后根据失败次数来设置失败回显内容,这里留一个疑问给大家思考——这里的error_count检测逻辑是否可以防御上面的用户枚举风险呢?如果认证成功会设置用户的Session,同时如果我们勾选了上面的remember_me会调用TaleUtils.setCookie来设置一个"有效"的Cookie
随后跟踪来到setCookie部分,可以看到这里使用了Tools.enAes来接受上层传递过来的用户uid信息并入参AES盐值进行加密存储生成一个val并设置其为USER_IN_COOKIE这个键对应的Value值来充当用户的Cookie字段部分,随后设置生命周期并返回内容
上溯可以看到盐值为0123456789abcdef,Cookie中用于设置用户的Key为——"S_L_ID"
那么这里存在什么问题呢?什么问题呢?大家可能有些一脸茫然,这里不是正常的验证逻辑和Cookie的设置逻辑吗?有啥问题呢?笔者刚开始也是这样想的,不过你如果细细想一下就会发现一个问题,这里设的Cookie是根据用户的uid并且使用硬编码的AES盐值和加密算法来生成的,同时也是有效的,有效的哦,有效的一个Cookie哦,现在是不是更加清楚了,如果我们在识别某一个网站使用了我们当前的这个系统模版来搭建网站,那么我们从开源的情形下获取到源代码并且部署方没有更改AES盐值,那么我们在本地来定义一个简单的测试类来生成一个有效的Cookie,同时查询数据库可以得到admin的uid默认为1,这样一来条件都满足了
随后本地构建一个测试类来生成一个有效的Cookie
package com.my.blog.website;
import com.my.blog.website.constant.WebConst;
import com.my.blog.website.utils.Tools;
public class CookieSecTest {
public static void main(String[] args) throws Exception{
System.out.println(Tools.enAes("1", WebConst.AES_SALT));
}
}
随后使用生成的Cookie部分构造Cookie内容:
S_L_ID=EqTraX5StRb4FiQjWzL90g==
随后刷新页面直接进入后台并正常操作使用业务功能
文末小结
本篇文章通过CMS代码审计案例对登录认证过程中不安全的业务逻辑设计和编码进行了漏洞风险介绍和梳理,同时借助这一个案例对Mapper文件中的动态SQL语句的构造以及对应的一些看似未使用预编译缺不存在SQL注入的场景和原因进行了介绍分析,当然在审计中大家也要和具体的场景相互结合来判定是否存在SQL注入安全问题,并不是这一大类不存在SQL注入问题哦,比如:limit入参从用户一侧入参并在后端直接使用拼接的方式进行拼接,那么这样就可以导致SQL注入安全问题
原文始发于微信公众号(七芒星实验室):记一次对某博客系统登录认证缺陷的代码审计
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论