diff --git a/plugins/go/veinmind-escalate/pkg/priv.go b/plugins/go/veinmind-escalate/pkg/priv.go index 833ba0bb..2bd22a02 100644 --- a/plugins/go/veinmind-escalate/pkg/priv.go +++ b/plugins/go/veinmind-escalate/pkg/priv.go @@ -3,7 +3,6 @@ package pkg import ( "bufio" "fmt" - "io/ioutil" "os" "path/filepath" "regexp" @@ -172,7 +171,7 @@ func isPrivileged(container api.Container) bool { return false } - status, err := ioutil.ReadFile(filepath.Join(func() string { + status, err := os.ReadFile(filepath.Join(func() string { fs := os.Getenv("LIBVEINMIND_HOST_ROOTFS") if fs == "" { return "/" diff --git a/plugins/go/veinmind-malicious/go.mod b/plugins/go/veinmind-malicious/go.mod index 47784d42..55a7ac70 100644 --- a/plugins/go/veinmind-malicious/go.mod +++ b/plugins/go/veinmind-malicious/go.mod @@ -8,6 +8,7 @@ require ( github.com/chaitin/libveinmind v1.5.6 github.com/chaitin/veinmind-common-go v1.4.2 github.com/joho/godotenv v1.4.0 + github.com/mattn/go-colorable v0.1.6 // indirect github.com/testwill/go-clamd v1.0.0 gorm.io/driver/sqlite v1.4.4 gorm.io/gorm v1.24.3 diff --git a/plugins/go/veinmind-malicious/go.sum b/plugins/go/veinmind-malicious/go.sum index f054c5ff..f596d5ac 100644 --- a/plugins/go/veinmind-malicious/go.sum +++ b/plugins/go/veinmind-malicious/go.sum @@ -697,11 +697,13 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -1275,6 +1277,7 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/plugins/go/veinmind-weakpass/hash/all.go b/plugins/go/veinmind-weakpass/hash/all.go index ffa2ace2..3eaf0472 100644 --- a/plugins/go/veinmind-weakpass/hash/all.go +++ b/plugins/go/veinmind-weakpass/hash/all.go @@ -3,6 +3,8 @@ package hash // All 注册所有的hash方法 service模块可以通过hash的ID获取具体的hash实例 var All = []Hash{ &Plain{}, - &MysqlNative{}, + &MySQL{}, + &MysqlNative{}, // This is the one we want to remove + &CachingSha2Password{}, // This is the one we want to remove &Shadow{}, } diff --git a/plugins/go/veinmind-weakpass/hash/base.go b/plugins/go/veinmind-weakpass/hash/base.go index 2b567d0d..83c68fe6 100644 --- a/plugins/go/veinmind-weakpass/hash/base.go +++ b/plugins/go/veinmind-weakpass/hash/base.go @@ -1,9 +1,9 @@ package hash type Hash interface { - // Match 密码匹配方式 - Match(hash, guess string) (flag bool, err error) - // ID 加密算法的ID ID() string + + // Match 密码匹配方式 + Match(hash, guess string) (flag bool, err error) } diff --git a/plugins/go/veinmind-weakpass/hash/hash_test.go b/plugins/go/veinmind-weakpass/hash/hash_test.go index eb5c8f8a..78bb8648 100644 --- a/plugins/go/veinmind-weakpass/hash/hash_test.go +++ b/plugins/go/veinmind-weakpass/hash/hash_test.go @@ -1,18 +1,38 @@ package hash import ( + "encoding/base64" "testing" "github.com/stretchr/testify/assert" ) -func TestHashMysqlNativePassword_Hash(t *testing.T) { +func TestHashMysqlNative(t *testing.T) { mysqlNative := &MysqlNative{} // password which got from file mysql.ibd in docker image stringGotFromFile := "*6bb4837eb74329105ee4568dda7dc67ed2ca2ad9" stringInDict := "123456" find, _ := mysqlNative.Match(stringGotFromFile, stringInDict) assert.True(t, find) + + stringGotFromFile = "6bb4837eb74329105ee4568dda7dc67ed2ca2ad9" + find, _ = mysqlNative.Match(stringGotFromFile, stringInDict) + assert.False(t, find) +} + +func TestHashCachingSha2Password(t *testing.T) { + cachingSha2Password := &CachingSha2Password{} + // password which got from file mysql.ibd in docker image + b64PwdFromFile := "JEEkMDA1JFodUGBzOkJlVwx/bwRYfwg8f3hoV3ZxRUN3c3JGa3pwa0kuVWxXbEpSeEM1ZkVYVUhscU42WFVuaGpHTS5zNwFixZPBAQICRkAAAAAgfAAAOPokJQ==" + stringInDict := "123456" + pwd, err := base64.StdEncoding.DecodeString(b64PwdFromFile) + if err != nil { + t.Fatal(err) + } + find, _ := cachingSha2Password.Match(string(pwd), stringInDict) + assert.True(t, find) + find, _ = cachingSha2Password.Match(string(pwd), "12345678") + assert.False(t, find) } func TestHashShadow(t *testing.T) { diff --git a/plugins/go/veinmind-weakpass/hash/mysql_native_password.go b/plugins/go/veinmind-weakpass/hash/mysql_native_password.go index 92d9e8f9..c82779e3 100644 --- a/plugins/go/veinmind-weakpass/hash/mysql_native_password.go +++ b/plugins/go/veinmind-weakpass/hash/mysql_native_password.go @@ -1,12 +1,36 @@ package hash import ( + "bytes" "crypto/sha1" + "crypto/sha256" "errors" "fmt" + "strconv" "strings" ) +var _ Hash = (*MySQL)(nil) + +type MySQL struct { +} + +func (m *MySQL) ID() string { + return "mysql" +} + +func (m *MySQL) Match(hash, guess string) (flag bool, err error) { + var checker Hash + if len(hash) > 3 && hash[:3] == "$A$" { + checker = &CachingSha2Password{} + } else { + checker = &MysqlNative{} + } + return checker.Match(hash, guess) +} + +var _ Hash = (*MysqlNative)(nil) + type MysqlNative struct { } @@ -25,3 +49,192 @@ func (i *MysqlNative) Match(hash, guess string) (flag bool, err error) { } return false, errors.New("mysql_passwd: malformed entry ") } + +var _ Hash = (*CachingSha2Password)(nil) + +type CachingSha2Password struct { +} + +func (i *CachingSha2Password) ID() string { + return "caching_sha2_password" +} + +func (i *CachingSha2Password) Match(hash, guess string) (flag bool, err error) { + if hash[:3] == "$A$" { + return checkHashingPassword([]byte(hash[:70]), guess) + } + return false, errors.New("mysql_passwd: malformed entry ") +} + +const ( + // MIXCHARS is the number of characters to use in the mix + MIXCHARS = 32 + // SALT_LENGTH is the length of the salt + SALT_LENGTH = 20 //nolint: revive + // ITERATION_MULTIPLIER is the number of iterations to use + ITERATION_MULTIPLIER = 1000 //nolint: revive +) + +func b64From24bit(b []byte, n int, buf *bytes.Buffer) { + b64t := []byte("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + + w := (int64(b[0]) << 16) | (int64(b[1]) << 8) | int64(b[2]) + for n > 0 { + n-- + buf.WriteByte(b64t[w&0x3f]) + w >>= 6 + } +} + +// sha256Hash is an util function to calculate sha256 hash. +func sha256Hash(input []byte) []byte { + res := sha256.Sum256(input) + return res[:] +} + +// 'hash' function should return an array with 32 bytes, the same as SHA-256 +// From https://github.com/pingcap/tidb/blob/ff78940594893cb970df58000dbc69cd5631e696/parser/auth/caching_sha2.go#L72 with some fix +func hashCrypt(plaintext string, salt []byte, iterations int, hash func([]byte) []byte) string { + // Numbers in the comments refer to the description of the algorithm on https://www.akkadia.org/drepper/SHA-crypt.txt + + // 1, 2, 3 + bufA := bytes.NewBuffer(make([]byte, 0, 4096)) + bufA.Write([]byte(plaintext)) + bufA.Write(salt) + + // 4, 5, 6, 7, 8 + bufB := bytes.NewBuffer(make([]byte, 0, 4096)) + bufB.Write([]byte(plaintext)) + bufB.Write(salt) + bufB.Write([]byte(plaintext)) + sumB := hash(bufB.Bytes()) + bufB.Reset() + + // 9, 10 + var i int + for i = len(plaintext); i > MIXCHARS; i -= MIXCHARS { + bufA.Write(sumB[:MIXCHARS]) + } + bufA.Write(sumB[:i]) + + // 11 + for i = len(plaintext); i > 0; i >>= 1 { + if i%2 == 0 { + bufA.Write([]byte(plaintext)) + } else { + bufA.Write(sumB[:]) + } + } + + // 12 + sumA := hash(bufA.Bytes()) + bufA.Reset() + + // 13, 14, 15 + bufDP := bufA + for range []byte(plaintext) { + bufDP.Write([]byte(plaintext)) + } + sumDP := hash(bufDP.Bytes()) + bufDP.Reset() + + // 16 + p := make([]byte, 0, sha256.Size) + for i = len(plaintext); i > 0; i -= MIXCHARS { + if i > MIXCHARS { + p = append(p, sumDP[:]...) + } else { + p = append(p, sumDP[0:i]...) + } + } + + // 17, 18, 19 + bufDS := bufA + for i = 0; i < 16+int(sumA[0]); i++ { + bufDS.Write(salt) + } + sumDS := hash(bufDS.Bytes()) + bufDS.Reset() + + // 20 + s := make([]byte, 0, 32) + for i = len(salt); i > 0; i -= MIXCHARS { + if i > MIXCHARS { + s = append(s, sumDS[:]...) + } else { + s = append(s, sumDS[0:i]...) + } + } + + // 21 + bufC := bufA + var sumC []byte + for i = 0; i < iterations; i++ { + bufC.Reset() + if i&1 != 0 { + bufC.Write(p) + } else { + bufC.Write(sumA[:]) + } + if i%3 != 0 { + bufC.Write(s) + } + if i%7 != 0 { + bufC.Write(p) + } + if i&1 != 0 { + bufC.Write(sumA[:]) + } else { + bufC.Write(p) + } + sumC = hash(bufC.Bytes()) + sumA = sumC + } + + // 22 + buf := bytes.NewBuffer(make([]byte, 0, 100)) + buf.Write([]byte{'$', 'A', '$'}) + rounds := fmt.Sprintf("%03X", iterations/ITERATION_MULTIPLIER) + buf.Write([]byte(rounds)) + buf.Write([]byte{'$'}) + buf.Write(salt) + + b64From24bit([]byte{sumC[0], sumC[10], sumC[20]}, 4, buf) + b64From24bit([]byte{sumC[21], sumC[1], sumC[11]}, 4, buf) + b64From24bit([]byte{sumC[12], sumC[22], sumC[2]}, 4, buf) + b64From24bit([]byte{sumC[3], sumC[13], sumC[23]}, 4, buf) + b64From24bit([]byte{sumC[24], sumC[4], sumC[14]}, 4, buf) + b64From24bit([]byte{sumC[15], sumC[25], sumC[5]}, 4, buf) + b64From24bit([]byte{sumC[6], sumC[16], sumC[26]}, 4, buf) + b64From24bit([]byte{sumC[27], sumC[7], sumC[17]}, 4, buf) + b64From24bit([]byte{sumC[18], sumC[28], sumC[8]}, 4, buf) + b64From24bit([]byte{sumC[9], sumC[19], sumC[29]}, 4, buf) + b64From24bit([]byte{0, sumC[31], sumC[30]}, 3, buf) + + return buf.String() +} + +// checkHashingPassword checks if a caching_sha2_password or tidb_sm3_password authentication string matches a password +// From https://github.com/pingcap/tidb/blob/ff78940594893cb970df58000dbc69cd5631e696/parser/auth/caching_sha2.go#L223 with some fix +func checkHashingPassword(pwHash []byte, password string) (bool, error) { + pwHashParts := bytes.Split(pwHash, []byte("$")) + if len(pwHashParts) < 3 { + return false, errors.New("failed to decode hash parts") + } + + hashType := string(pwHashParts[1]) + if hashType != "A" { + return false, errors.New("digest type is incompatible") + } + + iterations, err := strconv.ParseInt(string(pwHashParts[2]), 16, 64) + if err != nil { + return false, errors.New("failed to decode iterations") + } + iterations = iterations * ITERATION_MULTIPLIER + salt := pwHashParts[3][:SALT_LENGTH] + + newHash := hashCrypt(password, salt, int(iterations), sha256Hash) + + return bytes.Equal(pwHash, []byte(newHash)), nil +} diff --git a/plugins/go/veinmind-weakpass/hash/passwd_windows.go b/plugins/go/veinmind-weakpass/hash/passwd_windows.go new file mode 100644 index 00000000..78bdffdb --- /dev/null +++ b/plugins/go/veinmind-weakpass/hash/passwd_windows.go @@ -0,0 +1,32 @@ +package hash + +import ( + "errors" +) + +// PasswordMethod represents the entryption method. +type PasswordMethod uint8 + +// Password represents the data field inside. +type Password struct { + // Method of current password. + Method PasswordMethod + + // MethodString of the current password. + MethodString string + + // Salt inside the password. + Salt string + + // Hash hased value of the password. + Hash string +} + +func (pw *Password) Match(guesses []string) (string, bool) { + panic("not implemented") + return "", false +} + +func ParsePassword(pass *Password, phrase string) error { + return errors.New("not implemented") +} diff --git a/plugins/go/veinmind-weakpass/pkg/myisam/myisamParser.go b/plugins/go/veinmind-weakpass/pkg/myisam/myisamParser.go index 52ed4d86..5eccf57f 100644 --- a/plugins/go/veinmind-weakpass/pkg/myisam/myisamParser.go +++ b/plugins/go/veinmind-weakpass/pkg/myisam/myisamParser.go @@ -1,18 +1,19 @@ package myisam import ( + "errors" "io" ) const ( // EmptyPasswordPlaceholder 用于没有修改过密码的 user page - EmptyPasswordPlaceholder = "THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE" + EmptyPasswordPlaceholder = "*THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE" // LocalHost 用于识别Host是否为仅限本地登陆 LocalHost = "localhost" ) -type MysqlInfo struct { +type UserInfo struct { Host string Name string Plugin string @@ -27,16 +28,8 @@ type Record struct { DataBegin int } -type Result struct { - Host string - User string - Password string - Native bool -} - // ParseUserFile 从文件中解析用户名和密码 -// TODO: 目前仅支持MYSQL5默认使用的mysql_native_password插件,后续需要支持其他插件的解析。 -func ParseUserFile(f io.Reader) (infos []MysqlInfo, err error) { +func ParseUserFile(f io.Reader) (infos []*UserInfo, err error) { content, err := io.ReadAll(f) if err != nil { return nil, err @@ -47,13 +40,9 @@ func ParseUserFile(f io.Reader) (infos []MysqlInfo, err error) { record := dispatchRecord(content, idx) recType := record.RecType if 0 < recType && recType <= 6 { - info := MysqlInfo{} - res := parseRecord(content, record) - info.Host = res.Host - info.Plugin = "mysql_native_password" - info.Name = res.User - info.Password = res.Password - if info.Password != EmptyPasswordPlaceholder && info.Host != LocalHost { + if info, err := parseRecord(content, record); err != nil { + return infos, err + } else { infos = append(infos, info) } } @@ -63,13 +52,6 @@ func ParseUserFile(f io.Reader) (infos []MysqlInfo, err error) { return infos, nil } -func min(a, b int) int { - if a < b { - return a - } - return b -} - func readLen(content []byte, begin int, l int) int { sumLen := 0 for _, bit := range content[begin : begin+l] { @@ -84,7 +66,7 @@ func pad(dataLen int) int { return (byteLen + ((dataLen - (byteLen << 2)) & 1)) << 2 } -func readRecord(content []byte, idx int, headerLen int, dataPos int, dataLen int, nextPos int, unusedLen int) (record Record) { +func readRecord(content []byte, idx int, headerLen int, dataPos int, dataLen int, nextPos int, unusedLen int) (record *Record) { recType := content[idx] dataLenValue := readLen(content, idx+dataPos, dataLen) unusedLenValue := unusedLen @@ -92,27 +74,28 @@ func readRecord(content []byte, idx int, headerLen int, dataPos int, dataLen int unusedLenValue = int(content[idx+unusedLen]) } blockLen := pad(headerLen + dataLenValue + unusedLenValue) - nextRec := Record{} + var nextRec *Record if nextPos > 0 { nextRec = dispatchRecord(content, readLen(content, idx+nextPos, 8)) } - record = Record{ + record = &Record{ RecType: recType, BlockLen: blockLen, DataLen: dataLenValue, - NextRec: &nextRec, + NextRec: nextRec, DataBegin: idx + headerLen, } return } -func dispatchRecord(content []byte, idx int) (record Record) { +// Related info: https://github.com/twitter-forks/mysql/blob/865aae5f23e2091e1316ca0e6c6651d57f786c76/storage/myisam/mi_dynrec.c#LL1890C1-L1890C1 +func dispatchRecord(content []byte, idx int) (record *Record) { recType := content[idx] switch recType { case 0: - record = readRecord(content, idx, 20, 1, 3, -1, 0) + record = readRecord(content, idx, 0, 1, 3, -1, 0) case 1: record = readRecord(content, idx, 3, 1, 2, -1, 0) case 2: @@ -138,57 +121,72 @@ func dispatchRecord(content []byte, idx int) (record Record) { case 12: record = readRecord(content, idx, 12, 1, 3, 4, 0) case 13: - record = readRecord(content, idx, 16, 5, 3, 9, 0) + record = readRecord(content, idx, 16, 5, 3, 8, 0) default: - record = Record{} + record = nil } return } -func parseRecord(content []byte, rec Record) (result Result) { - first := rec.DataBegin + 3 - hostLen := int(content[first]) - host := string(content[first+1 : first+1+hostLen]) - - userLen := int(content[first+hostLen+1]) - user := string(content[first+hostLen+1+1 : first+hostLen+1+1+userLen]) - - native := false - passwordMaxLen := 40 - var password []byte - idx := first + hostLen + 1 + 1 + userLen - for { - last := rec.DataBegin + rec.DataLen - passwordLen := len(password) - if passwordLen == 0 { - for idx < last { - if content[idx] == 21 { - native = true - } - if content[idx] == 42 { +var RecordDataBrokenErr = errors.New("record data broken") + +// parseRecord parse user.MYD, return nil if error happened +func parseRecord(content []byte, rec *Record) (result *UserInfo, err error) { + var trueContent []byte + for rec != nil { + // 这里应该根据rec还原数据 content的内容是不能直接使用的 + // Related info: https://github.com/twitter-forks/mysql/blob/865aae5f23e2091e1316ca0e6c6651d57f786c76/storage/myisam/mi_dynrec.c#LL1890C1-L1890C1 + trueContent = append(trueContent, content[rec.DataBegin:rec.DataBegin+rec.DataLen]...) + rec = rec.NextRec + } + + hostLenPos := 3 + if len(trueContent) <= hostLenPos { + return nil, RecordDataBrokenErr + } + hostLen := int(trueContent[hostLenPos]) + + userLenPos := hostLenPos + hostLen + 1 // 可以简化 但是这么写便于阅读 + if len(trueContent) <= userLenPos { + return nil, RecordDataBrokenErr + } + userLen := int(trueContent[userLenPos]) + + if len(trueContent) <= userLenPos+1+userLen { + return nil, RecordDataBrokenErr + } + host := string(trueContent[hostLenPos+1 : hostLenPos+1+hostLen]) + user := string(trueContent[userLenPos+1 : userLenPos+1+userLen]) + + // caching_sha2_password在>=5.6版本被支持 也就是自5.6版开始 user表结构有所变化,加入authentication_string列 + // authentication_string列有一个固定特征是长度为21 且跟在user列后面(user数据结束后会有很长一段2) + // 如果是<5.6版本的表,user列后面直接紧跟着就是42了(*) + plugin := "mysql_native_password" + passwdLenPos := userLenPos + userLen + 1 // 先当作是<5.6的版本提取 + var password string + if passwdLen := int(trueContent[passwdLenPos]); passwdLen == 42 { // 判断出来不是>=5.6的版本 + password = string(trueContent[passwdLenPos : passwdLenPos+41]) + } else { // >=5.6版本 authentication_string列在前 password列在后 + passwdLenPos++ + for { + if passwdLenPos < len(trueContent) { + if trueContent[passwdLenPos] == 21 { // 读到了authentication_string列 + plugin = string(trueContent[passwdLenPos+1 : passwdLenPos+1+21]) + passwdLenPos += 1 + 21 + passwdLen = int(trueContent[passwdLenPos]) // TEXT类型 这里认为长度密码字段长度不会超过256 不计算第二位 https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html + password = string(trueContent[passwdLenPos+2 : passwdLenPos+2+passwdLen]) break } - idx++ + passwdLenPos++ + } else { + return nil, RecordDataBrokenErr } } - - if idx+1 <= min(last, passwordMaxLen-passwordLen+idx+1) { - password = append(password, content[idx:min(last, passwordMaxLen-passwordLen+idx+1)]...) - } - - if rec.NextRec != nil { - break - } else { - rec = *rec.NextRec - idx = rec.DataBegin - } } - result = Result{ + return &UserInfo{ Host: host, - User: user, - Password: string(password), - Native: native, - } - - return + Name: user, + Password: password, + Plugin: plugin, + }, nil } diff --git a/plugins/go/veinmind-weakpass/service/mysql5.go b/plugins/go/veinmind-weakpass/service/mysql5.go index d9a97629..f41464d9 100644 --- a/plugins/go/veinmind-weakpass/service/mysql5.go +++ b/plugins/go/veinmind-weakpass/service/mysql5.go @@ -10,16 +10,14 @@ import ( ) type mysql5Service struct { - name string - filepath []string } func (i *mysql5Service) Name() string { - return i.name + return "mysql5" } func (i *mysql5Service) FilePath() (paths []string) { - return i.filepath + return []string{"/var/lib/mysql/mysql/user.MYD"} } func (i *mysql5Service) GetRecords(file io.Reader) (records []model.Record, err error) { mysqlInfos, err := myisam.ParseUserFile(file) @@ -28,6 +26,9 @@ func (i *mysql5Service) GetRecords(file io.Reader) (records []model.Record, err } tmp := model.Record{} for _, info := range mysqlInfos { + if info.Password == myisam.EmptyPasswordPlaceholder || info.Host == myisam.LocalHost || info.Host == "127.0.0.1" { + continue + } tmp.Username = info.Name tmp.Password = strings.ToLower(info.Password) records = append(records, tmp) @@ -37,8 +38,6 @@ func (i *mysql5Service) GetRecords(file io.Reader) (records []model.Record, err func init() { mod := &mysql5Service{} - ServiceMatcherMap["mysql5"] = "mysql_native_password" - mod.name = "mysql5" - mod.filepath = []string{"/var/lib/mysql/mysql/user.MYD"} + ServiceMatcherMap[mod.Name()] = "mysql" Register("mysql", mod) } diff --git a/plugins/go/veinmind-weakpass/service/mysql8.go b/plugins/go/veinmind-weakpass/service/mysql8.go index ed4bcc5d..2b3872b9 100644 --- a/plugins/go/veinmind-weakpass/service/mysql8.go +++ b/plugins/go/veinmind-weakpass/service/mysql8.go @@ -9,18 +9,19 @@ import ( "github.com/chaitin/veinmind-tools/plugins/go/veinmind-weakpass/model" ) +var _ IService = (*mysql8Service)(nil) + type mysql8Service struct { - name string - filepath []string } func (i *mysql8Service) Name() string { - return i.name + return "mysql8" } func (i *mysql8Service) FilePath() (paths []string) { - return i.filepath + return []string{"/var/lib/mysql/mysql.ibd", "/var/lib/mysql/mysql2.ibd"} } + func (i *mysql8Service) GetRecords(file io.Reader) (records []model.Record, err error) { page, err := innodb.FindUserPage(file) if err != nil { @@ -41,10 +42,7 @@ func (i *mysql8Service) GetRecords(file io.Reader) (records []model.Record, err } func init() { - // TODO: Mysql8 mod := &mysql8Service{} - ServiceMatcherMap["mysql8"] = "mysql_native_password" - mod.name = "mysql8" - mod.filepath = []string{"/var/lib/mysql/mysql.ibd"} + ServiceMatcherMap[mod.Name()] = "mysql" Register("mysql", mod) } diff --git a/plugins/go/veinmind-weakpass/test/innodbParse_test.go b/plugins/go/veinmind-weakpass/test/innodbParse_test.go new file mode 100644 index 00000000..e31e6306 --- /dev/null +++ b/plugins/go/veinmind-weakpass/test/innodbParse_test.go @@ -0,0 +1,77 @@ +package test + +import ( + "github.com/chaitin/veinmind-tools/plugins/go/veinmind-weakpass/pkg/innodb" + "github.com/chaitin/veinmind-tools/plugins/go/veinmind-weakpass/pkg/myisam" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestParseMySQLInnoDB(t *testing.T) { + f, err := os.Open("./mysql8.ibd") + if err != nil { + return + } + defer f.Close() + + page, err := innodb.FindUserPage(f) + infos, err := innodb.ParseUserPage(page.Pagedata) + if err != nil { + return + } + for _, info := range infos { + if info.Password[:3] == "$A$" { + t.Log("caching_sha2_password") + } else { + t.Log("mysql_native_password") + } + t.Log(info) + } +} + +func TestParseMySQLInnoDB5(t *testing.T) { + f, err := os.Open("./mysql.ibd") + if err != nil { + return + } + defer f.Close() + + page, err := innodb.FindUserPage(f) + infos, err := innodb.ParseUserPage(page.Pagedata) + if err != nil { + return + } + for _, info := range infos { + if info.Password[:3] == "$A$" { + t.Log("caching_sha2_password") + } else { + t.Log("mysql_native_password") + } + t.Log(info) + } +} + +func TestParseMyISAM55(t *testing.T) { + mysqlMyd, err := os.Open("../test/mysql5_5_myisam.MYD") + assert.Nil(t, err) + + infos, err := myisam.ParseUserFile(mysqlMyd) + if err != nil { + t.Error(err) + } + assert.Len(t, infos, 2) + assert.Equal(t, "*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9", infos[0].Password) +} + +func TestParseMyISAM56(t *testing.T) { + mysqlMyd, err := os.Open("../test/user.MYD") + assert.Nil(t, err) + + infos, err := myisam.ParseUserFile(mysqlMyd) + if err != nil { + t.Error(err) + } + assert.Len(t, infos, 4) + assert.Equal(t, "*81F5E21E35407D884A6CD4A731AEBFB6AF209E1B", infos[0].Password) +} diff --git a/plugins/go/veinmind-weakpass/test/mysql5_5_myisam.MYD b/plugins/go/veinmind-weakpass/test/mysql5_5_myisam.MYD new file mode 100644 index 00000000..ac41cf6e Binary files /dev/null and b/plugins/go/veinmind-weakpass/test/mysql5_5_myisam.MYD differ diff --git a/plugins/go/veinmind-weakpass/test/mysql8.ibd b/plugins/go/veinmind-weakpass/test/mysql8.ibd new file mode 100644 index 00000000..4c146302 Binary files /dev/null and b/plugins/go/veinmind-weakpass/test/mysql8.ibd differ