我们在做企业安全时,弱口令检测是系统/网络安全的最基础的部分之一,根据经验,经常会出现弱口令的服务如下:
FTPSSHSMBMYSQLMSSQLPOSTGRESQLREDISMONGODBELASTICSEARCH那咱们就一起用GO来写一款常见服务的弱口令扫描器,且支持以插件的形式增加新的服务扫描模块。我们的教程暂定为只扫以上服务。
给扫描器启一个屌炸天的名字x-crack,在$GOPATH/src/中建立一个x-crack项目后开始撸码,不要给我说什么底层原理、框架内核,老夫敲代码就是一把梭。
开工数据结构定义扫描模块的输入内容为为IP、端口及协议的列表,我们需要定义一个IpAddr的数据结构;每个服务的每次扫描需要传入的参数为IP、端口、协议、用户名和密码,需要定义一个Service结构来包括这些内容;每条Service的记录在扫描模块进行尝试后,会得出扫描结果成功与否,我们再定义一个ScanResult数据结构。按照开发规范,数据结构的定义统一放到models目录中,全部的数据结构定义如下:
packagemodelstypeServicestruct{IpstringPortintProtocolstringUsernamestringPasswordstring}typeScanResultstruct{ServiceServiceResultbool}typeIpAddrstruct{IpstringPortintProtocolstring}FTP扫描模块go语言有现成的FTP模块,我们找一个star数最多的直接goget安装一下即可使用了:
goget-ugithub.com/jlaffaye/ftp我们把所有的扫描模块放到plugins目录中,FTP协议的扫描插件如下所示:
packagepluginsimport("github.com/jlaffaye/ftp""x-crack/models""x-crack/vars""fmt")funcScanFtp(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=sconn,err:=ftp.DialTimeout(fmt.Sprintf("%v:%v",s.Ip,s.Port),vars.TimeOut)iferr==nil{err=conn.Login(s.Username,s.Password)iferr==nil{deferconn.Logout()result.Result=true}}returnerr,result}每个连接需要设置超时时间,防止因网络问题导致的阻塞,我们打算通过程序的命令行来控制超时时间,所以定义了一个全局变量TimeOut。放在vars模块中的原因是防止放在这个模块中后会和其他模块互相调用导致的循环import
写代码虽然可以一把梭,但是不能等着洋洋洒洒地把几万行都写完再运行,比如我们的目标是造一辆豪车,不能等着所有零件设计好,都装上去再发动车测试,正确的开发流程是把写边测,不要等轮子造出来,而是在螺丝、齿轮阶段就测试。
以下为FTP扫描插件这个齿轮的测试代码及结果。
packageplugins_testimport("x-crack/models""x-crack/plugins""testing")funcTestScanFtp(t*testing.T){s:=models.Service{Ip:"127.0.0.1",Port:21,Protocol:"ftp",Username:"ftp",Password:"ftp"}t.Log(plugins.ScanFtp(s))}测试结果满足预期,说明我们这个零件不是次品,可以继续再造其他零件了。
$gotest-vplugins/ftp_test.go===RUNTestScanFtp---PASS:TestScanFtp(0.00s)ftp_test.go:36:dialtcp127.0.0.1:21:getsockopt:connectionrefused{{127.0.0.121ftpftpftp}false}PASSokcommand-line-arguments0.025sSSH扫描模块go的标准库中自带了ssh包,直接调用即可,完整代码如下:
packagepluginsimport("golang.org/x/crypto/ssh""x-crack/models""x-crack/vars""fmt""net")funcScanSsh(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=sconfig:=&ssh.ClientConfig{User:s.Username,Auth:[]ssh.AuthMethod{ssh.Password(s.Password),},Timeout:vars.TimeOut,HostKeyCallback:func(hostnamestring,remotenet.Addr,keyssh.PublicKey)error{returnnil},}client,err:=ssh.Dial("tcp",fmt.Sprintf("%v:%v",s.Ip,s.Port),config)iferr==nil{deferclient.Close()session,err:=client.NewSession()errRet:=session.Run("echoxsec")iferr==nil&&errRet==nil{defersession.Close()result.Result=true}}returnerr,result}同样,每个子模块写好后都需要先用gotest跑一下看是否满足预期,测试代码如下:
packageplugins_testimport("x-crack/models""x-crack/plugins""testing")funcTestScanSsh(t*testing.T){s:=models.Service{Ip:"127.0.0.1",Port:22,Username:"root",Password:"123456",Protocol:"ssh"}t.Log(plugins.ScanSsh(s))}测试结果如下:
$gotest-vplugins/ssh_test.go===RUNTestScanSsh---PASS:TestScanSsh(0.00s)ssh_test.go:36:dialtcp127.0.0.1:22:getsockopt:connectionrefused{{127.0.0.122sshroot123456}false}PASSokcommand-line-arguments0.026sSMB扫描模块SMB弱口令的扫描插件,我们使用了github.com/stacktitan/smb/smb包,同样直接goget安装一下即可拿来使用。代码如下:
packagepluginsimport("github.com/stacktitan/smb/smb""x-crack/models")funcScanSmb(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=soptions:=smb.Options{Host:s.Ip,Port:s.Port,User:s.Username,Password:s.Password,Domain:"",Workstation:"",}session,err:=smb.NewSession(options,false)iferr==nil{session.Close()ifsession.IsAuthenticated{result.Result=true}}returnerr,result}同样也先写测试用例来测试一下,测试代码如下:
packageplugins_testimport("x-crack/models""x-crack/plugins""testing")funcTestScanSmb(t*testing.T){s:=models.Service{Ip:"share.xsec.io",Port:445,Protocol:"smb",Username:"xsec",Password:"fsafffdsfdsa"}t.Log(plugins.ScanSmb(s))}测试结果:
hartnettathartnettdeMacBook-Proin/data/code/golang/src/x-crack(master)$gotest-vplugins/smb_test.go===RUNTestScanSmb---PASS:TestScanSmb(0.04s)smb_test.go:36:NTStatusError:Logonfailed{{share.xsec.io445smbxsecfsafffdsfdsa}false}PASSokcommand-line-arguments0.069sMYSQL、MSSQL和POSTGRESQL扫描模块MYSQL、MSSQL和POSTGRESQL的扫描模块,我使用了第三方的ORM xorm,当然也可以直接使用原生的sqldriver来实现,我们这里图方便用xorm一把梭了。对于xorm来说,这3个扫描插件的实现方法大同小异,为了节约篇幅,咱们只看mysql扫描插件的实现,其他2个插件可以参考github中的完整源码。首先还是先goget要用到的包:
gogetgithub.com/netxfly/mysqlgogetgithub.com/go-xorm/xormgithub.com/go-xorm/core接下来我们把需要验证的IP、port、username、password组成datasource传递给xorm,完整代码如下:
packagepluginsimport(_"github.com/netxfly/mysql""github.com/go-xorm/xorm""github.com/go-xorm/core""x-crack/models""fmt")funcScanMysql(servicemodels.Service)(errerror,resultmodels.ScanResult){result.Service=servicedataSourceName:=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)iferr==nil{Engine.SetLogLevel(core.LOG_OFF)//fix"[mysql]packets.go:33:unexpectedEOF"errorEngine.SetMaxIdleConns(0)//Engine.SetConnMaxLifetime(time.Second*30)deferEngine.Close()err=Engine.Ping()iferr==nil{result.Result=true}}returnerr,result}眼尖的同学也许发现了,上面 github.com/netxfly/mysql 这个mysql包是放在笔者的github下的,这是为什么呢?
因为直接用mysql这个包的话,在扫描的过程中会遇到[mysql]packets.go:33:unexpectedEOF"error的异常输出,影响了我们程序在扫描过程中输出UI的美观性,这对于帅气的我是无法接受的,通过设置参数的方法无法解决,最后只好直接fork了一份mysql的包,把打印这个异常的语句注释掉再提交上去直接使用了。
测试代码:
packageplugins_testimport("testing""x-crack/plugins""x-crack/models")funcTestScanMysql(t*testing.T){service:=models.Service{Ip:"10.10.10.10",Port:3306,Protocol:"mysql",Username:"root",Password:"123456"}t.Log(plugins.ScanMysql(service))}测试结果:
gotest-vplugins/mysql_test.go===RUNTestScanMysql---PASS:TestScanMysql(0.02s)mysql_test.go:36:Error1045:Accessdeniedforuser'root'@'10.10.10.100'(usingpassword:YES){{10.10.10.103306mysqlroot123456}false}PASSokcommand-line-arguments0.041sRedis扫描模块goget安装第三方包github.com/go-redis/redis,完整代码如下:
packagepluginsimport("github.com/go-redis/redis""x-crack/models""x-crack/vars""fmt")funcScanRedis(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=sopt:=redis.Options{Addr:fmt.Sprintf("%v:%v",s.Ip,s.Port),Password:s.Password,DB:0,DialTimeout:vars.TimeOut}client:=redis.NewClient(&opt)deferclient.Close()_,err=client.Ping().Result()iferr==nil{result.Result=true}returnerr,result}测试代码:
packageplugins_testimport("x-crack/models""x-crack/plugins""testing")funcTestScanRedis(t*testing.T){s:=models.Service{Ip:"127.0.0.1",Port:6379,Password:"test"}t.Log(plugins.ScanRedis(s))}测试结果:
gotest-vplugins/redis_test.go===RUNTestScanRedis---PASS:TestScanRedis(0.00s)redis_test.go:36:dialtcp127.0.0.1:6379:getsockopt:connectionrefused{{127.0.0.16379test}false}PASSokcommand-line-arguments0.025sMONGODB扫描模块mongodb扫描模块依赖mgo包,可用goget合令直接安装。
gogetgopkg.in/mgo.v2完整代码:
packagepluginsimport("gopkg.in/mgo.v2""x-crack/models""x-crack/vars""fmt")funcScanMongodb(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=surl:=fmt.Sprintf("mongodb://%v:%v@%v:%v/%v",s.Username,s.Password,s.Ip,s.Port,"test")session,err:=mgo.DialWithTimeout(url,vars.TimeOut)iferr==nil{defersession.Close()err=session.Ping()iferr==nil{result.Result=true}}returnerr,result}测试结果:
gotest-vplugins/mongodb_test.go===RUNTestScanMongodb---PASS:TestScanMongodb(3.53s)mongodb_test.go:36:noreachableservers{{127.0.0.127017mongodbtesttest}false}PASSokcommand-line-arguments3.558sELASTICSEARCH扫描模块ELASTICSEARCH扫描插件依赖第三方包gopkg.in/olivere/elastic.v3,同样也是直接goget安装。完整代码如下:
packagepluginsimport("gopkg.in/olivere/elastic.v3""x-crack/models""fmt")funcScanElastic(smodels.Service)(errerror,resultmodels.ScanResult){result.Service=sclient,err:=elastic.NewClient(elastic.SetURL(fmt.Sprintf("https://%v:%v",s.Ip,s.Port)),elastic.SetMaxRetries(3),elastic.SetBasicAuth(s.Username,s.Password),)iferr==nil{_,_,err=client.Ping(fmt.Sprintf("https://%v:%v",s.Ip,s.Port)).Do()iferr==nil{result.Result=true}}returnerr,result}测试代码:
packageplugins_testimport("x-crack/models""x-crack/plugins""testing")funcTestScanElastic(t*testing.T){s:=models.Service{Ip:"127.0.0.1",Port:9200,Protocol:"elastic",Username:"root",Password:"123456"}t.Log(plugins.ScanElastic(s))}测试结果如下:
gotest-vplugins/elastic_test.go===RUNTestScanElastic---PASS:TestScanElastic(5.02s)elastic_test.go:36:noElasticsearchnodeavailable{{127.0.0.19200elasticroot123456}false}PASSokcommand-line-arguments5.061s扫描模块插件化前面我们写好的扫描插件的函数原始是一致,我们可以将这组函数放到一个map中,在扫描的过程中自动化根据不同的协议调用不同的扫描插件。
以后新加的扫描插件,可以按这种方法直接注册。
packagepluginsimport("x-crack/models")typeScanFuncfunc(servicemodels.Service)(errerror,resultmodels.ScanResult)var(ScanFuncMapmap[string]ScanFunc)funcinit(){ScanFuncMap=make(map[string]ScanFunc)ScanFuncMap["FTP"]=ScanFtpScanFuncMap["SSH"]=ScanSshScanFuncMap["SMB"]=ScanSmbScanFuncMap["MSSQL"]=ScanMssqlScanFuncMap["MYSQL"]=ScanMysqlScanFuncMap["POSTGRESQL"]=ScanPostgresScanFuncMap["REDIS"]=ScanRedisScanFuncMap["ELASTICSEARCH"]=ScanElasticScanFuncMap["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对于标准的端口,程序可以自动判断其协议,对于非标准端口的协议,需要在后面加一个字段标注一下协议。
为了防止咱们的程序被脚本小子们滥用,老夫就不提供端口扫描、协议识别等功能了,安全工程师们可以把自己公司的端口扫描器产出的结果丢到这个里面来扫。
packageutilimport("x-crack/models""x-crack/logger""x-crack/vars""os""bufio""strings""strconv")funcReadIpList(fileNamestring)(ipList[]models.IpAddr){ipListFile,err:=os.Open(fileName)iferr!=nil{logger.Log.Fatalf("OpenipListfileerr,%v",err)}deferipListFile.Close()scanner:=bufio.NewScanner(ipListFile)scanner.Split(bufio.ScanLines)forscanner.Scan(){ipPort:=strings.TrimSpace(scanner.Text())t:=strings.Split(ipPort,":")ip:=t[0]portProtocol:=t[1]tmpPort:=strings.Split(portProtocol,"|")//ip列表中指定了端口对应的服务iflen(tmpPort)==2{port,_:=strconv.Atoi(tmpPort[0])protocol:=strings.ToUpper(tmpPort[1])ifvars.SupportProtocols[protocol]{addr:=models.IpAddr{Ip:ip,Port:port,Protocol:protocol}ipList=append(ipList,addr)}else{logger.Log.Infof("Notsupport%v,ignore:%v:%v",protocol,ip,port)}}else{//通过端口查服务port,err:=strconv.Atoi(tmpPort[0])iferr==nil{protocol,ok:=vars.PortNames[port]ifok&&vars.SupportProtocols[protocol]{addr:=models.IpAddr{Ip:ip,Port:port,Protocol:protocol}ipList=append(ipList,addr)}}}}returnipList}funcReadUserDict(userDictstring)(users[]string,errerror){file,err:=os.Open(userDict)iferr!=nil{logger.Log.Fatalf("Openuserdictfileerr,%v",err)}deferfile.Close()scanner:=bufio.NewScanner(file)scanner.Split(bufio.ScanLines)forscanner.Scan(){user:=strings.TrimSpace(scanner.Text())ifuser!=""{users=append(users,user)}}returnusers,err}funcReadPasswordDict(passDictstring)(password[]string,errerror){file,err:=os.Open(passDict)iferr!=nil{logger.Log.Fatalf("Openpassworddictfileerr,%v",err)}deferfile.Close()scanner:=bufio.NewScanner(file)scanner.Split(bufio.ScanLines)forscanner.Scan(){passwd:=strings.TrimSpace(scanner.Text())ifpasswd!=""{password=append(password,passwd)}}password=append(password,"")returnpassword,err}IP列表、用户名字典与密码字典读取的测试代码:
packageutil_testimport("x-crack/util""testing")funcTestReadIpList(t*testing.T){ipList:="/tmp/iplist.txt"t.Log(util.ReadIpList(ipList))}funcTestReadUserDict(t*testing.T){userDict:="/tmp/user.dic"t.Log(util.ReadUserDict(userDict))}funcTestReadPasswordDict(t*testing.T){passDict:="/tmp/pass.dic"t.Log(util.ReadPasswordDict(passDict))}这个模块的测试结果如下:
gotest-vutil/file_test.go===RUNTestReadIpList---PASS:TestReadIpList(0.00s)file_test.go:35:[{127.0.0.13306MYSQL}{8.8.8.822SSH}{9.9.9.96379REDIS}{108.61.223.1052222SSH}]===RUNTestReadUserDict---PASS:TestReadUserDict(0.00s)file_test.go:40:[rootadmintestguestinfoadmmysqluseradministratorftpsa]<nil>===RUNTestReadPasswordDict---PASS:TestReadPasswordDict(0.00s)file_test.go:45:[1314520520135246135246789135792468135792468014725836914725836901qaz2wsx5201314543215555565432178945612388888888888888888889876543219876543210^%$#@~!a123123a123456a12345678a123456789aa123456aa123456789aaa123456aaaaaaaaaaaaaaaaaaaabc123abc123456abc123456789abcd123abcd1234abcd123456adminadmin888]<nil>PASSokcommand-line-arguments0.022s其中iplist在加载的过程中不是无脑全部读进去的,在正式扫描前会先过滤一次,把不通的ip和端口对剔除掉,以免影响扫描效率,代码如下:
packageutilimport("gopkg.in/cheggaaa/pb.v2""x-crack/models""x-crack/logger""x-crack/vars""net""sync""fmt")var(AliveAddr[]models.IpAddrmutexsync.Mutex)funcinit(){AliveAddr=make([]models.IpAddr,0)}funcCheckAlive(ipList[]models.IpAddr)([]models.IpAddr){logger.Log.Infoln("checkingipactive")varwgsync.WaitGroupwg.Add(len(ipList))for_,addr:=rangeipList{gofunc(addrmodels.IpAddr){deferwg.Done()SaveAddr(check(addr))}(addr)}wg.Wait()vars.ProcessBarActive.Finish()returnAliveAddr}funccheck(ipAddrmodels.IpAddr)(bool,models.IpAddr){alive:=false_,err:=net.DialTimeout("tcp",fmt.Sprintf("%v:%v",ipAddr.Ip,ipAddr.Port),vars.TimeOut)iferr==nil{alive=true}vars.ProcessBarActive.Increment()returnalive,ipAddr}funcSaveAddr(alivebool,ipAddrmodels.IpAddr){ifalive{mutex.Lock()AliveAddr=append(AliveAddr,ipAddr)mutex.Unlock()}}通过标准端口查询对应服务的功能在vars包中定义了,为了避免多个包之间的循环导入,我们把所有的全局变量都集中到了一个独立的vars包中。
PortNamesmap为标准端口对应的服务,在加了新的扫描插件后,也需要更新这个map的内容。
packagevarsimport("github.com/patrickmn/go-cache""gopkg.in/cheggaaa/pb.v2""sync""time""strings")var(IpList="iplist.txt"ResultFile="x_crack.txt"UserDict="user.dic"PassDict="pass.dic"TimeOut=3*time.SecondScanNum=5000DebugModeboolStartTimetime.TimeProgressBar*pb.ProgressBarProcessBarActive*pb.ProgressBar)var(CacheService*cache.CacheMutexsync.MutexPortNames=map[int]string{21:"FTP",22:"SSH",445:"SMB",1433:"MSSQL",3306:"MYSQL",5432:"POSTGRESQL",6379:"REDIS",9200:"ELASTICSEARCH",27017:"MONGODB",}//标记特定服务的特定用户是否破解成功,成功的话不再尝试破解该用户SuccessHashmap[string]boolSupportProtocolsmap[string]bool)funcinit(){SuccessHash=make(map[string]bool)CacheService=cache.New(cache.NoExpiration,cache.DefaultExpiration)SupportProtocols=make(map[string]bool)for_,proto:=rangePortNames{SupportProtocols[strings.ToUpper(proto)]=true}}任务调度任务调度模块包含了生成扫描任务,按指定的协程数分发和执行扫描任务的功能。
packageutilimport("github.com/sirupsen/logrus""gopkg.in/cheggaaa/pb.v2""x-crack/models""x-crack/logger""x-crack/vars""x-crack/util/hash""x-crack/plugins""sync""strings""fmt""time")funcGenerateTask(ipList[]models.IpAddr,users[]string,passwords[]string)(tasks[]models.Service,taskNumint){tasks=make([]models.Service,0)for_,user:=rangeusers{for_,password:=rangepasswords{for_,addr:=rangeipList{service:=models.Service{Ip:addr.Ip,Port:addr.Port,Protocol:addr.Protocol,Username:user,Password:password}tasks=append(tasks,service)}}}returntasks,len(tasks)}funcDistributionTask(tasks[]models.Service)(){totalTask:=len(tasks)scanBatch:=totalTask/vars.ScanNumlogger.Log.Infoln("Starttoscan")fori:=0;i<scanBatch;i++{curTasks:=tasks[vars.ScanNum*i:vars.ScanNum*(i+1)]ExecuteTask(curTasks)}iftotalTask%vars.ScanNum>0{lastTask:=tasks[vars.ScanNum*scanBatch:totalTask]ExecuteTask(lastTask)}models.SavaResultToFile()models.ResultTotal()models.DumpToFile(vars.ResultFile)}funcExecuteTask(tasks[]models.Service)(){varwgsync.WaitGroupwg.Add(len(tasks))for_,task:=rangetasks{ifvars.DebugMode{logger.Log.Debugf("checking:Ip:%v,Port:%v,[%v],UserName:%v,Password:%v",task.Ip,task.Port,task.Protocol,task.Username,task.Password)}varkstringprotocol:=strings.ToUpper(task.Protocol)ifprotocol=="REDIS"||protocol=="FTP"{k=fmt.Sprintf("%v-%v-%v",task.Ip,task.Port,task.Protocol)}else{k=fmt.Sprintf("%v-%v-%v",task.Ip,task.Port,task.Username)}h:=hash.MakeTaskHash(k)ifhash.CheckTashHash(h){wg.Done()continue}gofunc(taskmodels.Service,protocolstring){deferwg.Done()fn:=plugins.ScanFuncMap[protocol]models.SaveResult(fn(task))}(task,protocol)vars.ProgressBar.Increment()}waitTimeout(&wg,vars.TimeOut)}个别扫描插件没有指定超时时间的功能,所以我们额外为所有的扫描插件都提供了一个超时函数,防止个别协程被阻塞,影响了扫描器整体的速度。
//waitTimeoutwaitsforthewaitgroupforthespecifiedmaxtimeout.//Returnstrueifwaitingtimedout.funcwaitTimeout(wg*sync.WaitGroup,timeouttime.Duration)bool{c:=make(chanstruct{})gofunc(){deferclose(c)wg.Wait()}()select{case<-c:returnfalse//completednormallycase<-time.After(timeout):returntrue//timedout}}任务调度模块的测试代码如下:
packageutil_testimport("x-crack/util""testing")funcTestGenerateTask(t*testing.T){ipList:="/tmp/iplist.txt"userDic:="/tmp/user.dic"passDic:="/tmp/pass.dic"users,_:=util.ReadUserDict(userDic)passwords,_:=util.ReadPasswordDict(passDic)t.Log(util.GenerateTask(util.ReadIpList(ipList),users,passwords))}funcTestDistributionTask(t*testing.T){ipList:="/tmp/iplist.txt"userDic:="/tmp/user.dic"passDic:="/tmp/pass.dic"users,_:=util.ReadUserDict(userDic)passwords,_:=util.ReadPasswordDict(passDic)tasks,_:=util.GenerateTask(util.ReadIpList(ipList),users,passwords)util.DistributionTask(tasks)}测试结果如下:
$gotest-vutil/task_test.go===RUNTestGenerateTask---PASS:TestGenerateTask(0.00s)task_test.go:41:[{127.0.0.13306MYSQLrootadmin}{8.8.8.822SSHrootadmin}{9.9.9.96379REDISrootadmin}{108.61.223.1052222SSHrootadmin}{127.0.0.13306MYSQLrootadmin888}{8.8.8.822SSHrootadmin888}{9.9.9.96379REDISrootadmin888}{108.61.223.1052222SSHrootadmin888}{127.0.0.13306MYSQLroot123456}{8.8.8.822SSHroot123456}{9.9.9.96379REDISroot123456}{108.61.223.1052222SSHroot123456}{127.0.0.13306MYSQLroot}{8.8.8.822SSHroot}{9.9.9.96379REDISroot}{108.61.223.1052222SSHroot}{127.0.0.13306MYSQLadminadmin}{8.8.8.822SSHadminadmin}{9.9.9.96379REDISadminadmin}{108.61.223.1052222SSHadminadmin}{127.0.0.13306MYSQLadminadmin888}{8.8.8.822SSHadminadmin888}{9.9.9.96379REDISadminadmin888}{108.61.223.1052222SSHadminadmin888}{127.0.0.13306MYSQLadmin123456}{8.8.8.822SSHadmin123456}{9.9.9.96379REDISadmin123456}{108.61.223.1052222SSHadmin123456}{127.0.0.13306MYSQLadmin}{8.8.8.822SSHadmin}{9.9.9.96379REDISadmin}{108.61.223.1052222SSHadmin}{127.0.0.13306MYSQLtestadmin}{8.8.8.822SSHtestadmin}{9.9.9.96379REDIStestadmin}{108.61.223.1052222SSHtestadmin}{127.0.0.13306MYSQLtestadmin888}{8.8.8.822SSHtestadmin888}{9.9.9.96379REDIStestadmin888}{108.61.223.1052222SSHtestadmin888}{127.0.0.13306MYSQLtest123456}{8.8.8.822SSHtest123456}{9.9.9.96379REDIStest123456}{108.61.223.1052222SSHtest123456}{127.0.0.13306MYSQLtest}{8.8.8.822SSHtest}{9.9.9.96379REDIStest}{108.61.223.1052222SSHtest}{127.0.0.13306MYSQLguestadmin}{8.8.8.822SSHguestadmin}{9.9.9.96379REDISguestadmin}{108.61.223.1052222SSHguestadmin}{127.0.0.13306MYSQLguestadmin888}{8.8.8.822SSHguestadmin888}{9.9.9.96379REDISguestadmin888}{108.61.223.1052222SSHguestadmin888}{127.0.0.13306MYSQLguest123456}{8.8.8.822SSHguest123456}{9.9.9.96379REDISguest123456}{108.61.223.1052222SSHguest123456}{127.0.0.13306MYSQLguest}{8.8.8.822SSHguest}{9.9.9.96379REDISguest}{108.61.223.1052222SSHguest}{127.0.0.13306MYSQLinfoadmin}{8.8.8.822SSHinfoadmin}{9.9.9.96379REDISinfoadmin}{108.61.223.1052222SSHinfoadmin}{127.0.0.13306MYSQLinfoadmin888}{8.8.8.822SSHinfoadmin888}{9.9.9.96379REDISinfoadmin888}{108.61.223.1052222SSHinfoadmin888}{127.0.0.13306MYSQLinfo123456}{8.8.8.822SSHinfo123456}{9.9.9.96379REDISinfo123456}{108.61.223.1052222SSHinfo123456}{127.0.0.13306MYSQLinfo}{8.8.8.822SSHinfo}{9.9.9.96379REDISinfo}{108.61.223.1052222SSHinfo}{127.0.0.13306MYSQLadmadmin}{8.8.8.822SSHadmadmin}{9.9.9.96379REDISadmadmin}{108.61.223.1052222SSHadmadmin}{127.0.0.13306MYSQLadmadmin888}{8.8.8.822SSHadmadmin888}{9.9.9.96379REDISadmadmin888}{108.61.223.1052222SSHadmadmin888}{127.0.0.13306MYSQLadm123456}{8.8.8.822SSHadm123456}{9.9.9.96379REDISadm123456}{108.61.223.1052222SSHadm123456}{127.0.0.13306MYSQLadm}{8.8.8.822SSHadm}{9.9.9.96379REDISadm}{108.61.223.1052222SSHadm}{127.0.0.13306MYSQLmysqladmin}{8.8.8.822SSHmysqladmin}{9.9.9.96379REDISmysqladmin}{108.61.223.1052222SSHmysqladmin}{127.0.0.13306MYSQLmysqladmin888}{8.8.8.822SSHmysqladmin888}{9.9.9.96379REDISmysqladmin888}{108.61.223.1052222SSHmysqladmin888}{127.0.0.13306MYSQLmysql123456}{8.8.8.822SSHmysql123456}{9.9.9.96379REDISmysql123456}{108.61.223.1052222SSHmysql123456}{127.0.0.13306MYSQLmysql}{8.8.8.822SSHmysql}{9.9.9.96379REDISmysql}{108.61.223.1052222SSHmysql}{127.0.0.13306MYSQLuseradmin}{8.8.8.822SSHuseradmin}{9.9.9.96379REDISuseradmin}{108.61.223.1052222SSHuseradmin}{127.0.0.13306MYSQLuseradmin888}{8.8.8.822SSHuseradmin888}{9.9.9.96379REDISuseradmin888}{108.61.223.1052222SSHuseradmin888}{127.0.0.13306MYSQLuser123456}{8.8.8.822SSHuser123456}{9.9.9.96379REDISuser123456}{108.61.223.1052222SSHuser123456}{127.0.0.13306MYSQLuser}{8.8.8.822SSHuser}{9.9.9.96379REDISuser}{108.61.223.1052222SSHuser}{127.0.0.13306MYSQLadministratoradmin}{8.8.8.822SSHadministratoradmin}{9.9.9.96379REDISadministratoradmin}{108.61.223.1052222SSHadministratoradmin}{127.0.0.13306MYSQLadministratoradmin888}{8.8.8.822SSHadministratoradmin888}{9.9.9.96379REDISadministratoradmin888}{108.61.223.1052222SSHadministratoradmin888}{127.0.0.13306MYSQLadministrator123456}{8.8.8.822SSHadministrator123456}{9.9.9.96379REDISadministrator123456}{108.61.223.1052222SSHadministrator123456}{127.0.0.13306MYSQLadministrator}{8.8.8.822SSHadministrator}{9.9.9.96379REDISadministrator}{108.61.223.1052222SSHadministrator}{127.0.0.13306MYSQLftpadmin}{8.8.8.822SSHftpadmin}{9.9.9.96379REDISftpadmin}{108.61.223.1052222SSHftpadmin}{127.0.0.13306MYSQLftpadmin888}{8.8.8.822SSHftpadmin888}{9.9.9.96379REDISftpadmin888}{108.61.223.1052222SSHftpadmin888}{127.0.0.13306MYSQLftp123456}{8.8.8.822SSHftp123456}{9.9.9.96379REDISftp123456}{108.61.223.1052222SSHftp123456}{127.0.0.13306MYSQLftp}{8.8.8.822SSHftp}{9.9.9.96379REDISftp}{108.61.223.1052222SSHftp}{127.0.0.13306MYSQLsaadmin}{8.8.8.822SSHsaadmin}{9.9.9.96379REDISsaadmin}{108.61.223.1052222SSHsaadmin}{127.0.0.13306MYSQLsaadmin888}{8.8.8.822SSHsaadmin888}{9.9.9.96379REDISsaadmin888}{108.61.223.1052222SSHsaadmin888}{127.0.0.13306MYSQLsa123456}{8.8.8.822SSHsa123456}{9.9.9.96379REDISsa123456}{108.61.223.1052222SSHsa123456}{127.0.0.13306MYSQLsa}{8.8.8.822SSHsa}{9.9.9.96379REDISsa}{108.61.223.1052222SSHsa}]176===RUNTestDistributionTask[0000]INFOxseccrack:Starttoscan[0003]INFOxseccrack:Finshedscan,totalresult:0,usedtime:2562047h47m16.854775807s---PASS:TestDistributionTask(3.01s)PASSokcommand-line-arguments3.035s到此为止,我们的扫描器的核心部件已经造好了,接下来需要给扫描器上个高上大的命令行调用的外壳就大功造成了。
命令行模块命令行控制模块,我们单独定义了一个cmd包,依赖第三方包github.com/urfave/cli。
我们在cmd模块中定义了扫描和扫描结果导出为txt文件2个命令及一系统全局选项。
packagecmdimport("github.com/urfave/cli""x-crack/util""x-crack/models")varScan=cli.Command{Name:"scan",Usage:"starttocrackweakpassword",Description:"starttocrackweakpassword",Action:util.Scan,Flags:[]cli.Flag{boolFlag("debug,d","debugmode"),intFlag("timeout,t",5,"timeout"),intFlag("scan_num,n",5000,"threadnum"),stringFlag("ip_list,i","iplist.txt","iplist"),stringFlag("user_dict,u","user.dic","userdict"),stringFlag("pass_dict,p","pass.dic","passworddict"),},}varDump=cli.Command{Name:"dump",Usage:"dumpresulttoatextfile",Description:"dumpresulttoatextfile",Action:models.Dump,Flags:[]cli.Flag{stringFlag("outfile,o","x_crack.txt","scanresultfile"),},}funcstringFlag(name,value,usagestring)cli.StringFlag{returncli.StringFlag{Name:name,Value:value,Usage:usage,}}funcboolFlag(name,usagestring)cli.BoolFlag{returncli.BoolFlag{Name:name,Usage:usage,}}funcintFlag(namestring,valueint,usagestring)cli.IntFlag{returncli.IntFlag{Name:name,Value:value,Usage:usage,}}然后再回到x-crack/util包为我们的scancommand模块专门写一个Action,如下:
funcScan(ctx*cli.Context)(errerror){ifctx.IsSet("debug"){vars.DebugMode=ctx.Bool("debug")}ifvars.DebugMode{logger.Log.Level=logrus.DebugLevel}ifctx.IsSet("timeout"){vars.TimeOut=time.Duration(ctx.Int("timeout"))*time.Second}ifctx.IsSet("scan_num"){vars.ScanNum=ctx.Int("scan_num")}ifctx.IsSet("ip_list"){vars.IpList=ctx.String("ip_list")}ifctx.IsSet("user_dict"){vars.UserDict=ctx.String("user_dict")}ifctx.IsSet("pass_dict"){vars.PassDict=ctx.String("pass_dict")}ifctx.IsSet("outfile"){vars.ResultFile=ctx.String("outfile")}vars.StartTime=time.Now()userDict,uErr:=ReadUserDict(vars.UserDict)passDict,pErr:=ReadPasswordDict(vars.PassDict)ipList:=ReadIpList(vars.IpList)aliveIpList:=CheckAlive(ipList)ifuErr==nil&&pErr==nil{tasks,_:=GenerateTask(aliveIpList,userDict,passDict)DistributionTask(tasks)}returnerr}然后再到x-crack/models中为dump命令写一个Action,如下:
packagemodelsimport("github.com/patrickmn/go-cache""github.com/urfave/cli""x-crack/vars""x-crack/logger""x-crack/util/hash""encoding/gob""time""fmt""os""strings")funcinit(){gob.Register(Service{})gob.Register(ScanResult{})}funcSaveResult(errerror,resultScanResult){iferr==nil&&result.Result{varkstringprotocol:=strings.ToUpper(result.Service.Protocol)ifprotocol=="REDIS"||protocol=="FTP"{k=fmt.Sprintf("%v-%v-%v",result.Service.Ip,result.Service.Port,result.Service.Protocol)}else{k=fmt.Sprintf("%v-%v-%v",result.Service.Ip,result.Service.Port,result.Service.Username)}h:=hash.MakeTaskHash(k)hash.SetTaskHask(h)_,found:=vars.CacheService.Get(k)if!found{logger.Log.Infof("Ip:%v,Port:%v,Protocol:[%v],Username:%v,Password:%v",result.Service.Ip,result.Service.Port,result.Service.Protocol,result.Service.Username,result.Service.Password)}vars.CacheService.Set(k,result,cache.NoExpiration)}}funcSavaResultToFile()(error){returnvars.CacheService.SaveFile("x_crack.db")}funcCacheStatus()(countint,itemsmap[string]cache.Item){count=vars.CacheService.ItemCount()items=vars.CacheService.Items()returncount,items}funcResultTotal(){vars.ProgressBar.Finish()logger.Log.Info(fmt.Sprintf("Finshedscan,totalresult:%v,usedtime:%v",vars.CacheService.ItemCount(),time.Since(vars.StartTime)))}funcLoadResultFromFile(){vars.CacheService.LoadFile("x_crack.db")vars.ProgressBar.Finish()logger.Log.Info(fmt.Sprintf("Finshedscan,totalresult:%v",vars.CacheService.ItemCount()))}funcDump(ctx*cli.Context)(errerror){LoadResultFromFile()err=DumpToFile(vars.ResultFile)iferr!=nil{logger.Log.Fatalf("Dumpresulttofileerr,Err:%v",err)}returnerr}funcDumpToFile(filenamestring)(errerror){file,err:=os.Create(filename)iferr==nil{_,items:=CacheStatus()for_,v:=rangeitems{result:=v.Object.(ScanResult)file.WriteString(fmt.Sprintf("%v:%v|%v,%v:%v\n",result.Service.Ip,result.Service.Port,result.Service.Protocol,result.Service.Username,result.Service.Password))}}returnerr}最后给IP\port过滤与任务扫描模块加上一个骚气的进度条,我们的扫描器就算大功告成了。
x-crack/util/util.go的代码片段:
funcCheckAlive(ipList[]models.IpAddr)([]models.IpAddr){logger.Log.Infoln("checkingipactive")vars.ProcessBarActive=pb.StartNew(len(ipList))vars.ProcessBarActive.SetTemplate(`{{rndcolor"Checkingprogress:"}}{{percent."[%.02f%%]""[?]"|rndcolor}}{{counters."[%s/%s]""[%s/?]"|rndcolor}}{{bar."「""-"(rnd"ᗧ""◔""◕""◷")"•""」"|rndcolor}}{{rtime.|rndcolor}}`)....x-crack/util/task.go的代码片断:
funcDistributionTask(tasks[]models.Service)(){totalTask:=len(tasks)scanBatch:=totalTask/vars.ScanNumlogger.Log.Infoln("Starttoscan")vars.ProgressBar=pb.StartNew(totalTask)vars.ProgressBar.SetTemplate(`{{rndcolor"Scanningprogress:"}}{{percent."[%.02f%%]""[?]"|rndcolor}}{{counters."[%s/%s]""[%s/?]"|rndcolor}}{{bar."「""-"(rnd"ᗧ""◔""◕""◷")"•""」"|rndcolor}}{{rtime.|rndcolor}}`)...扫描器代码中还有些细节没有在教程中详细说,有兴趣的同学可以思考下以下问题,然后再结合代码看看老夫的实现方式:
扫到一个弱口令后,如何取消相同IP\port和用户名请求,避免扫描效率低下对于FTP匿名访问,如何只记录一个密码,而不是把所有用户名都记录下来对于Redis这种没有用户名的服务,如何只记录一次密码,而不是记录所有的所有用户及正常的密码的组合对于不支持设置超时的扫描插件,如何统一设置超时时间扫描器测试到现在为止,我们的扫描器已经大功告成了,可以编译出来运行一下看看效果了。以下脚本可一键同时编译出mac、linux和Windows平台的可执行文件(笔者的开发环境为MAC)
#!/bin/bashgobuildx-crack.gomvx-crackx-crack_darwin_amd64CGO_ENABLED=0GOOS=linuxGOARCH=amd64gobuildx-crack.gomvx-crackx-crack_linux_amd64CGO_ENABLED=0GOOS=windowsGOARCH=amd64gobuildx-crack.gomvx-crack.exex-crack_windows_amd64.exegobuildx-crack.go使用参数hartnettathartnettdeMacBook-Proin/data/code/golang/src/x-crack(master)$./x-crackNAME:x-crack-Weakpasswordscanner,Support:FTP/SSH/MSSQL/MYSQL/PostGreSQL/REDIS/ElasticSearch/MONGODBUSAGE:x-crack[globaloptions]command[commandoptions][arguments...]VERSION:20171227AUTHOR(S):netxfly<x@xsec.io>COMMANDS:scanstarttocrackweakpassworddumpdumpresulttoatextfilehelp,hShowsalistofcommandsorhelpforonecommandGLOBALOPTIONS:--debug,-ddebugmode--timeoutvalue,-tvaluetimeout(default:5)--scan_numvalue,-nvaluethreadnum(default:5000)--ip_listvalue,-ivalueiplist(default:"iplist.txt")--user_dictvalue,-uvalueuserdict(default:"user.dic")--pass_dictvalue,-pvaluepassworddict(default:"pass.dic")--outfilevalue,-ovaluescanresultfile(default:"x_crack.txt")--help,-hshowhelp--version,-vprinttheversion使用截图
评论