泛星安全团队第16篇文章
文章内容为学习记录,请勿利用文章内的相关技术从事非法测试,如因此产生的一切不良后果与文章作者和本公众号无关。
0x00 开发背景
这个工具开发想法最早是去年国hvv,客户那边云上资产linux服务器比较多,d盾可以监控windows上的文件变化但是没有linux版本,github上也有一些用python开发的文件监控工具,但是每次跑到服务器上看觉得有点麻烦,后面想法就是写个多平台带消息推送的文件监控工具。
在去年的7月份用python开发了一个版本,效果如下:
最近一个客户那边做hvv,有老6非攻击时间半夜搞事情
通过一个OA的nday打进内网,后端值守监控到frp的流量看到群消息同事立马上服务器看D盾文件监控日志,态势感知对流量分析监控还是有一定时间过程,如果没有态势感知完全不知道这老6干了什么,那么这个时候如果我们有个可以实时监控服务器文件指定后缀创建修改这些时间的bot,就可以及时响应处理这种事件,最近又电脑坏了之前python写的版本都在老的电脑上,没有备份到网盘这些,决定用go重写一个。
0x01 设计思路
工具功能设计的大概思路
0x02 功能实现
这里只讲功能用go实现核心代码,目前工具的代码还没全部合到一块,需要使用的话可以使用之前的python版本的,实现了上面的大部分功能模块(bug比较多),python版本太久没更新了也一直没有用上。
命令行参数
命令行参数功能实现我使用的cobra,使用cobra生成命令,在我们的项目文件里面会自动生成一个cmd目录
最初始的是生成一个root文件,我这里用cobra-cli生成了version和conf命令行。
/*
Copyright © 2022 [email protected]
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var EnableCommandSorting = false
// confCmd represents the init command
var confCmd = &cobra.Command{
Use: "conf",
Long: "Profile initialization",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("conf called")
},
}
func init() {
rootCmd.AddCommand(confCmd)
confCmd.Flags().String("mu", "", "--mu [email protected]")
confCmd.Flags().String("mp", "", "--mp admin@#!123")
confCmd.Flags().String("dp", "", "--dp http://test.com")
confCmd.Flags().String("wp", "", "--wp http://test.com")
confCmd.MarkFlagRequired("mu")
confCmd.MarkFlagRequired("mp")
confCmd.MarkFlagRequired("dp")
confCmd.MarkFlagRequired("wp")
}
version
/*
Copyright © 2022 [email protected]
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Sentry V0.1")
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
在root里面定义test功能参数,这个主要测试推送配置是否正常。
func init() {
rootCmd.AddCommand(test)
}
test功能代码
var test = &cobra.Command{
Use: "test",
Long: "Message push test",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("test...") // 后面换成我们的功能代码
},
}
其它参数在root.go
func init() {
rootCmd.AddCommand(test)
// 命令参数
rootCmd.Flags().StringP("dir", "d", "", "--dir c:/test")
rootCmd.Flags().StringP("excludedir", "e", "", "--excludedir c:/test")
rootCmd.Flags().StringP("config", "c", "conf.ini", "--config conf.ini")
}
效果:
基本的命令行就构建完成了,当然这里不是最终的效果,后面优化帮助信息还有使用案例输出效果,cobra这个输出有点丑了,可以用cobra自定义模板信息做个美化
文件变化监控功能
使用的是fsnotify库
项目地址:https://github.com/fsnotify/fsnotify
package main
import (
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
err = watcher.Add("c:/")
if err != nil {
log.Fatal(err)
}
<-done
}
基础代码、文件监控逻辑一些细节还得通过官方demo去修改,提取我们需要的信息,比如说指定的后缀修改告警
可执行文件检测
这个功能的想法是监控一些windows机器权限比较高的目录和web目录,对exe可执行文件上传到微步沙箱,微步给了每个月1000条限额完全够了
微步在线api文档:https://x.threatbook.cn/v5/apiDocs
主要使用下面两个api接口,微步提供了go代码模板封装到函数里面加一个处理结果函数返回我们需要的结果就可以了。
-
https://x.threatbook.cn/v5/apiDocs
-
https://x.threatbook.cn/v5/apiDocs
webshell在线检测
这里我用了两个在线检测的平台,百度webdir+和河马在线web检测。
百度webdir+
百度webdir+是有api接口的,可以直接调用。
// 百度webdir+ 扫描
func WebDirScan(scanfile string) [3]string {
url := "https://scanner.baidu.com/enqueue"
r, w := io.Pipe()
m := multipart.NewWriter(w)
go func() {
defer w.Close()
defer m.Close()
part, err := m.CreateFormFile("archive", "scanfile.zip")
if err != nil {
return
}
file, err := os.Open(scanfile)
if err != nil {
return
}
defer file.Close()
if _, err = io.Copy(part, file); err != nil {
return
}
}()
a, _ := http.Post(url, m.FormDataContentType(), r)
rspBody, _ := ioutil.ReadAll(a.Body)
result := viper.New()
result.SetConfigType("json")
result.ReadConfig(bytes.NewBuffer(rspBody))
resulturl := result.GetString("url")
scanresult := bdresult(resulturl)
// 输出结果
return scanresult
}
WebDirScan函数传参文件进来之后上传文件获取到文件扫描结果url传给bdresult函数返回扫描结果,扫描结果里面包含文件名、md5、威胁类型。
func bdresult(url string) [3]string {
client := &http.Client{}
req, _ := http.NewRequest("GET", url, nil)
// 请求头设置 设置长连接
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36")
req.Header.Set("Connection", "keep-alive")
resetcheck:
// 开始请求
resp, err := client.Do(req)
if err != nil {
fmt.Println("Http get err:", err)
}
//if resp.StatusCode != 200 {
// fmt.Println("status code:", err)
//}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var scanresult [3]string
// 扫描结果判断扫描是否完成 未完成sleep 3s goto重新请求结果再次判断
if checkstatus := strings.Contains(string(body), "pending"); checkstatus {
time.Sleep(3 * time.Second)
goto resetcheck
} else {
// 临时调试输出 后面统一换成log输出到文件
fmt.Printf("百度webdir+扫描完成")
}
result := viper.New()
result.SetConfigType("json")
html := string(body)
html = html[1 : len(html)-2]
result.ReadConfig(bytes.NewBuffer([]byte(html)))
if err := result.ReadConfig(bytes.NewBuffer([]byte(html))); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("找不到配置文件..")
} else {
fmt.Println("配置文件出错..")
}
}
scanresult[1] = result.GetString(`md5`)
//fmt.Printf(result.GetString(`data.descr`))
//fmt.Printf(result.GetString(`md5`))
re_json := strings.Split(html, "],")
re_json1 := re_json[0]
results := re_json1[9:len(re_json1)]
// path descr
result.ReadConfig(bytes.NewBuffer([]byte(results)))
if err := result.ReadConfig(bytes.NewBuffer([]byte(results))); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("找不到配置文件..")
} else {
fmt.Println("配置文件出错..")
}
}
scanresult[0] = result.GetString(`path`)
scanresult[2] = result.GetString(`descr`)
return scanresult
}
测试效果
河马webshell
河马官方没有提供api接口,我们在浏览器看下请求包分析下,写个简单的爬虫处理扫描结果。
文件上传后自动跳转到了https://n.shellpub.com/detail/59f0fea5-ca5a-42cb-b3dd-c265441b561c,根据上面的返回结果可以知道结果detail后面拼接的是返回的tid
页面返回结果里面有一串json就是我们需要的结果内容
代码实现如下
func Hmscan(scanfile string) []string {
// 上传到河马webshell在线检测
url := "https://n.shellpub.com/api/v1/zip"
r, w := io.Pipe()
m := multipart.NewWriter(w)
go func() {
defer w.Close()
defer m.Close()
part, err := m.CreateFormFile("userfile", "scanfile.zip")
if err != nil {
return
}
file, err := os.Open(scanfile)
if err != nil {
return
}
defer file.Close()
if _, err = io.Copy(part, file); err != nil {
return
}
}()
a, _ := http.Post(url, m.FormDataContentType(), r)
rspBody, _ := ioutil.ReadAll(a.Body)
result := viper.New()
// 设置配置文件类型为json 这里很重要 不是指的话会读取不到 踩坑踩坑
result.SetConfigType("json")
result.ReadConfig(bytes.NewBuffer(rspBody))
if err := result.ReadConfig(bytes.NewBuffer(rspBody)); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("找不到配置文件..")
} else {
fmt.Println("配置文件出错..")
}
}
// 结果url拼接
result_url := "https://n.shellpub.com/detail/" + result.GetString(`data.tid`)
scanresult := hmresult(result_url)
return scanresult
}
拼接得到结果url处理返回页面的json信息
func hmresult(url string) []string {
client := &http.Client{}
req, _ := http.NewRequest("GET", url, nil)
// 请求头设置 设置长连接
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36")
req.Header.Set("Connection", "keep-alive")
resetcheck:
// 开始请求
resp, err := client.Do(req)
if err != nil {
fmt.Println("Http get err:", err)
}
//if resp.StatusCode != 200 {
// fmt.Println("status code:", err)
//}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if checkstatus := strings.Contains(string(body), "服务目前不可用"); checkstatus {
time.Sleep(3 * time.Second)
goto resetcheck
} else {
fmt.Printf("河马webshell扫描完成")
}
//fmt.Printf(string(body))
html := strings.Replace(string(body), "n", "", -1)
re_json := regexp.MustCompile(`"results":[(.*?)]`)
result_json := re_json.FindString(html)
result_json = result_json[11 : len(result_json)-1]
result := viper.New()
// 设置配置文件类型为json 这里很重要 不是指的话会读取不到 踩坑踩坑
result.SetConfigType("json")
result.ReadConfig(bytes.NewBuffer([]byte(result_json)))
if err := result.ReadConfig(bytes.NewBuffer([]byte(result_json))); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("找不到配置文件..")
} else {
fmt.Println("配置文件出错..")
}
}
// 返回字符数组
var scanresult []string
scanresult = append(scanresult, result.GetString(`filename`))
scanresult = append(scanresult, result.GetString(`md5`))
scanresult = append(scanresult, result.GetString(`description`))
return scanresult
}
测试效果
消息推送
钉钉推送
这里用的是github上面的一个库
项目地址:https://github.com/blinkbean/dingtalk
直接调用就可以了,基础推送代码
package main
import (
"fmt"
"github.com/blinkbean/dingtalk"
"os"
)
func main() {
// 单个机器人有单位时间内消息条数的限制,如果有需要可以初始化多个token,发消息时随机发给其中一个机器人。
var dingToken = "*****************************"
cli := dingtalk.InitDingTalkWithSecret(dingToken, "*****************************")
//var testphone = "18888888888"
//cli.SendTextMessage("test", dingtalk.WithAtMobiles([]string{"18888888888"}))
//cli.SendLinkMessage("test", "test", "", "https://www.baidu.com")
msg := []string{
"# Sentry 文件监控提醒",
"---",
"事件信息:<font color=#ff2a00 size=20>敏感文件后缀创建</font>n",
"服务器主机名:testn",
"服务器IP:127.0.0.1n",
"服务器MAC:n",
"百度Webdir+检测结果:<font color=#ff2a00 size=20>危险</font>n",
}
cli.SendMarkDownMessageBySlice("Markdown title", msg, dingtalk.WithAtAll())
fmt.Println(os.Hostname())
}
测试效果
日志记录
自定义Logger输出格式
package monitor
import (
"fmt"
"io"
"log"
"os"
)
var (
Trace *log.Logger
Info *log.Logger
Warning *log.Logger
Error *log.Logger
)
func init() {
dir, _ := os.Getwd()
fmt.Println(dir)
f, err := os.OpenFile(dir+"\logs\"+"test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln(err)
}
Trace = log.New(io.MultiWriter(f, os.Stdout), "[TRACE] ", log.Ldate|log.Ltime)
Info = log.New(io.MultiWriter(f, os.Stdout), "[INFO] ", log.Ldate|log.Ltime)
Warning = log.New(io.MultiWriter(f, os.Stdout), "[WARNING] ", log.Ldate|log.Ltime)
Error = log.New(io.MultiWriter(f, os.Stdout), "[ERROR] ", log.Ldate|log.Ltime)
}
后面所有需要日志输出的地方都通过这个包去输出,统一日志输出格式
0x03 结语
一个简单的带推送的文件监控工具,在一些特定的场景下可能有用,也有可能用处不大哈哈!算是go工具开发的小尝试,大佬们勿喷!
往期回顾
·END·
原文始发于微信公众号(泛星安全团队):工具开发 | Go实现带推送的文件监控工具
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论