这里再补习一下go的线程池的实现
这个例子很清晰了,可以看到work是用来处理输入信号和输出信号的地方,可以用for循环的方式启动3个worker,每个 worker 都在一个独立的 goroutine 中运行,这实际上就是启动了 3 个并发线程。
然后就是一个循环结构来发送信号(任务),往信道里发,goroutine中启动的worker会自动处理,然后再启动一个循环结构接收返回值
package main
import "fmt"
import "time"
// 这里是 worker,我们将并发执行多个 worker。
// worker 将从 `jobs` 通道接收任务,并且通过 `results` 发送对应的结果。
// 我们将让每个任务间隔 1s 来模仿一个耗时的任务。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
// 为了使用 worker 线程池并且收集他们的结果,我们需要 2 个通道。
jobs := make(chan int, 100)
results := make(chan int, 100)
// 这里启动了 3 个 worker,初始是阻塞的,因为还没有传递任务。
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 这里我们发送 9 个 `jobs`,然后 `close` 这些通道
// 来表示这些就是所有的任务了。
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 最后,我们收集所有这些任务的返回值。
for a := 1; a <= 9; a++ {
<-results
}
}
机制实现
再来看第一个问题
scanner.go最后这里动态调用机制的实现,包括线程锁,信号量啥的,看看怎么实现的
下面的ch和wg早已定义
var ch = make(chan struct{}, common.Threads)
var wg = sync.WaitGroup{}
动态调用机制:
- 使用 reflect 包动态调用函数。
-
代码中的 ScanFunc 函数演示了如何通过反射动态调用不同的扫描插件。
func ScanFunc(name string, info common.HostInfo) {
defer func() { if err := recover(); err != nil { fmt.Printf("[-] %v:%v scan error: %v\n", info.Host, info.Ports, err) } }() f := reflect.ValueOf(PluginList[*name]) in := []reflect.Value{reflect.ValueOf(info)} f.Call(in)
}
ScanFunc 函数使用 reflect 包的 ValueOf 方法获取 PluginList 中对应名称的函数。
reflect.ValueOf 获取到函数的反射值后,通过 f.Call 动态调用该函数。
defer 和 recover 用于捕获并处理运行时错误,防止程序崩溃。
线程锁(Mutex):
- sync.Mutex 用于保护对共享资源(如 common.Num 和 common.End)的访问,确保线程安全。
-
在 AddScan 函数中,Mutex.Lock() 和 Mutex.Unlock() 确保在对这些共享资源进行读写操作时不会发生竞争条件。
var Mutex = &sync.Mutex{}
func AddScan(scantype string, info common.HostInfo, ch chan struct{}, wg sync.WaitGroup) {
*ch <- struct{}{} wg.Add(1) go func() { Mutex.Lock() common.Num += 1 Mutex.Unlock() ScanFunc(&scantype, &info) Mutex.Lock() common.End += 1 Mutex.Unlock() wg.Done() <-*ch }()
}
Mutex 是一个全局的互斥锁,用于保护对共享资源的访问。
在 AddScan 中,Mutex.Lock() 和 Mutex.Unlock() 确保对 common.Num 和 common.End 的增减操作是线程安全的。
信号量(Channel):
- ch 通道用于限制并发执行的扫描任务的数量。
- 每当启动一个新的扫描任务时,AddScan 函数会向 ch 发送一个空结构体,表示有一个新的任务在进行中。
-
扫描任务完成后,会从 ch 中接收一个值,表示一个任务已经完成。
func AddScan(scantype string, info common.HostInfo, ch chan struct{}, wg sync.WaitGroup) {
*ch <- struct{}{} wg.Add(1) go func() { // 执行扫描任务 wg.Done() <-*ch }()
}
ch 是一个通道,用于控制并发任务的数量。每个任务启动时,都会向 ch 发送一个信号,表示有一个新的任务在进行中。
当任务完成时,会从 ch 中接收一个信号,表示任务已经完成。
这可以用于限制同时运行的任务数量,避免系统过载。
web扫描
再来看第二个问题
重点看一下对web的扫描,debug一下看看流程,看看怎么去匹配指纹(icohash咋算),怎么解析、调用poc,怎么抓取和分析web
调用的条件有三个:
调用ScanFunc
这里10003对应的:
"web": 1000003,
"webonly": 1000003,
"webpoc": 1000003,
但是这个call一直调试不进去
f := reflect.ValueOf(PluginList[*name])
in := []reflect.Value{reflect.ValueOf(info)}
f.Call(in)
不太懂go的反射,这是一个简单的demo
package main
import (
"fmt"
"reflect"
)
func Hello(name string) {
fmt.Println("Hello,", name)
}
func main() {
f := reflect.ValueOf(Hello)
params := []reflect.Value{reflect.ValueOf("World")}
f.Call(params) // 调用 Hello("World")
}
这里又补习了go导包的一个知识
我新定义了一个hello包,他放在根目录貌似不行,根目录的go文件都是Main包的,需要新建一个目录
go.mod是这样的
导包是这样的
我这里写的demo也是调不进去,可能是因为我的go版本有点老?但是应该可以根据调用的模块名和函数名去打断点吧
打了一些断点,执行顺序大概是这几个文件
来看下执行的流程
我用vulhub本地搭了一个tp5的漏洞,看看他是怎么执行,怎么调yaml的
先跑一下
然后来debug
配置如下
这里不知道它动态调用怎么调的,调试不进去,就姑且一切从这个webtitle函数开始看
这里没有指定m参数,Scantype默认是all,进入WebTitle函数
规范数据
进入geturl函数,这里flag默认为1
进入函数以后看注释有对flag的解释
//flag 1 first try
//flag 2 /favicon.ico
//flag 3 302
//flag 4 400 -> https
这里我们是第一次请求,这一块应该就是go进行http请求的方式,设置好req发出去,他这里都是写死的了
req, err := http.NewRequest("GET", Url, nil)
if err != nil {
return err, "", CheckData
}
req.Header.Set("User-agent", common.UserAgent)
req.Header.Set("Accept", common.Accept)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if common.Cookie != "" {
req.Header.Set("Cookie", common.Cookie)
}
//if common.Pocinfo.Cookie != "" {
// req.Header.Set("Cookie", "rememberMe=1;"+common.Pocinfo.Cookie)
//} else {
// req.Header.Set("Cookie", "rememberMe=1")
//}
req.Header.Set("Connection", "close")
var client *http.Client
if flag == 1 {
client = lib.ClientNoRedirect
} else {
client = lib.Client
}
resp, err := client.Do(req)
后面对接收到的响应包进行处理,进入getRespBody函数
func getRespBody(oResp *http.Response) ([]byte, error) {
var body []byte
if oResp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(oResp.Body)
if err != nil {
return nil, err
}
defer gr.Close()
for {
buf := make([]byte, 1024)
n, err := gr.Read(buf)
if err != nil && err != io.EOF {
return nil, err
}
if n == 0 {
break
}
body = append(body, buf...)
}
} else {
raw, err := io.ReadAll(oResp.Body)
if err != nil {
return nil, err
}
body = raw
}
return body, nil
}
读取 HTTP 响应体,并处理可能的 gzip 压缩,没有压缩就直接返回
body如下
把body塞进checkdata
flag!=2,第一次请求,先检查编码保证解析,gettitle提取标题,读取 Content-Length 字段拿到长度
resp.Location() 获取响应中的重定向 URL(如果存在)。如果获取成功(err1 为 nil),将重定向 URL 转换为字符串并赋值给 reurl。
使用 fmt.Sprintf 格式化一个结果字符串,包含网页 URL、HTTP 状态码、内容长度和网页标题。如果 reurl 不为空(表示存在重定向 URL),将其添加到结果字符串中。
最后如果有跳转的话就返回跳转的url,没有就返回空,此外返回checkdata
然后我们就又回到了GOWebTitle这个函数,可见这个函数并不只是拿title
有跳转的话就再次进入我们刚出来的geturl方法,flag为3,这里访问图标(flag为2)被注释了,我们在调回去看一眼
//有跳转
if strings.Contains(result, "://") {
info.Url = result
err, result, CheckData = geturl(info, 3, CheckData)
if err != nil {
return
}
}
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
err, result, CheckData = geturl(info, 1, CheckData)
//有跳转
if strings.Contains(result, "://") {
info.Url = result
err, _, CheckData = geturl(info, 3, CheckData)
if err != nil {
return
}
}
}
//是否访问图标
//err, _, CheckData = geturl(info, 2, CheckData)
if err != nil {
return
}
return
}
代码就一小段,计算ico hash不在这里?
if flag == 2 {
URL, err := url.Parse(Url)
if err == nil {
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
} else {
Url += "/favicon.ico"
}
}
至此我们又回到刚一开始的函数
进入WebScan.InfoCheck
进行指纹匹配
for _, data := range *CheckData {
for _, rule := range info.RuleDatas {
if rule.Type == "code" {
matched, _ = regexp.MatchString(rule.Rule, string(data.Body))
} else {
matched, _ = regexp.MatchString(rule.Rule, data.Headers)
}
if matched == true {
infoname = append(infoname, rule.Name)
}
}
//flag, name := CalcMd5(data.Body)
//if flag == true {
// infoname = append(infoname, name)
//}
}
不过感觉他这里有点迷,tp都认不出来,不过又回去看了下靶机确实是啥也没有,不过我这里改了下他就认识了
试了下命中的是这一条规则,这里我的body里有ThinkPHP
{"ThinkPHP", "headers", "(ThinkPHP)"},
我自己加了条规则改了下代码就能匹配上了,看了下fscan匹配body的指纹才三条,不知道是不是bug,感觉我这么改才是符合逻辑的?
{"ThinkPHPtest", "body", "(ThinkPHP)"},
接下来进入removeDuplicateElement函数去重一下指纹名称
func removeDuplicateElement(languages []string) []string {
result := make([]string, 0, len(languages))
temp := map[string]struct{}{}
for _, item := range languages {
if _, ok := temp[item]; !ok {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}
至此又回到WebTitle函数,我们没有指定NoPoc,所以在进入Poc扫描,也就是WebScan.WebScan函数
传入的Info如下
先初始化poc,然后对数据进行处理,后面for循环读取之前拿到的指纹,给pocinfo.PocName赋值,执行Execute(pocinfo),我们一个个来看
func WebScan(info *common.HostInfo) {
once.Do(initpoc)
var pocinfo = common.Pocinfo
buf := strings.Split(info.Url, "/")
pocinfo.Target = strings.Join(buf[:3], "/")
if pocinfo.PocName != "" {
Execute(pocinfo)
} else {
for _, infostr := range info.Infostr {
pocinfo.PocName = lib.CheckInfoPoc(infostr)
Execute(pocinfo)
}
}
}
once.Do(initpoc) 是 Go 语言中用于实现单次初始化操作的一个常见模式。这是通过 sync.Once 类型来实现的。sync.Once 确保某个操作在整个程序运行期间只执行一次,即使有多个 goroutine 试图执行这个操作。
先来看看是怎么初始化poc的
func initpoc() {
if common.PocPath == "" {
entries, err := Pocs.ReadDir("pocs")
if err != nil {
fmt.Printf("[-] init poc error: %v", err)
return
}
for _, one := range entries {
path := one.Name()
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
if poc, _ := lib.LoadPoc(path, Pocs); poc != nil {
AllPocs = append(AllPocs, poc)
}
}
}
} else {
fmt.Println("[+] load poc from " + common.PocPath)
err := filepath.Walk(common.PocPath,
func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return err
}
if !info.IsDir() {
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
poc, _ := lib.LoadPocbyPath(path)
if poc != nil {
AllPocs = append(AllPocs, poc)
}
}
}
return nil
})
if err != nil {
fmt.Printf("[-] init poc error: %v", err)
}
}
}
首先if判断,没指定poc就用默认的那个poc文件夹的
Pocs.ReadDir这应该也是go的一个解析yaml的库,先读进内存
看一下lib.LoadPoc
加载一个 POC 文件,并将其解析成 Poc 结构体,返回p,err
解析出来就是这样的
把他们放进AllPocs
这里如果是指定了自定义的poc就直接执行,否则就进入下面的for循环一个个匹配
if pocinfo.PocName != "" {
Execute(pocinfo)
} else {
for _, infostr := range info.Infostr {
pocinfo.PocName = lib.CheckInfoPoc(infostr)
Execute(pocinfo)
}
}
来看一下CheckInfoPoc,它传入的是匹配到的指纹的名字
strings.Contains是检查后面的在不在前面的字符串里,所以我新加的thinkphptest被匹配到了
func CheckInfoPoc(infostr string) string {
for _, poc := range info.PocDatas {
if strings.Contains(infostr, poc.Name) {
return poc.Alias
}
}
return ""
}
接下来Execute(pocinfo)
func Execute(PocInfo common.PocInfo) {
req, err := http.NewRequest("GET", PocInfo.Target, nil)
if err != nil {
errlog := fmt.Sprintf("[-] webpocinit %v %v", PocInfo.Target, err)
common.LogError(errlog)
return
}
req.Header.Set("User-agent", common.UserAgent)
req.Header.Set("Accept", common.Accept)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if common.Cookie != "" {
req.Header.Set("Cookie", common.Cookie)
}
pocs := filterPoc(PocInfo.PocName)
lib.CheckMultiPoc(req, pocs, common.PocNum)
}
common.PocInfo 是一个结构体,包以目标 URL和POC 名称
常规的创建req,设置请求头,然后 filterPoc和CheckMultiPoc
先来看前者
func filterPoc(pocname string) (pocs []*lib.Poc) {
if pocname == "" {
return AllPocs
}
for _, poc := range AllPocs {
if strings.Contains(poc.Name, pocname) {
pocs = append(pocs, poc)
}
}
return
}
其实就是查找包含了这个名称的poc返回一个列表
再来看后者
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
tasks := make(chan Task)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
go func() {
for task := range tasks {
isVul, _, name := executePoc(task.Req, task.Poc)
if isVul {
result := fmt.Sprintf("[+] PocScan %s %s %s", task.Req.URL, task.Poc.Name, name)
common.LogSuccess(result)
}
wg.Done()
}
}()
}
for _, poc := range pocs {
task := Task{
Req: req,
Poc: poc,
}
wg.Add(1)
tasks <- task
}
wg.Wait()
close(tasks)
}
主要还是俩部分,一个是创建一堆worker线程等着执行poc(从通道tasks里读取),一个是循环给通道传入poc,传一个就add(1),而执行poc的那边结束一个就执行一个done
最后结束
那么这里还剩我们要看的应该是最后一个函数executePoc
真的是老长了,其实无非就是根据poc里面写好的东西去发包而已
不过感觉这块函数对一个扫描器来说是非常核心的,这块函数的功能决定poc可以怎么写,有多灵活的功能等,如果自己开发扫描器这里是一个很重点的地方
func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
c := NewEnvOption()
c.UpdateCompileOptions(p.Set)
if len(p.Sets) > 0 {
var setMap StrMap
for _, item := range p.Sets {
if len(item.Value) > 0 {
setMap = append(setMap, StrItem{item.Key, item.Value[0]})
} else {
setMap = append(setMap, StrItem{item.Key, ""})
}
}
c.UpdateCompileOptions(setMap)
}
env, err := NewEnv(&c)
if err != nil {
fmt.Printf("[-] %s environment creation error: %s\n", p.Name, err)
return false, err, ""
}
req, err := ParseRequest(oReq)
if err != nil {
fmt.Printf("[-] %s ParseRequest error: %s\n", p.Name, err)
return false, err, ""
}
variableMap := make(map[string]interface{})
defer func() { variableMap = nil }()
variableMap["request"] = req
for _, item := range p.Set {
k, expression := item.Key, item.Value
if expression == "newReverse()" {
if !common.DnsLog {
return false, nil, ""
}
variableMap[k] = newReverse()
continue
}
err, _ = evalset(env, variableMap, k, expression)
if err != nil {
fmt.Printf("[-] %s evalset error: %v\n", p.Name, err)
}
}
success := false
//爆破模式,比如tomcat弱口令
if len(p.Sets) > 0 {
success, err = clusterpoc(oReq, p, variableMap, req, env)
return success, nil, ""
}
DealWithRule := func(rule Rules) (bool, error) {
Headers := cloneMap(rule.Headers)
var (
flag, ok bool
)
for k1, v1 := range variableMap {
_, isMap := v1.(map[string]string)
if isMap {
continue
}
value := fmt.Sprintf("%v", v1)
for k2, v2 := range Headers {
if !strings.Contains(v2, "{{"+k1+"}}") {
continue
}
Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
}
rule.Path = strings.ReplaceAll(rule.Path, "{{"+k1+"}}", value)
rule.Body = strings.ReplaceAll(rule.Body, "{{"+k1+"}}", value)
}
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
} else {
req.Url.Path = rule.Path
}
// 某些poc没有区分path和query,需要处理
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
//req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))), strings.NewReader(rule.Body))
if err != nil {
//fmt.Println("[-] newRequest error: ",err)
return false, err
}
newRequest.Header = oReq.Header.Clone()
for k, v := range Headers {
newRequest.Header.Set(k, v)
}
Headers = nil
resp, err := DoRequest(newRequest, rule.FollowRedirects)
newRequest = nil
if err != nil {
return false, err
}
variableMap["response"] = resp
// 先判断响应页面是否匹配search规则
if rule.Search != "" {
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
if len(result) > 0 { // 正则匹配成功
for k, v := range result {
variableMap[k] = v
}
} else {
return false, nil
}
}
out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
return false, err
}
//如果false不继续执行后续rule
// 如果最后一步执行失败,就算前面成功了最终依旧是失败
flag, ok = out.Value().(bool)
if !ok {
flag = false
}
return flag, nil
}
DealWithRules := func(rules []Rules) bool {
successFlag := false
for _, rule := range rules {
flag, err := DealWithRule(rule)
if err != nil || !flag { //如果false不继续执行后续rule
successFlag = false // 如果其中一步为flag,则直接break
break
}
successFlag = true
}
return successFlag
}
if len(p.Rules) > 0 {
success = DealWithRules(p.Rules)
} else {
for _, item := range p.Groups {
name, rules := item.Key, item.Value
success = DealWithRules(rules)
if success {
return success, nil, name
}
}
}
return success, nil, ""
}
最后又回到了WebTitle,差不多到这里对web的扫描就结束了
插件实现
大致看一下各种插件的实现
wmiexec
github.com/C-Sto/goWMIExec/pkg/wmiexec这是第三方包,用于执行 WMI 命令。
核心代码如下
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
execer := wmiexec.NewExecer(cfgIn)
err = execer.RPCConnect()
然后就是考虑怎么进行判断
暂时不看了
代理实现
代理怎么实现?
我本地搞了个正向代理,模拟机还是那个tp的洞,debug一下看看代理怎么走的
先跑一下证明没问题
调试了一下,调用顺序应该是这样的
scanner.go刚开始的时候会进行初始化http,进去看看
net.Dialer 是 Go 语言标准库 net 包中的一个结构体,用于创建网络连接。它允许你配置和定制网络连接的各种参数,比如连接超时、保持连接活动等。
package main
import (
"context"
"fmt"
"net"
"time"
)
func main() {
dialer := &net.Dialer{
Timeout: 5 * time.Second, // 设置连接超时时间为 5 秒
KeepAlive: 30 * time.Second, // 设置 Keep-Alive 时间为 30 秒
}
conn, err := dialer.Dial("tcp", "example.com:80")
if err != nil {
fmt.Println("Error:", err)
return
}
defer conn.Close()
fmt.Println("Connected to example.com:80")
}
定位到InitHttpClient函数
可以看到我们传的socks5参数是在这里被解析的
跟进去看一下common.Socks5Dailer函数
func Socks5Dailer(forward *net.Dialer) (proxy.Dialer, error) {
u, err := url.Parse(Socks5Proxy)
if err != nil {
return nil, err
}
if strings.ToLower(u.Scheme) != "socks5" {
return nil, errors.New("Only support socks5")
}
address := u.Host
var auth proxy.Auth
var dailer proxy.Dialer
if u.User.String() != "" {
auth = proxy.Auth{}
auth.User = u.User.Username()
password, _ := u.User.Password()
auth.Password = password
dailer, err = proxy.SOCKS5("tcp", address, &auth, forward)
} else {
dailer, err = proxy.SOCKS5("tcp", address, nil, forward)
}
if err != nil {
return nil, err
}
return dailer, nil
}
用于创建并返回一个 SOCKS5 代理的拨号器(proxy.Dialer)
代码很简单,最后想执行的无非是这一句dailer, err = proxy.SOCKS5("tcp", address, nil, forward)
它调用了net包的sokcs5方法,并且fscan只支持socks5,这里也是一个可以优化的点?
这里其实都是提前设置好client(Dialer),然后等发包的时候执行client.do()
其实我不是很了解go网络编程的细节,主要还是对net包的了解,后面都得去了解