1. 靶场环境与目标解析
拿到一个CTF题目,尤其是Web方向的,第一步永远是信息收集和环境理解。这道题目标题是“【CTF_SQL】[极客大挑战 2019]LoveSQL 1”,结合相关热词,核心指向非常明确:这是一道关于SQL注入的CTF题目,并且很可能基于一个名为“LoveSQL”的登录或查询界面。题目年份是2019年,这意味着它可能不会涉及太新的数据库特性或防护手段,但作为经典题型,其考察的注入原理和手工绕过技巧是永恒的。
从网络热词中频繁出现的“sql注入万能密码”、“mariadb”等词汇,我们可以合理推测靶场环境。很多CTF平台和线下赛题喜欢使用MariaDB或MySQL作为后端数据库,因为其语法普及,且默认配置下可能存在的安全特性(如sql_mode)为注入提供了更多可能性。题目编号为“1”,通常意味着这是一个系列题的开端,难度不会设置得过于变态,旨在考察基础的联合查询注入流程。
我们的核心目标是什么?CTF中的SQL注入,最终目的几乎都是获取一个被称为“flag”的特定字符串。这个flag可能藏在数据库的某个表、某个字段里。因此,解题思路通常遵循一个标准流程:判断注入点 -> 判断数据库类型和结构 -> 获取数据库名 -> 获取表名 -> 获取字段名 -> 获取数据(即flag)。整个过程,我们都需要与网页进行交互,通过精心构造的输入,让数据库执行我们期望的SQL语句,并将结果回显在页面上。
2. 手工注入探测与信息收集
面对一个疑似存在SQL注入的Web界面(比如一个登录框、搜索框或传参点),我们不能一上来就“狂轰滥炸”。有策略、有步骤的探测是成功的关键。
2.1 初步试探与注入类型判断
首先,我们需要找到注入点。假设题目是一个登录页面,有用户名(username)和密码(password)两个输入框。后端SQL语句可能类似:SELECT * FROM users WHERE username='$username' AND password='$password'
我们的输入会被代入到$username和$password的位置。经典的试探方法是使用单引号‘来尝试闭合原有的SQL字符串,观察页面反应。
单引号测试:在用户名框输入一个单引号
‘,然后随意输入密码(如123)。点击登录。- 如果页面返回了数据库错误信息(如“You have an error in your SQL syntax...”),这强烈暗示存在SQL注入漏洞,并且错误信息可能暴露数据库类型(如MySQL/MariaDB)。
- 如果页面只是提示“登录失败”或“用户名不存在”,这并不代表没有注入,可能只是被应用层优雅地处理了。我们需要进一步测试。
逻辑测试:这是判断注入是否存在以及类型的核心。
- 永真条件测试:尝试输入
admin‘ or ‘1’=‘1。代入到SQL语句中,会变成:SELECT * FROM users WHERE username='admin' or '1'='1' AND password='$password'由于‘1’=‘1‘永远为真,只要username=’admin‘这部分能匹配上,或者整个OR条件成立,就可能绕过密码验证。如果使用‘ or 1=1 --(注意最后的空格和注释符--),效果更彻底,它会注释掉后面的AND password部分。 - 永假条件测试:输入
admin‘ and 1=2 --。如果页面正常返回“用户不存在”或登录失败,而永真条件却能返回不同结果(比如跳转到其他页面),这基本确认存在基于字符型的SQL注入,并且我们可以控制查询逻辑。
- 永真条件测试:尝试输入
注意:注释符在不同数据库中不同。MySQL/MariaDB中,
--(后面有个空格)、#、/*...*/都是有效的。在URL或表单中,#有时会被当作锚点,所以--+(用加号代表空格)或--%20(URL编码的空格)是更稳妥的选择。
2.2 确定字段数与回显位
确认存在注入后,下一步是使用ORDER BY和UNION SELECT来确定当前查询语句选取的字段数量以及哪些字段的内容会被回显在页面上。这是后续获取数据的基础。
ORDER BY猜解字段数:ORDER BY子句用于根据指定列排序。如果ORDER BY 5表示根据第5列排序,如果查询结果只有4列,数据库就会报错。我们可以利用这个特性来猜解。 在注入点输入:‘ order by 1 --,页面正常。 然后尝试:‘ order by 2 --,‘ order by 3 --,‘ order by 4 --... 直到某个数字N使得页面报错或显示异常,那么字段数就是 N-1。 假设order by 4正常,order by 5报错,那么字段数就是4。UNION SELECT联合查询与确定回显位: 得知字段数(假设为4)后,我们构造联合查询。UNION操作符用于合并两个SELECT语句的结果集,前提是列数必须相同。 输入:‘ union select 1,2,3,4 --这个语句会尝试将我们自定义的(1,2,3,4)结果集与原有查询结果集合并。如果页面某处原本显示数据的地方,变成了数字1,2,3,4中的某一个或几个,那就说明该位置是“回显位”。这些数字可以被替换为我们想查询的数据库信息。 例如,如果页面某处显示了2和3,那么第2和第3个字段就是回显位。我们后续的查询,比如database(),version(),就需要放在这两个位置上,像这样:‘ union select 1, database(), version(), 4 --。
3. 数据库结构探秘与信息提取
确定了回显位,我们就拿到了从数据库读取信息的“通道”。接下来就是标准的“拖库”流程:先看当前数据库,再看有哪些表,然后看目标表有哪些列,最后把数据读出来。
3.1 获取基础信息与数据库名
利用回显位,我们可以一次性获取多条有用信息:‘ union select 1, database(), version(), user() --
database(): 返回当前数据库名称。这是我们的首要目标。version(): 返回数据库版本信息。可以确认是MySQL还是MariaDB,以及具体版本,对于后续利用某些特性(如报错注入函数)有帮助。user(): 返回当前数据库用户。了解权限,有时高权限用户(如root)能访问更多系统表。
假设返回结果中,database()显示为geek。那么我们的目标数据库就是geek。
3.2 枚举数据库中的表名
在MySQL和MariaDB中,数据库的元数据(如表名、列名信息)存储在名为information_schema的默认数据库中。其中,TABLES表存储了所有表的信息。 关键字段是:
TABLE_SCHEMA: 数据库名TABLE_NAME: 表名
我们要查询geek数据库下的所有表。因为回显位可能有限(比如只有2个),而一个数据库里可能有几十张表,我们需要用group_concat()函数将所有表名合并成一个字符串返回。‘ union select 1, group_concat(table_name), 3, 4 from information_schema.tables where table_schema=‘geek’ --
这条语句的意思是:从information_schema.tables中,选取table_schema为geek的所有记录的table_name字段,并用group_concat()合并成一个字符串,在回显位2显示出来。 返回结果可能像这样:geekuser, l0ve1ysq1, news, admin...等等。我们需要从中寻找可能存储flag的表。表名有时会给出提示,比如l0ve1ysq1(love sql的变体)就非常可疑,flag,secret,users也值得关注。
3.3 枚举目标表的字段名
假设我们怀疑l0ve1ysq1这个表。下一步是查看它有哪些列(字段)。这需要查询information_schema.COLUMNS表。 关键字段:
TABLE_SCHEMA: 数据库名TABLE_NAME: 表名COLUMN_NAME: 字段名
输入:‘ union select 1, group_concat(column_name), 3, 4 from information_schema.columns where table_schema=‘geek’ and table_name=‘l0ve1ysq1’ --
返回结果可能像:id, username, password。如果看到像flag,value,secret这样的字段名,那很可能就是目标。
3.4 最终数据提取与Flag获取
知道了表名和字段名,最后一步就是直接查询数据。如果字段是id, username, password,我们可以直接查询:‘ union select 1, username, password, 4 from l0ve1ysq1 --
如果字段太多,或者我们只想看某个字段,可以指定:‘ union select 1, 2, 3, 4 from l0ve1ysq1 --(先看结构) 或者更直接地,如果怀疑flag在password字段里:‘ union select 1, group_concat(password), 3, 4 from l0ve1ysq1 --
通常,flag会以特定格式出现,比如flag{xxxx-xxxx-xxxx},CTF{...}等。在返回的结果中仔细查找这样的字符串,那就是本题的答案。
4. 实战技巧、常见问题与深度防御思考
手工注入的过程看似线性,但实战中会遇到各种“小惊喜”。下面分享一些我踩过坑后总结的技巧和常见问题的排查思路。
4.1 绕过过滤与编码技巧
- 大小写绕过:如果代码简单过滤了
SELECT、UNION等关键词,可以尝试SeLeCt、UnIoN。MySQL默认是不区分关键字大小写的。 - 双写绕过:如果过滤方式是删除关键词,比如将
select替换为空,可以尝试selselectect,删除中间的select后,剩下的字符正好拼成select。 - 注释符内联:
/*!SELECT*/这种格式在MySQL中是一种内联注释,其中的代码会被执行。可以用来包裹关键词,如/*!UNION*/ /*!SELECT*/。 - 十六进制编码:将字符串转换成十六进制。例如,
‘geek‘的十六进制是0x6765656b。在注入时,可以用0x6765656b来代替‘geek‘,有时可以绕过对单引号的过滤。‘ union select 1,2,3 from test where name=0x6765656b --。 - URL编码:在GET请求传参时,特殊字符需要URL编码。空格是
%20,单引号是%27,井号#是%23。--+中的+在URL中代表空格。
4.2 无回显注入(盲注)的初步感知
这道“LoveSQL 1”很可能是有回显的联合查询注入,属于最简单的一类。但你要知道,很多题目会关闭错误显示,并且UNION查询的结果不会输出到页面(即页面无论对错,看起来都一样)。这就是盲注。 盲注主要分两类:
- 布尔盲注:页面会根据注入的SQL语句执行结果的真假,返回不同的内容(比如“存在”或“不存在”)。我们通过
and length(database())=4、and substr(database(),1,1)=‘g‘这样的语句,像猜密码一样一位一位地判断。 - 时间盲注:页面无论真假都返回相同内容。我们通过
and sleep(5)这样的语句,如果页面响应延迟了5秒,就说明条件为真。通过判断延迟,来逐位获取信息。 虽然本题可能用不到,但这是SQL注入知识体系的重要部分。
4.3 常见错误与排查清单
UNION语句前后列数不一致:这是最常见的错误。务必先用ORDER BY精确确定字段数。- 数据类型不匹配:如果原查询的某个字段是字符串类型,而你在
UNION的对应位置放了个数字1,可能引发错误。稳妥起见,可以用null或者‘a‘来占位。 - 单引号被转义或过滤:如果输入的单引号被前置了反斜杠
\‘,那么它会被转义,失去闭合作用。可以尝试宽字节注入(如果数据库使用GBK等编码),或者寻找不需要单引号的注入点(如数字型注入)。 - 关键词被过滤:尝试上述的绕过技巧。或者使用
<>代替!=,like代替=等。 information_schema被禁用:在极少数高版本MySQL或特定配置下,可能无法访问information_schema。这时需要了解MySQL的替代方案,如通过sys.schema_table_statistics等视图,或者利用innodb引擎的特性进行盲猜,但这属于高阶技巧。
4.4 从攻击者视角到防御者视角
作为学习者,我们通过CTF题目理解攻击原理。但更重要的是,要建立起防御的意识。针对这类联合查询注入,防御措施包括:
- 最小权限原则:数据库连接账户不应拥有
FILE、PROCESS等高权限,且只授予其访问必要数据库和表的权限。 - 预编译语句(Prepared Statements):这是最有效、最根本的防御手段。使用参数化查询,将用户输入的数据始终视为“数据”,而非“代码”,从原理上杜绝了SQL注入的可能。几乎所有现代编程语言和框架都支持。
- 输入验证与过滤:虽然不能作为主要防御手段,但可以作为一种辅助。对输入进行严格的类型检查(比如ID必须是数字)、长度限制、使用安全的允许字符白名单。
- 禁用详细错误信息:避免将数据库的原始错误信息直接展示给用户,防止泄露数据库结构。
- 使用Web应用防火墙(WAF):可以拦截常见的注入攻击特征,但存在被绕过的风险,不能依赖。
手工完成一道SQL注入题,就像是完成了一次对目标数据库的“外科手术式”探查。从试探、判断、确定路径,到一步步深入,最终获取目标数据,整个过程要求你对SQL语法、数据库结构有清晰的认识,并且要足够细心和耐心。这道“LoveSQL 1”作为入门题,完美地串联了这些基础步骤。当你熟练掌握这套流程后,面对更复杂的盲注、二次注入、堆叠注入等变形题目时,你便有了坚实的分析基础和解题框架。记住,工具(如sqlmap)可以提升效率,但手工理解每一步的原理,才是你安全能力成长的基石。在真实环境中,复杂的过滤和防护往往需要你灵活运用这些基础知识进行手工测试和绕过,这是自动化工具无法完全替代的。