很多Web 安全漏洞的产生原因都绕不开两条:
1.违背了“数据与代码分离“原则。它有两个条件:一是用户能够控制数据的输入;二是代码拼凑了用户输入的数据,把数据当作代码执行了。
2.违背了权限管理的黄金法则:最小权限原则。
一、有关html/css, js, php, cgi 的一些认识
selector 常见的就 type, class(.), id(#),注意的是 id 只能一个页面一个。
e.g
在html 中引入css 一般有以下3种方式:
---行内式
---链入外部样式表文件 (Linking to a Style Sheet)
Remember, HTML will define the content and structure of our web pages, while CSS will define the visual style and appearance of our web pages.
还有一些其他类似的文件类型,如 shtml, phtml, jhtml,这类可以算是动态网页,比如 shtml (ssi)可以 引入一个html,服务器会将其解析并填充在返回的页面中;phtml 即源码包含 语句;jhtml 源码包含 jsp <% %> 语句。
当我们浏览器访问一个站点的静态文件,会把文件内容都下载下来(一般压缩),当然如果遇到外联的css/js,会再发起请求得
到。如果我们右键查看网页源代码,一片混乱没法看,可以使用firefox + firebug,可以清晰看到html dom tree,右键inspect
element 可以很快定位到tree node,由于是下载到本地,所以可以自己尝试修改element 查看效果,这并不影响服务器上的原始
文件。最后浏览器会开始渲染,包括执行js比如document.write() 之类,就呈现出现在我们所看到的网页模样,可以使用firefox F12 断点调试js。
所谓的 dom 树操作就是一系列类似 getElementById 之类的函数。注意 js 是在客户端执行的,可以动态地改变 dom 树,通俗地说就是可以改变页面
的html,人们从浏览器看见的页面也就变化了。
html 的解析顺序:html parser --> css parser -->javascript parser
CGI 的意思是啥?不是一种语言,也不是一种技术,而是一种模式。搜索一下CGI的定义Common Gateway Interface,简称CGI。在物理上是一段程序,存放在服务器上。只要是提供数据输出的服务器端程序都可以叫CGI,ASP/PHP/JSP这些都可以认为是,你用C/C++写一个可以提供数据输出的服务器端bin文件,也叫CGI,至于python/perl/shell 等脚本当然也能写cgi。
对一个 CGI 程序,做的工作其实只有:从环境变量(environment variables)和标准输入(standard input)中读取数据、处理数据、
向标准输出(standard output)输出数据。环境变量中存储的叫 Request Meta-Variables,也就是诸如 QUERY_STRING、
PATH_INFO 之类的东西,这些是由 Web Server 通过环境变量传递给 CGI 程序的,CGI 程序也是从环境变量中读取的。
标准输入中存放的往往是用户通过GET 或者 POST 提交的数据,这些数据也是由 Web Server 传过来的(客户端提交)。传统的
get 即是以 url?key1=value1&key2=value2的 形式传输过去。而post 形式(http请求包体)就比较多了,可以是传统的
key=value,也可以是json/xml 等形式,只是这些从标准输入得到后还需要经过一个解析的过程才能得到想要的key=value 形式的呈现。
注意标准输入的概念,如果在本地执行 php xx.php args 那些 xx.php 的标准输入就是控制命令窗口,获取输入需要通过 $argv;如果是通过 uri 路径访问 xx.php 如 http://localhost/xx.php,那么 xx.php 的标准输入来自 webserver 给的数据,可以通过 php://input 获取。
当然cgi 的body输出也是多种形式了,可以是简单的application/json、text/xml 形式,也可以是php echo 出一个text/plain or text/html,但要明确的是php 等脚本是在服务器端执行的,也就是说当客户端访问test.php 时,server 先执行php脚本(php 会 读取标准输入,处理过程,向标准输出输出数据),形象地来说,就是“戳一次就动一次”,根据用户输入的不同而产生不同的输出结果,即动态网页的概念。注意,php, js css, 都可以和html 标签写在同个文件中。
有时候访问出现403 forbidden ,有种原因是 apache 设置的user,即运行httpd的user 是nobody(假设),对你想要访问的目
录/文件 没有读或者执行的权限,所以server 没办法读取执行文件,故 禁止访问。还有种情况是配置文件写明 deny xxx,禁止某些来源ip 访问,或者
禁止访问某些目录、某种后缀的文件。
二. 各种编码、转义相关
从浏览器 url发出的请求,如果进行了 urlencode(比如chrome一般会编码 "<>,firefox 一般会编码 ' " `<>, 而低版本 ie 不会编码任何字符),比
如将 " 转成%22 发出去,在服务器端的php 接收到的是原始的" 还是编码后的%22 得看用$_GET["key"] 还是$_SERVER['QUERY_STRING'],还要看
在php 脚本内有没有做addslashes 或者 htmlspecialchars 等函数调用,这样就能判断解析脚本 echo/print 出来的html 是怎样的组织形式,当然客
户端请求得到的html 也就是这样的形式了。那为什么在chrome中对于< 等没有alert 弹窗呢,只是因为某些浏览器有anti_xss 模块或者filter,在浏览
器解析 html 的时候 过滤掉这些危险的script 而没有执行,比如 ie 可以关闭掉 xss 筛选器让其弹框。
对于低版本 ie 而言,如果页面 js 取location.href or #锚参数 or &get参数 的值,则保持 地址栏原有模样。而高版本 ie 或者 其他浏览器 取到的都是
编码后的样子(取决于浏览器本身会编码哪些字符发起请求)。这对于 domxss 来说是一个比较重要的区分点。需要注意的是 chrome:
http://www.foo.com/dom/loc.html? -- 编码
http://www.foo.com/dom/loc.html# -- 不编码
为了看参数是否Urlencode对返回结果是否有影响,可以用一些工具比如 fiddle 发出编码和不编码时的请求,对比观察。
这种不编码访问才能触发的xss 漏洞,最简单的利用方式是写一个html,里面用 iframe src 引入完整不编码 payload 链接,用 ie 访问此 html。
注意如果此时弹 cookie 的话弹出的是 iframe 内 domain 域的 cookie,因为浏览器在请求第三方站点时也会把相关cookie发送出去(没有P3P 属性
的 persistent cookie 有例外),如下:
注意:由于同源策略的存在,本地html 是读取不到第三方站点 cookie的,但这里演示的是第三方站点自己存在漏洞,自己执行 js 弹cookie。
现在常见的参数格式除了最原始的 /path/aa.html?a=1&b=2;还有 restful 的 /page/1/id/2011 即 传入的参数是 page=1&id=2011;
此外还有 rewrite,比如 /path/2015/a(a : 1~100)/b(b: 10~20) 对应后端的cgi 可能是 /path/c.cgi?p=a&q=b;一些 MVC 框架的 cgi 可能需要根
据某个参数的值选择不同的逻辑分支,比如 a.cgi?_path=/gdata/6/data&id=5,映射到一个类;还有一种是参数直接跟在uri后面的,
如http://aa.qq.com/m/index/nav/type=bus&cond=3,可以理解为 /m/{module}/{action}/{query_list},
module 和 action 可以不断对请求进行路由,逐级分发到不同的模块,最终query_list是使用常规的webserver解析方式。
htmlspecialchars 会把 < 编码成 < 还有 >,",' ,& 等
addslashes() 函数在指定的预定义字符前添加反斜杠。这些预定义字符是:
单引号 (')
双引号 (")
反斜杠 (\)
NULL
这样就无法从url 传递带引号的参数来闭合引号来达到xss的目的,但是在charset=gbk 的情况下,如果参数含大于127的值如%ae,后面再跟引号,
虽然引号变成\', 因为 %5c 在 gbk 低字节范围内,%ae\ 在gbk 看来也许是一个字符,当然我们看起来好像是一个乱码,这样也会造成引号可以闭
合,sql 注入也存在这样的字符集解析问题。设置了某种字符集,浏览器会按这种编码去解析html 来展示给用户。(memchr)
比如>
虽然大部分浏览器不会解析 Content-type: application/plain 响应头下的 body,但如 safari 以及某些版本的ie 都可能不按套路行事。在一些不需要html 解释的页面,可以通过设置Content-type:application/json来避免xss。还有些站点直接返回 如 baiduApp/json,浏览器识别不了就会直接下载成文件到本地。ie 浏览器在确定文件类型时不完全依赖Content-type,比如 foo.cgi?id=123&a.html,ie 可能会认为这是个html 文件或忽略 Content-type。
反斜杠 \ 在script域内会起转义作用,而在html 标签内就是表示的字符含义,从下面alert()出来的字符可以得知。
三. 登录跳转、登录验证
一般网站登录前的验证可能是这样实现的:
在用户填完信息后会先调用validate() 函数进行验证,如果返回true 才会真正提交表单。
在validate() 里类似 if(document.forms.login.agreement.value != checked) { return false;}
// document.getElementById("login").agreement.value id 必须全局唯一
在不想重载页面,也就是不提交,可以 οnsubmit="quote(); return false;" 在quote()里面可以 xhr= new XMLHttpRequest();
即ajax的方式来做一些操作。
现在很多提交的实现不再使用 form 表单,比如只要监听某 button 标签事件,点击触发时执行事件,里面用 ajax 方式提交请求。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
$(
".submit").on(
'click',
function ()
{ var msg = ""; if ($( ".user_name").val().trim() && $( ".mail").val().trim() && $( ".phone").val().trim()) { submitUserInfo(); } else {
}
}); |
一般的网站登录跳转实现方式之一是:在login.php 对表单post 过来的user&pwd&email 验证(见下面),如果对则设置一个键值如 $_SESSION["auth"]=true,设置response 的Location Head : home.php,本程序exit。浏览器接收到rsp,看到Location 头部,于是跳转请求至home.php。home.php 可以对$_SESSION["auth"] 继续判断一次,若true 则显示登录后的页面。当然这一切的前提是login.php开启了session_start(),这样第二次访问home.php 也会带上Cookie:PHPSESSID=xxx ,这样server 通过 $_COOKIE 获取sessionId就知道是同个用户的请求,通过sessionId 就可以知道 $_SESSION 结构体中原本存放的数据,比如auth=True 之类。
superglobals : $_COOKIE $_ENV $_FILES $_GET $_POST $_REQUEST $_SERVER $_SESSION
如果可以通过get 参数控制 Location 地址跳转,需要做好验证,不然可能让坏人利用造成钓鱼。
还有如 dom 跳转,即 浏览器在解析 js 时进行跳转。
实际上带登录态的漏洞扫描也是带上cookie 实现的,需要注意cookie失效的问题。一般server对每个网页请求会做登录验证,常用两种方式:
1). 从cookie头中获取sessionId,进而从server 端存储的Session信息中获取相关验证信息,如user&pwd&email之类,与post过来的信息进行比对(可能需要根据post数据字段查数据库),验证通过则显示请求的网页,否则跳转到登录页面。
Session 的优点是简单易用,可以直接从Session中取出用户登录信息。Session 的缺点是服务器需要在内存中维护一个映射表来存储用户登录信息,如果有两台以上服务器就需要对Session 做集群,因此使用Session的Web App 很难扩展。
比如 web.py 框架中 session.py 有:
'cookie_name': 'webpy_session_id' // 定义
self.session_id = web.cookies().get(cookie_name) // server 获取sessionId
self._setcookie(self.session_id) // server 设置Client 的cookie,Set-Cookie 头,即调用下面的函数
web.setcookie(cookie_name, session_id, expires=expires, domain=cookie_domain, httponly=httponly, secure=secure, path=cookie_path)
self.store[self.session_id] = dict(self._data) // server 端存储session_id 相关的用户数据
2). 先直接根据post 过来的数据字段查数据库进行比对,但此时还需要验证cookie 不是伪造的,实现防伪造cookie的关键是通过一个单向算法
(例如MD5),举例如下:
当用户输入了正确的口令登录成功后,服务器可以根据post数据从数据库取到用户的id,并按照如下方式计算出一个字符串:
"用户id" - "过期时间" - MD5("用户id" + "用户口令" + "过期时间" + "SecretKey")
并通过set-cookie 设置浏览器的cookie。
当浏览器发送cookie 到服务器端后,服务器可以拿到的信息包括:用户id、过期时间、MD5值
如果未到过期时间,服务器就根据用户id 查找用户口令,并计算:
MD5("用户id" + "用户口令" + "过期时间" + "SecretKey")
并与浏览器cookie中的MD5 进行比较,如果相等则说明用户已登录,否则cookie就是伪造的。
这个算法的关键在于MD5是一种单向算法,即可以通过原始字符串计算出MD5,但无法通过MD5反推出原始字符串。
但数据量巨大的彩虹表维护的 字符串与 md5的映射也许可以反推出原始字符串,对此可以在 md5 前再加 salt 一下。
四、session, token, cookie的区别与联系
1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识sessionId。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。即使浏览器的 session cookie 在其关闭时被清除,但此时服务器却是不知道的,故服务器可能会设置一个过期时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把这个session删除以节省存储空间。
2. Session cookie
A session cookie, also known as an in-memory cookie or transient cookie, exists only in temporary memory while the user navigates the website.When an expiry date or validity interval is not set at cookie creation time, a session cookie is created. Web browsers normally delete session cookies when the user closes the browser.
思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息(如 Cookie: PHPSESSID=xxxxx;)到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID(Examples of the names that some programming languages use when naming their cookie include JSESSIONID (JEE), PHPSESSID (PHP), and ASPSESSIONID (Microsoft ASP).),以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。sid 出现在 url 中的情形有个安全隐患是,假设一个站点被引入了一个外部图片,打开这个站点会发起图片的get 请求,而 referer 就是受害站点的 url,由此泄露了 sid。
会话cookie: 是一种临时的cookie,它记录了用户访问站点时的设置和偏好,关闭浏览器,会话cookie就被删除了。
3.Persistent cookie
A persistent cookie outlasts user sessions. If a persistent cookie has its Max-Age set to one year (for example), then, during that year, the initial value set in that cookie would be sent back to the server every time the user visited the server. This could be used to record a vital piece of information such as how the user initially came to this website. For this reason, persistent cookies are also called tracking cookies.
持久型cookie 一般用来保存一些少量信息,如当初用户是从哪个url 跳转来的。
持久cookie: 存储在硬盘上,(不管浏览器退出,或者电脑重启,持久cookie都存在), 持久cookie有过期时间。
所以,总结一下:
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
client :
Cookie: name=value; name2=value2
server:
Set-Cookie: LSID=DQAAAK…Eaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
Set-Cookie: HSID=AYQEVn….DKrdst; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly
Set-Cookie: SSID=Ap4P….GTEq; Domain=foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
cookie 无法跨浏览器存在。
Domain 和 Path 决定浏览器在访问此站点某目录下的网页时cookie 才会被发送出去(domain 可以设置为父域,但不可设置为子域和外域)。
Expires 确定cookie的过期时间,没有过期时间则为session cookie,有则是persistent cookie,过期时间是过去时间点则表示删除cookie。Secure 表
示只有通过https 连接才会发送cookie。HttpOnly 表示只有通过http 访问才会发送cookie,比如在客户端执行js: document.cookie 是获取不到cookie
的,如果只设置了 Secure 而未设置 httponly,那么还是可以通过 客户端 js 获取到 cookie。
需要注意的是设置 path 不能防止重要的cookie 被盗取,假设在同域下的a路径存在xss漏洞可以执行js,想盗取b 路径的cookie,只需在 b 路径用
iframe 方式加载 a 路径,获取 a 路径的cookie(iframe 加载的是同域页面,故 b路径的js 可以访问 iframe document 的属性),如下所示:
桌面应用程序也通过HTTP协议跟Web服务器交互, 桌面应用程序一般不会使用cookie, 而是把 "用户名+冒号+密码"用BASE64 编码的字符串放在http request 中的header Authorization 中发送给服务端, 这种方式叫HTTP基本认证(Basic Authentication)
在chrome浏览器里可以用 http://username:password@url这种方式直接略过这个基本认证
这里贴一下 401 与 403 的不同之处:
A clear explanation from Daniel Irvine:
401 Unauthorized, the HTTP status code for authentication errors. And that’s just it: it’s for authentication, not authorization. Receiving a 401 response is the server telling you, “you aren’t authenticated–either not authenticated at all or authenticated incorrectly–but please reauthenticate and try again.” To help you out, it will always include a WWW-Authenticate header that describes how to authenticate.
This is a response generally returned by your web server, not your web application.
It’s also something very temporary; the server is asking you to try again.
So, for authorization I use the 403 Forbidden response. It’s permanent, it’s tied to my application logic, and it’s a more concrete response than a 401.
Receiving a 403 response is the server telling you, “I’m sorry. I know who you are–I believe who you say you are–but you just don’t have permission to access this resource. Maybe if you ask the system administrator nicely, you’ll get permission. But please don’t bother me again until your predicament changes.”
In summary, a 401 Unauthorized response should be used for missing or bad authentication, and a 403 Forbidden response should be used afterwards, when the user is authenticated but isn’t authorized to perform the requested operation on the given resource.
Authentication是认证,Authorization 是授权。认证的目的是为了认出用户是谁,而授权的目的是为了决定用户能够做什么。