教你MySQL忘记密码怎么处理

导读

MySQL 密码遗忘不用慌!这篇文章为你呈上一份全面且详尽的指南,手把手教你轻松找回密码。我们会细致地介绍多种行之有效的方法,比如借助安全模式重置密码,通过修改配置文件重设密码等。只要你仔细阅读本文,往后再也不用担心因密码丢失而陷入困境,让你的 MySQL 使用之路畅通无阻 。

MySQL重启版

通过跳过密码验证来重置密码

  1. 停止MySQL服务:

    • 在Linux上,您可以使用systemctl stop mysqld或service mysql stop命令来停止MySQL服务。
    • 在Windows上,您可以在服务管理器中找到MySQL服务并停止它。
  2. 编辑MySQL配置文件:

    • 找到MySQL的配置文件my.cnf(在Linux上通常位于/etc/my.cnf/etc/mysql/my.cnf,在Windows上可能位于MySQL安装目录下的my.ini)。
    • 在[mysqld]部分添加skip-grant-tables行,这将使MySQL在启动时跳过权限表,允许您无需密码即可登录。
  3. 启动MySQL服务:

    • 使用修改后的配置文件重新启动MySQL服务。
  4. 登录MySQL:

    • 使用mysql -u root命令登录MySQL,此时不需要密码。
  5. 重置密码:

    • 切换到mysql数据库:USE mysql;
    • 更新user表中的authentication_string字段(对于MySQL 5.7及更高版本)或password字段(对于旧版本):
    UPDATE user SET authentication_string=PASSWORD('新密码') WHERE User='root';
    -- 或者对于旧版本:
    -- UPDATE user SET password=PASSWORD('新密码') WHERE User='root';
    

    注意:从MySQL 5.7.6开始,password字段已被authentication_string替代,并且应使用ALTER USER语句来更改密

  6. 刷新权限:

    • 执行FLUSH PRIVILEGES;命令来使更改生效。
  7. 恢复MySQL配置并重启服务:

    • 从my.cnf文件中删除skip-grant-tables行。

    • 重新启动MySQL服务。

  8. 使用新密码登录:

    • 使用新设置的密码登录MySQL。

无需重启MySQL版

原理分析

linux

原理很简单, 既然验证的密码是在内存中的, 那我们找到该密码直接修改为我们需要的密码即可.

以上, 就是这么滴简单

其实难点在于怎么访问mysqld进程的内存. /proc/PID/ 下面有很多信息, 比如 io 表示这个进程读写了多少数据, 我们导入进度脚本就是查看的这个文件的rchar. 除此之外还有常用的stat,comm,fd等. 详情可以查看 内核官网.

File Content
clear_refs Clears page referenced bits shown in smaps output
cmdline Command line arguments
cpu Current and last cpu in which it was executed (2.4)(smp)
cwd Link to the current working directory
environ Values of environment variables
exe Link to the executable of this process
fd Directory, which contains all file descriptors
maps Memory maps to executables and library files (2.4)
mem Memory held by this process
root Link to the root directory of this process
stat Process status
statm Process memory status information
status Process status in human readable form
wchan Present with CONFIG_KALLSYMS=y: it shows the kernel function symbol the task is blocked in - or “0” if not blocked.
pagemap Page table
stack Report full stack trace, enable via CONFIG_STACKTRACE
smaps An extension based on maps, showing the memory consumption of each mapping and flags associated with it
smaps_rollup Accumulated smaps stats for all mappings of the process. This can be derived from smaps, but is faster and more convenient
numa_maps An extension based on maps, showing the memory locality and binding policy as well as mem usage (in pages) of each mapping.

其中有个 mapsmem 文件, 就是该进程所使用的内存映射和具体的内存fd. linux上一切皆文件, 硬件也是文件, 包括内存,磁盘等.

我们查看mysqld进程的maps文件,得到如下信息

我们以第一行为例:

00400000-00c6f000 r--p 00000000 fd:00 307653646                         /soft/mysql_3386/mysqlbase/mysql/bin/mysqld

00400000-00c6f000 表示内存使用的范围为(16进制): 00400000 –> 00c6f000

r–p 表示权限. 具体的含义如下:

r = read
w = write
x = execute
s = shared
p = private (copy on write)

00000000 表示offset. 对于进程来讲, 使用的内存应该是连续的, 而实际分配的内存是断断续续的. 所以就使用offset来表示内存的相对位置, 这样每个进程看到的内存都是连续的了.

比如第一块内存是0x00c6f000 -> 0x00400000, 占用了8843264字节, 那么第二块内存的位置就该是8843264开始, 也就是offset=8843264 (0x0086f000) 同理: 0x02a10000 - 0x00c6f000 + 0x0086f000 = 0x02610000

注: 该offset是相对于fd来说的

fd:00 表示dev,就是上面说的fd

307653646 表示inode,就是文件系统的inode

/soft/mysql_3386/mysqlbase/mysql/bin/mysqld 就是对应的文件了.

mysql

所以我们只需要遍历maps就可以知道mysqld进程的内存分配情况了, 然后读取mem文件对应位置的数据查找需要的数据即可.

我们直接将这部分操作整理为函数. 这里仅为演示原理. 实际使用的时候见后面演示部分即可.

# 在内存中查找某个关键词
def find_data_in_mem(pid,key):
        keysize = len(key)
        with open(f'/proc/{pid}/maps','r') as f:
                maps = f.readlines()

        result = []
        with open(f'/proc/{pid}/mem','rb') as f:
                for line in maps:
                        addr = line.split()[0]
                        _flags = line.split()[1]
                        if _flags != 'rw-p':
                                continue
                        start_addr,stop_addr = addr.split('-')
                        start_addr = int(start_addr,16)
                        stop_addr  = int(stop_addr ,16)
                        f.seek(start_addr,0)
                        data = f.read(stop_addr-start_addr)
                        offset = 0
                        while True:
                                offset = data.find(key,offset)
                                if offset != -1:
                                        result.append([start_addr,stop_addr,offset])
                                        offset += keysize
                                else:
                                        break
        return result

看着是不是很熟悉, 其实就是mysql.user表里面的数据, 但是mysql的认证并不是登录的时候,直接查询mysql.user里面的密码去匹配. 所以仅修改这里是没用的. 不然直接update mysql.user表修改密码不就得了, 干嘛还要 flush privileges 呢? 这个fllush去了哪来呢?

回顾一下之前我们讲的mysql连接协议可知, 密码验证的是二进制的,而非16进制的, 所以内存中还存在着二进制的加密密码. flush刷新的值应该就是这里. 所以我们查找的应该是二进制的密码.

这个位置并没有用户之类的信息,所以我们得修改所有密码为该值的账号密码. 比如u1和u2的密码都是123456, 那么修改u2的密码的时候,u1的密码也会被修改. 当然我们可以查询源码, 找到具体的对应关系. 但这都是后话了.

修改内存的话, 就是打开mem时候, 使用 r+b 即可, 即有写的权限, 然后f.write()即可, 就不单独演示了, 直接看后面的演示.

演示

理论是非常枯燥的, 所以我们来演示瞧瞧效果. 脚本见文末或者 github 上.

注意: –user指定user@host的时候, user和host都不需要加引号

查看用户的密码

查看的原理是遍历内存,找mysql.user表里面对应的账号记录. 如果没有查询过mysql.user表, 即mysql.user表不在内存里面的话, 是无法查询用户密码的. 当然可以查询ibd文件获取密码, frm的直接hexdump -C 就行, innodb的之前也提供过脚本的.

python3 online_modify_mysql_password.py --user u33@%

修改用户密码(方法1)

我们只是修改的flush处的密码, 所以如果再次flush的话, 我们修改的密码就失效了. 而且我们修改的是所有密码和u33一样的账号的, 所以我们还得登录数据库, 使用alter修改密码, 然后flush privileges刷新其它和u33密码一样的无辜者.

python3 online_modify_mysql_password.py --user u33@% --password newpassword_u33

修改用户密码(方法2)

还有种情况就是mysql.user不在内存中, 或者flush处的密码和mysql.user的不一致(比如使用update修改密码), 那么我们就需要人工提供mysql.user里面的密码(其实是flush处的密码).

python3 online_modify_mysql_password.py --user u33@% --password newpassword_u33 --old-password 0FE8A0B9017E2374037E9B151CBB384A05E6466B

存在多个mysqld进程的时候

如果服务器上存在多个mysqld进程, 则需要使用 --pid PID 执行实际要修改的账号所在实例的进程号.

python3 online_modify_mysql_password.py --user u33@% --password newpassword_u33 --pid 18721

总结

虽然本文提供了不需要重启数据库就能强制修改密码的方法, 但还是建议重启数据库(还能释放下内存). 目前测试了5.7.38 8.0.28 8.0.41 均成功了的. 目前 仅支持mysql_native_password插件 的密码.

如果使用本脚本修改密码后,未登录数据库,做alter和flush的话, 再次使用脚本时也需要加上–old-password

参考:

https://www.kernel.org/doc/html/latest/filesystems/proc.html

源码

https://github.com/ddcw/ddcw/tree/master/python/online_modify_mysql_password

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# writen by ddcw @https://github.com/ddcw
# 在线修改mysql密码的工具. 仅支持 mysql_native_password 插件的

import os
import sys
import struct
import hashlib
import binascii
import argparse

def _argparse():
	parser = argparse.ArgumentParser(add_help=False, description='在线修改mysqld进程的脚本')
	parser.add_argument('--help', '-h', action='store_true', dest="HELP", default=False,  help='show help')
	parser.add_argument('--password', '-p', dest="PASSWORD",  help='mysql new password')
	parser.add_argument('--old-password', dest="OLD_PASSWORD",  help='last modify password')
	parser.add_argument('--pid', dest="PID", help='mysql pid', type=int)
	parser.add_argument('--user', dest="USER", help='mysql account (user@host, root@localhost)')
	if parser.parse_args().HELP:
		parser.print_help()
		print('Example:')
		print(f'python3 f{sys.argv[0]} --user root@localhost')
		print(f'python3 f{sys.argv[0]} --user root@localhost --password 123456')
		print(f'python3 f{sys.argv[0]} --user root@localhost --password 123456 --pid `pidof mysqld`')
		sys.exit(0)
	if parser.parse_args().USER is None:
		print('必须使用 --user 指定用户')
		sys.exit(10)
	return parser.parse_args()

def encode_password(NEW_PASSWORD):
	return hashlib.sha1(hashlib.sha1(NEW_PASSWORD.encode()).digest()).digest()

# 在内存中查找某个关键词
def find_data_in_mem(pid,key):
	keysize = len(key)
	with open(f'/proc/{pid}/maps','r') as f:
		maps = f.readlines()

	result = []
	with open(f'/proc/{pid}/mem','rb') as f:
		for line in maps:
			addr = line.split()[0]
			_flags = line.split()[1]
			if _flags != 'rw-p':
				continue
			start_addr,stop_addr = addr.split('-')
			start_addr = int(start_addr,16)
			stop_addr  = int(stop_addr ,16)
			f.seek(start_addr,0)
			data = f.read(stop_addr-start_addr)
			offset = 0
			while True:
				offset = data.find(key,offset)
				if offset != -1:
					result.append([start_addr,stop_addr,offset])
					offset += keysize
				else:
					break
	return result

# 设置新密码, 是直接将旧密码改为新密码, 如果多个用户的密码是一样的, 则都会修改, 不修改mysql.user等信息
def set_new_password(OLD_PASSWORD,NEW_PASSWORD,pid):
	maps = find_data_in_mem(pid,OLD_PASSWORD)
	if len(maps) == 0:
		print('可能之前已经修改过了, 可以使用--old-password 指定上一次的密码')
		sys.exit(1)
	with open(f'/proc/{pid}/mem','r+b') as f:
		for start,stop,offset in maps:
			f.seek(start+offset-20,0)
			data = f.read(20)
			if data[-4:] != b'\x00\x00\x00\x00':
				continue
			# 5.7 255, 4, 0, 0, 0, 0
			# 8.0 41, 0, 0, 0, 0, 0, 0, 0
			print([ x for x in data ])
			f.seek(start+offset,0)
			f.write(NEW_PASSWORD)
	print(f'set new password succuss! ({binascii.hexlify(NEW_PASSWORD).decode()})')


def get_pid(): # 获取mysqld进程的pid
	pid = []
	for entry in os.listdir('/proc'):
		if not entry.isdigit():
			continue
		try:
			comm = '/proc/'+str(entry)+'/comm'
			with open(comm,'r') as f:
				if f.read() == 'mysqld\n':
					pid.append(entry)
		except:
			pass
	return pid

if __name__ == "__main__":
	parser = _argparse()
	user,host = parser.USER.split('@')
	flags = struct.pack('<B',len(host)) + host.encode() + struct.pack('<B',len(user)) + user.encode()
	PIDS = get_pid()
	pid = 0
	if parser.PID is not None:
		if str(parser.PID) in PIDS:
			pid = parser.PID
		else:
			print(f'pid:{parser.PID} not exists {PIDS}')
			sys.exit(0)
	elif len(PIDS) == 1:
		pid = PIDS[0]
	elif len(PIDS) == 0:
		print('当前不存在mysqld进程')
		sys.exit(2)
	else:
		print(f'当前存在多个mysqld进程, 请指定一个')
		sys.exit(3)
	MODIFY_PASSWORD = False # 是否要修改密码, 如果没有指定密码, 则仅查看即可. 若指定了密码, 则为强制修改
	NEW_PASSWORD = b''
	if parser.PASSWORD is not None:
		NEW_PASSWORD = encode_password(parser.PASSWORD)
		MODIFY_PASSWORD = True
	if parser.OLD_PASSWORD is not None:
		set_new_password(bytes.fromhex(parser.OLD_PASSWORD),NEW_PASSWORD,pid,)
		sys.exit(0)
	# 查看当前的密码
	maps = find_data_in_mem(pid,flags)
	if len(maps) == 0:
		print('没找到...')
		sys.exit(1)
	with open(f'/proc/{pid}/mem','rb') as f:
		for start,stop,offset in maps:
			f.seek(start,0)
			data = f.read(stop-start)
			MATCHED = True
			offset += len(flags)
			for i in range(29): # 29个权限
				if data[offset:offset+1] != b'\x01':
					MATCHED = False
					break
				else:
					offset += 2
			if not MATCHED:
				continue
			# 然后就是ssl,max_conn之类的信息
			for i in range(8):
				vsize = struct.unpack('<B',data[offset:offset+1])[0]
				offset += 1 + vsize
			# 然后就是mysql_native_password了
			vsize = struct.unpack('<B',data[offset:offset+1])[0]
			plugins = data[offset+1:offset+1+vsize].decode()
			offset += 1 + vsize
			if plugins != 'mysql_native_password':
				continue
			# 最后就是密码(password_expired之类的就不管了. 没必要)
			vsize = struct.unpack('<B',data[offset:offset+1])[0] # 肯定得是41, 就懒得验证了
			old_password = data[offset+1:offset+1+vsize].decode()
			print(f'{parser.USER} password:{old_password}  {start}-{stop}:{offset}') # mysql.user的信息
			if MODIFY_PASSWORD: # 要修改的密码实际上是二进制的, 修改page的是没用的
				set_new_password(bytes.fromhex(old_password[1:]),NEW_PASSWORD,pid,)
				# mysql.user也修改下, 不然再次修改的时候,就找不到位置了. 算逑!
				#with open(f'/proc/{pid}/mem','r+b') as fw:
				#	fw.seek(start+offset+1,0)
				#	fw.write(NEW_PASSWORD)
			break