0x00
首先简单介绍一下DVWA。
Damn Vulnerable Web App (DVWA) is a PHP/MySQL web application that is damn vulnerable.
简单来说就是一个web漏洞实验环境,可以用来练习web向各种漏洞的利用。
第一次写专栏,这一篇试试水,知乎上大牛很多,如果有写的不对的地方,还请各位大牛指导!
0x01
环境怎么安装就不啰嗦了,可以去官网或者github上查找教程。
Sqli的实验有三种难度,low,medium和high。
实验的目的是要我们通过sqli漏洞,从数据库中拿到各个账号的密码。
0x02
从low的看起。
一般我们测试sql注入的注入点,通常是在参数值后面加一个单引号,如果这时候数据库报错,就可以确认此处有注入。
正常情况下,参数为id,然后后台将id的值放入sql语句中查询,然后返回查询的结果
我们在id字段的值后面添加一个单引号测试,发现sql语法错误,于是我们可以确定,此处存在注入点。
于是开始琢磨怎么拿数据,方法有很多种,常见的有利用union select,报错注入,时间盲注,布尔盲注,思路和步骤上大体差不多,只是具体的payload和适用环境有所区别。
以下以用union select为例,简单说明一下注入思路。确定查询字段数。
确定数据库名。
确定表名。
确定列名。
导出数据。
因为此处使用union select来注数据,所以需要确定字段的个数,因为union select需要跟原本的sql语句保持相同的字段数目和字段类型。此处既可以用ordey by从句,也可以直接使用union select 尝试。
在提交payload的时候,我们需要用注释符注释掉源代码中payload之后的sql语句,从而避免语法错误。mysql的注释符有#和--,用#注释的时候需要用url编码,用--的时候可以不编码,但是要在之后加一个空格,或者加号(本质上都是空格)。
order by 3时报错,order by 2时正确回显。可以得出应该是两列。
确立字段数之后我们需要确立回显点。用union select 1,2 来确定查询的数据会回显在页面的什么位置上。
看下图,第二条查询记录中first name和last name之后的1,2就代表回显位置,这里的1,2的意思是select的第一列和第二列。并且这个环境这里是简单的模式,通常遇到的情况是每次回显只能回显一条select的结果,那这种情况下,就要使原本的查询无结果,比如此处另id=0,从而使union select的查询结果存放在第一位显示。
接下来都是固定格式的payload,注数据库名。如果环境是每次只能显示一条查询结果的话,通过后面添加limit子句逐条将名称爆出。
表名同理。
字段名。
然后就可以注数据了,利用payload:union select 字段名 1,字段名2 from 数据库名.表名 limit ...的格式逐个注出用户名,密码。
这里发现密码都是MD5存储的,感兴趣的可以爆一爆.
比如admin的MD5值是5f4dcc3b5aa765d61d8327deb882cf99
解密一下,结果是password
在存在回显的情况下,还有一种常见的注入方式是报错注入(double query)。利用sql错误发生时的回显将数据注出来。剩下的两种盲注我们暂时不说。DVWA中有另外一个单独的sql盲注专题。盲注通常使用在不管sql语句对错与否都不发生回显的状况下。
这里报错信息会回显,所以我们讲一下报错注入,这种注入的payload格式也比较固定。payload如下:
and(select 1 from(select count(*),concat((select (select (语句)) from information_schema.tables limit 0,1),0x7e,floor(rand(0)*2))x from information_schema.tables group by x)a) and 1=1
我们在本实验环境下用这种方法来试一试结果。
以注数据库名为例。
表名字段名及数据后不赘述,大家感兴趣的可以自己实验,payload结合上面这条和之前用union注入所用的格式即可。
当存在注入点的时候,我们还可以通过一些sql自带的函数干一些其他羞羞的事情。
user() 获取数据库用户名
database() 获取当前数据库名
version() 获取MYSQL数据库版本
load_file() MYSQL读取本地文件的函数
@@datadir 读取数据库路径
@@basedir MYSQL 安装路径
@@version_compile_os 操作系统
into_outfile() 写一句话木马
以读取本地文件及数据库为例。
sqli最基础的思路和步骤就介绍完了。以上是在应用上未做任何防御的前提下实施的注入攻击。当应用侧做了防御之后,我们需要对这些基础的payload作出一些微小的调整,来获取数据。
sqli的本质,是数据和代码的混淆不清。传入的数据,构成了代码的一部分,从而达到攻击者的目的。所有有输入的程序,都可能发生这种原理的漏洞。
0x03
然后我们来看medium难度下的实验环境。我们还是试探性用单引号测试。结果报错了。报错信息中我们发现单引号前面存在\,于是猜测单引号被转义了,主机侧代码可能做了addslashes().
为了更好的解释问题,此处我们直接查看源代码。
$id = $_GET['id'];
$id = mysql_real_escape_string($id);
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id";
这里对传入的参数进行了叫做mysql_real_escape_string()的预处理操作,这个函数可以转义 SQL 语句中使用的字符串中的特殊字符。所以我们看到,单引号被转义了,没有办法构成代码。
那么怎么办呢?
我们仔细看发现,这里的id参数是直接引用的,并没有加任何的引号,所以get请求中id后面的所有内容,可以直接参与sql语句的构建。比如如果id的内容是id = 0 union select database(),user().那拼接到php代码中就是
select first_name,last_name from users where user_id=0 union select database(),user().
这完全是一条合法的sql语句,它甚至都不用在payload的结尾加上注释符。所以实质上这里的mysql_real_escape_string()并没有构成任何的防御作用,这个函数只有当参数被引号包围起来的时候,才起作用(当然也能突破,这个我们后面再说)。
所以我们还是顺着上一章提到的思路,确定字段数,确定回显位置,爆数据库名,爆表名,爆字段名,爆数据内容。
在这些步骤中,可能会遇到一丢丢小的麻烦,那就是当我们爆表名的时候,payload中where子句需要明确表所在的数据库的名字,这个参数是用单引号引起来的,那么引号既然被转义了,我们怎么才能把这个参数弄进去呢?
这里我们可以将数据库的名称用十六进制表示,来绕过引号,数据库同样是可以解析的。
成功绕过,后面列名同理。用引号引起来的数据都可以用16进制表示来绕过这个限制。
在平常的学习实验还有ctf中,大多注入题都会存在一些防御手段,基本都是针对payload的关键字进行过滤。过滤select等关键字
绕过方法:大小写混合,用selselectect等方式
过滤空格
绕过方法:注释符/**/,利用(),%0a,%0b,%0c,%0d,%09,%a0等等
转义单引号
绕过方法:当应用的字符集为GBK时,可以采用宽字节方法绕过,%BF‘
以下是一些绕waf的思路和总结:
0x04
终于到了搞基的部分,啊不,是高级的部分。
直接看代码。这段代码我看了很久很久很久,都没想出来怎么破,实在没有办法谷歌了一下。
然后查到的结果说,这段代码就是没有办法注入的,它代表着平台作者所认为的sql注入的正确防御姿势。
if (isset($_GET['Submit'])) {
// Retrieve data
$id = $_GET['id'];
$id = stripslashes($id);
$id = mysql_real_escape_string($id);
if (is_numeric($id)){
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";
$result = mysql_query($getid) or die('
' . mysql_error() . '' );
$num = mysql_numrows($result);
$i=0;
while ($i < $num) {
$first = mysql_result($result,$i,"first_name");
$last = mysql_result($result,$i,"last_name");
echo '
';
echo 'ID: ' . $id . '
First name: ' . $first . '
Surname: ' . $last;
echo '
';$i++;
}
}
}
?>
我们来简单分析一下,防御步骤一共有三步。用stripslashes()去掉参数中的斜杠
用mysql_real_escape_string()将参数中的特殊字符转义
用is_numeric()判断参数是否为数字型,是则进行查询,否则drop.
并且id在sql语句中还处在单引号中。在这种情况下,即使存在GBK字符集条件下的宽字节绕过mysql_real_escape_string()函数,下面的is_numeric()函数,也会直接将恶意的payload丢弃。所以该处无法注入。
谷歌下有人提到sqlmap在此处可以注出数据,但是sqlmap是有缓存的,当url在它的记录中,它会直接返回缓存中的结果,也就是low,medium难度下得到的结果,并不是在high level下获取的数据。
0x05
DVWA上关于sql注入的还有一个盲注的篇章,下次有机会再分享。
在实际中,sql还有很多其他的形式insert,update,delete语句的注入
存储型的二次注入
stacked注入
另外一个专门的sqli练习平台上有很好的归类,大家有兴趣的可以自己安装来练习。
第一次做这样的分享,肯定有很多写的不好的地方,还请各位大神多多指正。这两个虚拟机,有感兴趣的朋友愿意一起玩儿的,欢迎交流。