[聚合文章] 『安全开发教程』年轻人的第一款弱口令扫描器(x-crack)

MySQL 2017-12-30 25 阅读
『安全开发教程』年轻人的第一款弱口令扫描器(x-crack)

『安全开发教程』年轻人的第一款弱口令扫描器(x-crack)

netxflynetxfly

『安全开发教程』年轻人的第一款弱口令扫描器(x-crack)

概述

我们在做企业安全时,弱口令检测是系统/网络安全的最基础的部分之一,根据经验,经常会出现弱口令的服务如下:

  • FTP
  • SSH
  • SMB
  • MYSQL
  • MSSQL
  • POSTGRESQL
  • REDIS
  • MONGODB
  • ELASTICSEARCH

那咱们就一起用GO来写一款常见服务的弱口令扫描器,且支持以插件的形式增加新的服务扫描模块。我们的教程暂定为只扫以上服务。

给扫描器启一个屌炸天的名字x-crack,在$GOPATH/src/中建立一个x-crack项目后开始撸码,不要给我说什么底层原理、框架内核,老夫敲代码就是一把梭。

开发完毕的项目地址为:github.com/netxfly/x-cr

开工

数据结构定义

  • 扫描模块的输入内容为为IP、端口及协议的列表,我们需要定义一个IpAddr的数据结构;
  • 每个服务的每次扫描需要传入的参数为IP、端口、协议、用户名和密码,需要定义一个Service结构来包括这些内容;
  • 每条Service的记录在扫描模块进行尝试后,会得出扫描结果成功与否,我们再定义一个ScanResult数据结构。

按照开发规范,数据结构的定义统一放到models目录中,全部的数据结构定义如下:

package modelstype Service struct {	Ip       string	Port     int	Protocol string	Username string	Password string}type ScanResult struct {	Service Service	Result  bool}type IpAddr struct {	Ip       string	Port     int	Protocol string}

FTP扫描模块

go语言有现成的FTP模块,我们找一个star数最多的直接go get安装一下即可使用了:

go get -u github.com/jlaffaye/ftp

我们把所有的扫描模块放到plugins目录中,FTP协议的扫描插件如下所示:

package pluginsimport (	"github.com/jlaffaye/ftp"	"x-crack/models"	"x-crack/vars"	"fmt")func ScanFtp(s models.Service) (err error, result models.ScanResult) {	result.Service = s	conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", s.Ip, s.Port), vars.TimeOut)	if err == nil {		err = conn.Login(s.Username, s.Password)		if err == nil {			defer conn.Logout()			result.Result = true		}	}	return err, result}

每个连接需要设置超时时间,防止因网络问题导致的阻塞,我们打算通过程序的命令行来控制超时时间,所以定义了一个全局变量TimeOut。 放在vars模块中的原因是防止放在这个模块中后会和其他模块互相调用导致的循环import

写代码虽然可以一把梭,但是不能等着洋洋洒洒地把几万行都写完再运行,比如我们的目标是造一辆豪车,不能等着所有零件设计好,都装上去再发动车测试,正确的开发流程是把写边测,不要等轮子造出来,而是在螺丝、齿轮阶段就测试。

以下为FTP扫描插件这个齿轮的测试代码及结果。

package plugins_testimport (	"x-crack/models"	"x-crack/plugins"	"testing")func TestScanFtp(t *testing.T) {	s := models.Service{Ip: "127.0.0.1", Port: 21, Protocol: "ftp", Username: "ftp", Password: "ftp"}	t.Log(plugins.ScanFtp(s))}

测试结果满足预期,说明我们这个零件不是次品,可以继续再造其他零件了。

$ go test -v plugins/ftp_test.go=== RUN   TestScanFtp--- PASS: TestScanFtp (0.00s)	ftp_test.go:36: dial tcp 127.0.0.1:21: getsockopt: connection refused {{127.0.0.1 21 ftp ftp ftp} false}PASSok  	command-line-arguments	0.025s

SSH扫描模块

go的标准库中自带了ssh包,直接调用即可,完整代码如下:

package pluginsimport (	"golang.org/x/crypto/ssh"	"x-crack/models"	"x-crack/vars"	"fmt"	"net")func ScanSsh(s models.Service) (err error, result models.ScanResult) {	result.Service = s	config := &ssh.ClientConfig{		User: s.Username,		Auth: []ssh.AuthMethod{			ssh.Password(s.Password),		},		Timeout: vars.TimeOut,		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {			return nil		},	}	client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", s.Ip, s.Port), config)	if err == nil {		defer client.Close()		session, err := client.NewSession()		errRet := session.Run("echo xsec")		if err == nil && errRet == nil {			defer session.Close()			result.Result = true		}	}	return err, result}

同样,每个子模块写好后都需要先用go test跑一下看是否满足预期,测试代码如下:

package plugins_testimport (	"x-crack/models"	"x-crack/plugins"	"testing")func TestScanSsh(t *testing.T) {	s := models.Service{Ip: "127.0.0.1", Port: 22, Username: "root", Password: "123456", Protocol: "ssh"}	t.Log(plugins.ScanSsh(s))}

测试结果如下:

$ go test -v plugins/ssh_test.go=== RUN   TestScanSsh--- PASS: TestScanSsh (0.00s)	ssh_test.go:36: dial tcp 127.0.0.1:22: getsockopt: connection refused {{127.0.0.1 22 ssh root 123456} false}PASSok  	command-line-arguments	0.026s

SMB扫描模块

SMB弱口令的扫描插件,我们使用了github.com/stacktitan/smb/smb包,同样直接go get安装一下即可拿来使用。 代码如下:

package pluginsimport (	"github.com/stacktitan/smb/smb"	"x-crack/models")func ScanSmb(s models.Service) (err error, result models.ScanResult) {	result.Service = s	options := smb.Options{		Host:        s.Ip,		Port:        s.Port,		User:        s.Username,		Password:    s.Password,		Domain:      "",		Workstation: "",	}	session, err := smb.NewSession(options, false)	if err == nil {		session.Close()		if session.IsAuthenticated {			result.Result = true		}	}	return err, result}

同样也先写测试用例来测试一下,测试代码如下:

package plugins_testimport (	"x-crack/models"	"x-crack/plugins"	"testing")func TestScanSmb(t *testing.T) {	s := models.Service{Ip: "share.xsec.io", Port: 445, Protocol: "smb", Username: "xsec", Password: "fsafffdsfdsa"}	t.Log(plugins.ScanSmb(s))}

测试结果:

hartnett at hartnettdeMacBook-Pro in /data/code/golang/src/x-crack (master)$ go test -v plugins/smb_test.go=== RUN   TestScanSmb--- PASS: TestScanSmb (0.04s)	smb_test.go:36: NT Status Error: Logon failed		 {{share.xsec.io 445 smb xsec fsafffdsfdsa} false}PASSok  	command-line-arguments	0.069s

MYSQL、MSSQL和POSTGRESQL扫描模块

MYSQL、MSSQL和POSTGRESQL的扫描模块,我使用了第三方的ORM xorm,当然也可以直接使用原生的sql driver来实现,我们这里图方便用xorm一把梭了。 对于xorm来说,这3个扫描插件的实现方法大同小异,为了节约篇幅,咱们只看mysql扫描插件的实现,其他2个插件可以参考github中的完整源码。 首先还是先go get要用到的包:

go get github.com/netxfly/mysqlgo get github.com/go-xorm/xormgithub.com/go-xorm/core

接下来我们把需要验证的IP、port、username、password组成datasource传递给xorm,完整代码如下:

package pluginsimport (	_ "github.com/netxfly/mysql"	"github.com/go-xorm/xorm"	"github.com/go-xorm/core"	"x-crack/models"	"fmt")func ScanMysql(service models.Service) (err error, result models.ScanResult) {	result.Service = service	dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8", service.Username,		service.Password, service.Ip, service.Port, "mysql")	Engine, err := xorm.NewEngine("mysql", dataSourceName)	if err == nil {		Engine.SetLogLevel(core.LOG_OFF)		// fix "[mysql] packets.go:33: unexpected EOF" error		Engine.SetMaxIdleConns(0)		// Engine.SetConnMaxLifetime(time.Second * 30)		defer Engine.Close()		err = Engine.Ping()		if err == nil {			result.Result = true		}	}	return err, result}

眼尖的同学也许发现了,上面 github.com/netxfly/mysql 这个mysql包是放在笔者的github下的,这是为什么呢?

因为直接用mysql这个包的话,在扫描的过程中会遇到[mysql] packets.go:33: unexpected EOF" error的异常输出,影响了我们程序在扫描过程中输出UI的美观性,这对于帅气的我是无法接受的,通过设置参数的方法无法解决,最后只好直接fork了一份mysql的包,把打印这个异常的语句注释掉再提交上去直接使用了。

测试代码:

package plugins_testimport (	"testing"	"x-crack/plugins"	"x-crack/models")func TestScanMysql(t *testing.T) {	service := models.Service{Ip: "10.10.10.10", Port: 3306, Protocol: "mysql", Username: "root", Password: "123456"}	t.Log(plugins.ScanMysql(service))}

测试结果:

go test -v plugins/mysql_test.go=== RUN   TestScanMysql--- PASS: TestScanMysql (0.02s)	mysql_test.go:36: Error 1045: Access denied for user 'root'@'10.10.10.100' (using password: YES) {{10.10.10.10 3306 mysql root 123456} false}PASSok  	command-line-arguments	0.041s

Redis扫描模块

go get安装第三方包github.com/go-redis/redis,完整代码如下:

package pluginsimport (	"github.com/go-redis/redis"	"x-crack/models"	"x-crack/vars"	"fmt")func ScanRedis(s models.Service) (err error, result models.ScanResult) {	result.Service = s	opt := redis.Options{Addr: fmt.Sprintf("%v:%v", s.Ip, s.Port),		Password: s.Password, DB: 0, DialTimeout: vars.TimeOut}	client := redis.NewClient(&opt)	defer client.Close()	_, err = client.Ping().Result()	if err == nil {		result.Result = true	}	return err, result}

测试代码:

package plugins_testimport (	"x-crack/models"	"x-crack/plugins"	"testing")func TestScanRedis(t *testing.T) {	s := models.Service{Ip: "127.0.0.1", Port: 6379, Password: "test"}	t.Log(plugins.ScanRedis(s))}

测试结果:

go test -v plugins/redis_test.go=== RUN   TestScanRedis--- PASS: TestScanRedis (0.00s)	redis_test.go:36: dial tcp 127.0.0.1:6379: getsockopt: connection refused {{127.0.0.1 6379   test} false}PASSok  	command-line-arguments	0.025s

MONGODB扫描模块

mongodb扫描模块依赖mgo包,可用go get合令直接安装。

go get gopkg.in/mgo.v2

完整代码:

package pluginsimport (	"gopkg.in/mgo.v2"	"x-crack/models"	"x-crack/vars"	"fmt")func ScanMongodb(s models.Service) (err error, result models.ScanResult) {	result.Service = s	url := fmt.Sprintf("mongodb://%v:%v@%v:%v/%v", s.Username, s.Password, s.Ip, s.Port, "test")	session, err := mgo.DialWithTimeout(url, vars.TimeOut)	if err == nil {		defer session.Close()		err = session.Ping()		if err == nil {			result.Result = true		}	}	return err, result}

测试结果:

go test -v plugins/mongodb_test.go=== RUN   TestScanMongodb--- PASS: TestScanMongodb (3.53s)	mongodb_test.go:36: no reachable servers {{127.0.0.1 27017 mongodb test test} false}PASSok  	command-line-arguments	3.558s

ELASTICSEARCH扫描模块

ELASTICSEARCH扫描插件依赖第三方包gopkg.in/olivere/elastic.v3,同样也是直接go get安装。 完整代码如下:

package pluginsimport (	"gopkg.in/olivere/elastic.v3"	"x-crack/models"		"fmt")func ScanElastic(s models.Service) (err error, result models.ScanResult) {	result.Service = s	client, err := elastic.NewClient(elastic.SetURL(fmt.Sprintf("http://%v:%v", s.Ip, s.Port)),		elastic.SetMaxRetries(3),		elastic.SetBasicAuth(s.Username, s.Password),	)	if err == nil {		_, _, err = client.Ping(fmt.Sprintf("http://%v:%v", s.Ip, s.Port)).Do()		if err == nil {			result.Result = true		}	}	return err, result}

测试代码:

package plugins_testimport (	"x-crack/models"	"x-crack/plugins"	"testing")func TestScanElastic(t *testing.T) {	s := models.Service{Ip: "127.0.0.1", Port: 9200, Protocol: "elastic", Username: "root", Password: "123456"}	t.Log(plugins.ScanElastic(s))}

测试结果如下:

go test -v plugins/elastic_test.go=== RUN   TestScanElastic--- PASS: TestScanElastic (5.02s)	elastic_test.go:36: no Elasticsearch node available {{127.0.0.1 9200 elastic root 123456} false}PASSok  	command-line-arguments	5.061s

扫描模块插件化

前面我们写好的扫描插件的函数原始是一致,我们可以将这组函数放到一个map中,在扫描的过程中自动化根据不同的协议调用不同的扫描插件。

以后新加的扫描插件,可以按这种方法直接注册。

package pluginsimport (	"x-crack/models")type ScanFunc func(service models.Service) (err error, result models.ScanResult)var (	ScanFuncMap map[string]ScanFunc)func init() {	ScanFuncMap = make(map[string]ScanFunc)	ScanFuncMap["FTP"] = ScanFtp	ScanFuncMap["SSH"] = ScanSsh	ScanFuncMap["SMB"] = ScanSmb	ScanFuncMap["MSSQL"] = ScanMssql	ScanFuncMap["MYSQL"] = ScanMysql	ScanFuncMap["POSTGRESQL"] = ScanPostgres	ScanFuncMap["REDIS"] = ScanRedis	ScanFuncMap["ELASTICSEARCH"] = ScanElastic	ScanFuncMap["MONGODB"] = ScanMongodb}

扫描任务调度

前面我们写好了一些常见服务的弱口令扫描插件,也测试通过了。 接下来我们需要实现从命令行参数传递iplist、用户名字典和密码字典进去,并读取相应的信息进行扫描调度的功能,细分一下,需要做以下几件事:

  • 读取iplist列表
  • 读取用户名字典
  • 读取密码字典
  • 生成扫描任务
  • 扫描任务调度
  • 扫描任务执行
  • 扫描结果保存
  • 命令行调用外壳

读取ip\用户名和密码字典

该模块主要用了标准库中的bufio包,逐行读取文件,进行过滤后直接生成相应的slice。其中iplist支持以下格式:

127.0.0.1:3306|mysql8.8.8.8:229.9.9.9:6379108.61.223.105:2222|ssh

对于标准的端口,程序可以自动判断其协议,对于非标准端口的协议,需要在后面加一个字段标注一下协议。

为了防止咱们的程序被脚本小子们滥用,老夫就不提供端口扫描、协议识别等功能了,安全工程师们可以把自己公司的端口扫描器产出的结果丢到这个里面来扫。

package utilimport (	"x-crack/models"	"x-crack/logger"	"x-crack/vars"	"os"	"bufio"	"strings"	"strconv")func ReadIpList(fileName string) (ipList []models.IpAddr) {	ipListFile, err := os.Open(fileName)	if err != nil {		logger.Log.Fatalf("Open ip List file err, %v", err)	}	defer ipListFile.Close()	scanner := bufio.NewScanner(ipListFile)	scanner.Split(bufio.ScanLines)	for scanner.Scan() {		ipPort := strings.TrimSpace(scanner.Text())		t := strings.Split(ipPort, ":")		ip := t[0]		portProtocol := t[1]		tmpPort := strings.Split(portProtocol, "|")		// ip列表中指定了端口对应的服务		if len(tmpPort) == 2 {			port, _ := strconv.Atoi(tmpPort[0])			protocol := strings.ToUpper(tmpPort[1])			if vars.SupportProtocols[protocol] {				addr := models.IpAddr{Ip: ip, Port: port, Protocol: protocol}				ipList = append(ipList, addr)			} else {				logger.Log.Infof("Not support %v, ignore: %v:%v", protocol, ip, port)			}		} else {			// 通过端口查服务			port, err := strconv.Atoi(tmpPort[0])			if err == nil {				protocol, ok := vars.PortNames[port]				if ok && vars.SupportProtocols[protocol] {					addr := models.IpAddr{Ip: ip, Port: port, Protocol: protocol}					ipList = append(ipList, addr)				}
                

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。