后台常见安全漏洞安全
常见漏洞:
- 弱口令
- 形同虚设的登录
- 越权漏洞
# 弱口令
**介绍:**弱口令没有严格和准确的定义,通常认为容易被别人猜测破解的口令均为弱口令。弱口令指的是仅包含
简单数字和字母的口令,例如”123”、”abc”等,因为这样的口令很容易被别人破解。
2022弱指令top10:
提示
password、123456、123456789、guest、qwerty、12345678、111111、col123456、123123
分类:
提示
- 普通型弱口令就是常见的密码
- 条件型弱口令就是和用户信息相关的密码,比如生日+手机号、姓名首字母+生日、爱人姓名首字
母+生日+常用字母(520、1314等)
# 攻击方式
# 爆破(暴力破解)
**概念:**通过爆破工具就可以很容易破解用户的弱口令。暴力破解的原理就是使用用户名和密码字典,一个一个
去枚举尝试是否能够登录。
在理论上来说只要字典足够庞大,枚举总是能够成功的!
爆破工具示例
提示
- Burp Suite:https://portswigger.net/burp (opens new window)
- Bruter
# 社工(社会工程学)
**概念:**社工是(社会工程学)简称,一种黑客的攻击手段,通过社会工程的方式获取收集你的相关信息,作为
生成攻击字典的素材。
社会工程的方式很多,如:
提示
- 网络搜集
- 社工库
- 网络钓鱼
- 使用IM、论坛、邮件、短信等方式接近目标人群以收集更多信息
在互联网上你几乎没有个人隐私!
姓名、生日、邮箱、手机号、某些账号密码都有可能被公开或半公开过。
撞库:某网站A遭到入侵泄露了你部分数据,攻击者可以利用这部分数据对你在网站B的账号发起入侵尝试。
# 预防策略
# 爆破的预防
成因分析:
提示
- 密码过于简单
- 请求中没有动态参数,都是相对固定内容
- 请求可以无限次尝试,没有限制措施
预防策略:
提示
- 强制密码复杂度长度、字母大小写数字特殊字符组合、定期修改
- 图像验证码双刃剑,太简单了容易辨认也容易被攻击者程序识别,从而失去价值;太复杂模糊了,会增加攻击难度,但同时人眼识别也比较困难,降低用户体验。好在现在有越来越多的验证码方式可供选择,如基于文字语言理解的、基于文字计算的、滑块式的或者识别图像式的等等,比如拼图验证码(网易、极验)、12306的标记图片验证码方式。简单的图形验证码很容易被机器识别(百度提供基于AI的识别接口),复杂的图形验证码也有打码平台人工识别。可以解决请求中没有动态参数,导致攻击者可随意爆破的问题。
- 限制重试次数,添加锁定机制根据IP地址、用户名进行尝试次数限制,随着AI的发展对图像验证码识别越来越容易,单靠验证码有时候不能防止爆破,这就需要在服务端,增加更多的安全机制,可以在程序中通过拦截器或者网关做统一的控制。解决攻击者可以无限次尝试的问题,增加攻击难度。
# 图形验证码
验证逻辑
提示
- 用户访问登录页面,发起验证码请求
- 后端生成验证码图片,并将字符串放入session
- 用户输入账密+验证码,点击登录
- 后端验证账密及验证码,正确通过,不正确返回登录页面
- 删除session
controller示例代码
/**
* 生成验证码
*/
@RequestMapping("/getVerifyCodeImg")
public void getVerifyCodeImg(HttpServletRequest request, HttpServletResponse response) {
String code = VerifyCodeUtils.getRandomCodeStr();
BufferedImage buffImg = VerifyCodeUtils.getImgCode(code);
logger.debug("Code is {}", code);
// 将四位数字的验证码保存到Session中。
HttpSession session = request.getSession();
session.setAttribute(SESSION_NAME_VERIFY_CODE, code);
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
try (ServletOutputStream sos = response.getOutputStream()) {
ImageIO.write(buffImg, "jpeg", sos);
} catch (IOException e) {
logger.error("获取验证码异常", e);
}
}
/**
* 后台登录提交
*
* @param userName
* @param password
* @param request
* @return
*/
@PostMapping("/login")
public String postLogin(String userName,
String password,
String verifyCode,
HttpServletRequest request,
ModelMap map) {
//第三章增加图形验证码判断
Object sessionCode =
request.getSession().getAttribute(SESSION_NAME_VERIFY_CODE);
verifyCode = verifyCode.toUpperCase();
if (sessionCode == null || !((String) sessionCode).equals(verifyCode)) {
//验证不通过
logger.debug("用户输入的验证码{},session中的验证码{}", verifyCode,
sessionCode == null ? "" : sessionCode.toString());
map.put("result", "登陆失败,请正确输入验证码。");
return "admin/login";
} else {
//Todo 这段代码很关键,没有这段代码的验证码方案1 不能预防爆破,加这段代码
才可以预防爆破
request.getSession().removeAttribute(SESSION_NAME_VERIFY_CODE);
//验证 验证码 通过,进行用户名密码判断校验
SystemUser systemUser =
systemUserService.getSystemUserByUserNameAndPwd(userName, password);
if (systemUser != null) {
request.getSession().setAttribute("isLogin", true);
request.getSession().setAttribute("userName", userName);
request.getSession().setAttribute("user", systemUser);
//先添加到session,在跳转
return "redirect:/admin/main";
} else {
map.put("result", "登陆失败,检查账号密码是否有误。");
return "admin/login";
}
//增加对象放入session中
//只是密码的简单判断,哈哈。当然也可以连数据判断
//if ("admin".equals(userName) && "qweasd".equals(password)) {
//第二章更新为数据库验证账号密码
//if (systemUserService.validateLogin(userName, password)) {
//第三章 更新获取对象,便于根据对象获取id等信息
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 账号锁定机制
结合数据库或者redis显示登陆失败的锁定机制,可以使用长期锁定或者临时锁定策略,长期锁定需要管理员手动解锁,临时锁定策略过一定时间自动解除锁定。
- 临时锁定
- 长期锁定
下面的示例代码通过一个缓存的Map来实现演示效果,生产环境请按照思路替换为redis或者数据库实
现。
逻辑步骤
提示
- 判断账密前先判断账户是否锁定,如果锁定直接返回;
- 判断账密后如果账密错误,通过工具类更新登录失败信息;
- 判断账密正确,通过工具类清理登录失败信息;
/**
* 这里采用简单的静态Map锁定方案,仅仅用于思路演示
* 生产环境一般使用数据库或者redis实现账号锁定机制
*
*/
public class UserLockUtils {
static final Logger logger = LogManager.getLogger(UserLockUtils.class);
public static Map<String, UserLockObject> userLockMap = new HashMap();
public static int LOCK_FAIL_NUM = 3; //失败锁定的次数
public static int LOCK_TIME_MINUTE = 30;//30分钟锁定时间
/**
* 检查账户是否锁定
* 达到3次错误尝试后锁定30分钟
*
* @param userName
* @return
*/
public static boolean checkLock(String userName) {
UserLockObject user = userLockMap.get(userName);
if (user != null) {
logger.debug("用户{}被锁定,对象信息{}", userName, user);
int num = user.getFailNum();
Date date = user.getLastFailTime();
long timeDifference = ((new Date().getTime() - date.getTime()) / 60 / 1000);
if (num >= LOCK_FAIL_NUM && timeDifference < LOCK_TIME_MINUTE)
{
return true;
}
}
return false;
}
/**
* 账密验证失败后
* 增加失败记录,失败次数+1,失败时间更新为当前时间
*
* @param userName
*/
public static void addFailEven(String userName) {
logger.debug("增加登录失败信息记录{}", userName);
UserLockObject user = userLockMap.get(userName);
if (user == null) {
user = new UserLockObject();
}
user.setFailNum(user.getFailNum() + 1);
user.setLastFailTime(new Date());
userLockMap.put(userName, user);
}
/**
* 账密验证正常后
* 清理该用户的失败记录
*
* @param userName
*/
public static void clearUser(String userName) {
logger.debug("增加成功,清理{}的登录失败信息", userName);
userLockMap.remove(userName);
}
@Data
class UserLockObject {
private int failNum;
private Date lastFailTime;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@PostMapping("/login")
public String postLogin(String userName, String password, String
verifyCode, HttpServletRequest request, ModelMap map) {
//第三章增加图形验证码判断
Object sessionCode =
request.getSession().getAttribute(SESSION_NAME_VERIFY_CODE);
verifyCode = verifyCode.toUpperCase();
if (sessionCode == null || !((String)
sessionCode).equals(verifyCode)) {
//验证不通过
logger.debug("用户输入的验证码{},session中的验证码{}", verifyCode,
sessionCode == null ? "" : sessionCode.toString());
map.put("result", "登陆失败,请正确输入验证码。");
return "admin/login";
} else {
//验证 验证码 通过,进行用户名密码判断校验
//这段代码很关键,没有这段代码的验证码方案1 不能预防爆破,加这段代码才可以预
防爆破
request.getSession().removeAttribute(SESSION_NAME_VERIFY_CODE);
//在验证账密前,先判断用户是否锁定
if (UserLockUtils.checkLock(userName)) {
map.put("result", "登陆失败,该账户已被锁定。");
return "admin/login";
}
SystemUser systemUser =
systemUserService.getSystemUserByUserNameAndPwd(userName, password);
if (systemUser != null) {
//验证成功,清理锁定信息
UserLockUtils.clearUser(userName);
request.getSession().setAttribute("isLogin", true);
request.getSession().setAttribute("userName", userName);
//增加对象放入session中
request.getSession().setAttribute("user", systemUser);
//先添加到session,在跳转
return "redirect:/admin/main";
} else {
//验证失败,更新锁定信息
UserLockUtils.addFailEven(userName);
map.put("result", "登陆失败,检查账号密码是否有误。");
return "admin/login";
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 网关限流
结合网关实现限流,也可以从一定程度上减少爆破攻击的可能。如:spring cloud gateway
server:
port: 80
spring:
cloud:
gateway:
routes:
- id: requestRateLimiter
uri: http://127.0.0.1:80
order: 10000
predicates:
- Path=/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶的容积
redis-rate-limiter.burstCapacity: 3 # 流速 每秒
key-resolver: "#{@hostAddrKeyResolver}" #SPEL表达式对应的bean
application:
name: gateway-limiter
redis:
host: 172.17.0.139
port: 36379
database: 0
pass:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
@Component
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
System.out.println("限流解析访问的IP地址");
// 如果一秒内超过三次访问则限制,如返回429状态码
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
黑客仍有手段针对ip限流,如采用动态ip,可以基于这个思路采取用户限制等
# 形同虚设的登录
概念:
登录校验不全面,仅针对了某个页面或某个入口,如:只针对了首页做了登录校验,未对新增/删除类接口做身份校验
**预防:**做全局校验或仔细检查项目中的增删改查接口的登录校验
# 越权漏洞
概念:
越权漏洞是一种很常见的逻辑安全漏洞。是由于服务器端对客户提出的数据操作请求过分信任,忽略了
对该用户操作权限的判定,导致修改相关参数就可以拥有了其他账户的增、删、查、改功能,从而导致
越权漏洞。
分类:
提示
- 水平越权:尝试访问与自己拥有相同权限的用户的资源
- 垂直越权:低级别攻击者尝试访问高级别用户的资源
原因:
提示
- 后台页面未做登录校验或者校验不严谨
- 请求接口未做登录校验或者校验不严谨
- 对于登录用户未做权限校验(功能权限、操作权限、数据权限)
# 预防策略
提示
- 使用成熟的权限管理框架如shiro、spring security等
- 所有后台请求统一拦截,避免遗漏
- 用户进行访问操作的凭证(如用户ID)优先采用在服务端关联session的方式获取
- 用户进行访问操作的访问参数(如订单号等)时,采用难以猜测的构造方式,如UUID方式
- 应对客户端提交的凭证与会话的权限进行严格的验证,如提交的订单号是否为隶属于登录用户
- 权限不仅仅是菜单的显示与否,更重要的页面的能否访问
# 其他的预防建议
提示
- 后台入口隐蔽
- 域名、url、IP限制
- 后台网址与前台网址要独立
- 记录操作日志并定期审查日志
- 定期更换密码
- 隐藏异常信息
- html页面源码、JS代码等不要泄露敏感信息
- 网络传输中明文密码很容易被窃取,一般在传输前先对密码做加密或Hash处理,如页面做md5或
者App做加密