前言
这是以前编写的文章,肯定有一些错误和稚嫩(某些加粗的个人点评实属年轻气盛,若说的不对,劳烦大佬捉虫了)。
fscan是我用过的比较顺手的扫描器,究其根源大概是内部封装了大量具有实战意义的功能,以及用户友好的傻瓜式操作吧。
通过对fscan源码的分析,可以学习到很多东西,非常适合作为年轻人的第一款扫描器的学习。
当然,其中还有比较多的可以优化的点,后续我会针对应用场景的不同,进行代码的重构,与功能的添加。
概述
fscan是一个非常有名的扫描器,在内网方面很好用。
主要功能:
- 信息搜集:
- 存活探测(icmp)
- 端口扫描
- 爆破功能:
- 各类服务爆破(ssh、smb、rdp等)
- 数据库密码爆破(mysql、mssql、redis、psql、oracle等)
- 系统信息、漏洞扫描:
- netbios探测、域控识别
- 获取目标网卡信息
- 高危漏洞扫描(ms17010等)
- Web探测功能:
- webtitle探测
- web指纹识别(常见cms、oa框架等)
- web漏洞扫描(weblogic、st2等,支持xray的poc)
- 漏洞利用:
- redis写公钥或写计划任务
- ssh命令执行
- ms17017利用(植入shellcode),如添加用户等
- 其他功能:
- 文件保存
## 主要参数-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 跳过sql、ftp、ssh等的密码爆破 -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 fcgi、smb 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进行收发。
这里都是一次收发,说实在我没想到有什么区别,可以优化,可能是我太菜了。可能是调用接口的开销?
开放端口扫描
在这里有两种情况:
- 指定参数使用特定模块,进入
NoPortScan
,不进行端口存活探测 - 常见扫描开放端口
如指定了参数"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
函数,否则调用gowebtitle
、infocheck
函数,进行网页的访问和信息检查,最后根据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, " ", " ", -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请求,简单构建了下。
接下来调用filterpoc
和lib.CheckMultiPoc
。
filterpoc做了两件事:
- 若没指定pocname,则使用embed读入的文件夹内所有poc
- 若指定了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表达式有三个过程:
- 构建cel环境,初始化cel.Env
- 向cel环境中注入类型、方法。
-
计算表达式。
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
改用NewVar
或NewConst
。
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.Search
和headers、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
.........
后面的暂时没有精力写了,前面我相信已经量大管饱了。
参考
-
-
-
-
-
-
-