MySQL中间人攻击

MySQL中间人攻击

详情

https://github.com/actiontech/sqle

SQLE由上海爱可生信息技术股份有限公司(以下简称爱可生公司)出品和维护,是爱可生公司“云树SQL审核软件”(简称:CTREE SQLE)软件产品的开源版本。SQLE 是一个支持多场景,原生支持 MySQL 审核且数据库类型可扩展的 SQL 审核工具。

登录SQLE后开可以看到数据库连接地址,用户信息,但无法获取用户密码。

/v1/instances (GET)接口源码:

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
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
func GetInstances(c echo.Context) error {
req := new(GetInstancesReqV1)
if err := controller.BindAndValidateReq(c, req); err != nil {
return err
}
s := model.GetStorage()

user, err := controller.GetCurrentUser(c)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}

var offset uint32
if req.PageIndex >= 1 {
offset = req.PageSize * (req.PageIndex - 1)
}
data := map[string]interface{}{
"filter_instance_name": req.FilterInstanceName,
"filter_db_host": req.FilterDBHost,
"filter_db_port": req.FilterDBPort,
"filter_db_user": req.FilterDBUser,
"filter_workflow_template_name": req.FilterWorkflowTemplateName,
"filter_rule_template_name": req.FilterRuleTemplateName,
"filter_role_name": req.FilterRoleName,
"filter_db_type": req.FilterDBType,
"current_user_id": user.ID,
"check_user_can_access": user.Name != model.DefaultAdminUser,
"limit": req.PageSize,
"offset": offset,
}

instances, count, err := s.GetInstancesByReq(data, user)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}

instancesReq := []InstanceResV1{}
for _, instance := range instances {
instanceReq := InstanceResV1{
Name: instance.Name,
Desc: instance.Desc,
Host: instance.Host,
Port: instance.Port,
User: instance.User,
WorkflowTemplateName: instance.WorkflowTemplateName.String,
Roles: instance.RoleNames,
RuleTemplates: instance.RuleTemplateNames,
}
instancesReq = append(instancesReq, instanceReq)
}
return c.JSON(http.StatusOK, &GetInstancesResV1{
BaseRes: controller.NewBaseReq(nil),
Data: instancesReq,
TotalNums: count,
})
}

修改数据源接口/v1/instances/ (PATCH) 可以在不传入(不修改)数据库密码的情况下修改数据库地址与端口, 源码:

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
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
func UpdateInstance(c echo.Context) error {
req := new(UpdateInstanceReqV1)
if err := controller.BindAndValidateReq(c, req); err != nil {
return err
}

s := model.GetStorage()
instanceName := c.Param("instance_name")
instance, exist, err := s.GetInstanceByName(instanceName)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
if !exist {
return controller.JSONBaseErrorReq(c, errInstanceNotExist)
}

if !CheckInstanceCanBindOneRuleTemplate(req.RuleTemplates) {
return controller.JSONBaseErrorReq(c, errInstanceBind)
}

ruleTemplates, err := s.GetRuleTemplatesByNames(req.RuleTemplates)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
err = CheckInstanceAndRuleTemplateDbType(ruleTemplates, instance)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}

updateMap := map[string]interface{}{}
if req.Desc != nil {
updateMap["desc"] = *req.Desc
}
if req.Host != nil {
updateMap["db_host"] = *req.Host
}
if req.Port != nil {
updateMap["db_port"] = *req.Port
}
if req.User != nil {
updateMap["db_user"] = *req.User
}
if req.Password != nil {
password, err := utils.AesEncrypt(*req.Password)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
updateMap["db_password"] = password
}

if req.WorkflowTemplateName != nil {
// Workflow template name empty is unbound instance workflow template.
if *req.WorkflowTemplateName == "" {
updateMap["workflow_template_id"] = 0
} else {
workflowTemplate, exist, err := s.GetWorkflowTemplateByName(*req.WorkflowTemplateName)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
if !exist {
return controller.JSONBaseErrorReq(c, errors.New(errors.DataNotExist,
fmt.Errorf("workflow template is not exist")))
}
updateMap["workflow_template_id"] = workflowTemplate.ID
}
}

if req.RuleTemplates != nil {
ruleTemplates, err := s.GetAndCheckRuleTemplateExist(req.RuleTemplates)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
err = s.UpdateInstanceRuleTemplates(instance, ruleTemplates...)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
}
if req.Roles != nil {
roles, err := s.GetAndCheckRoleExist(req.Roles)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
err = s.UpdateInstanceRoles(instance, roles...)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
}

if req.AdditionalParams != nil {
additionalParams := driver.AllAdditionalParams()[instance.DbType]
for _, additionalParam := range req.AdditionalParams {
err = additionalParams.SetParamValue(additionalParam.Name, additionalParam.Value)
if err != nil {
return controller.JSONBaseErrorReq(c, errors.New(errors.DataInvalid, err))
}
}
updateMap["additional_params"] = additionalParams
}

err = s.UpdateInstanceById(instance.ID, updateMap)
if err != nil {
return controller.JSONBaseErrorReq(c, err)
}
return c.JSON(http.StatusOK, controller.NewBaseReq(nil))
}

在了解MySQL的认证流程后,发现只需要搭建一个虚假的MySQL服务器,即可利用保存在SQLE中的MySQL账户执行任意SQL。

MySQL认证流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
数据库中保存的密码是用 SHA1(SHA1(password)) 加密的,其流程为:

1. 服务器发送随机字符串 (scramble) 给客户端。
2. 客户端作如下计算,然后客户端将 token 发送给服务端。

stage1_hash = SHA1(明文密码)

token = SHA1(scramble + SHA1(stage1_hash)) XOR stage1_hash

3. 服务端作如下计算,比对 SHA1(stage1_hash) 和 mysql.user.password 是否相同。

stage1_hash = token XOR SHA1(scramble + mysql.user.password)

这里实际用到了异或的自反性: A XOR B XOR B = A ,对于给定的数 A,用同样的运算因子 B 作两次异或运算后仍得到 A 本身。对于当前情况的话,实际的计算过程如下。

token = SHA1(scramble + SHA1(SHA1(password))) XOR SHA1(password) // 客户端返回的值
= PASSWORD XOR SHA1(password)

stage1_hash = token XOR SHA1(scramble + mysql.user.password) = token XOR PASSWORD
= [PASSWORD XOR SHA1(password)] XOR PASSWORD
= SHA1(password)
因此,校验时,只需要 SHA1(stage1_hash) 与 mysql.user.password 比较一下即可。

利用代码

https://github.com/xjohjrdy/PyMySQL

demo

参考连接

MySQL 通讯协议

PyMySQL