Go渗透测试笔记(三)—HTTP客户端与工具的远程交互 0x00 Go的HTTP基础知识
HTTP是一种无状态的协议,服务器不会维护每个请求的状态,而是通过多种方式跟踪其状态,这些方式可能包括:会话标识符,cookie,HTTP标头等
。客户端和服务器有责任正确协商和验证状态
其次,客户端和服务器之间的通信可以一部或者同步进行,但他们需要以请求/响应
的方式循环运行。可以在请求头中添加几个选项和表标头,以影响服务器的行为并创建可用的Web应用程序。最常见的是服务器托管Web浏览器渲染的文件,以生成数据的图形化,组织化和时尚化的表示形式。API通常使用XML,JSON,MSGRPC
进行通信,某些情况下,可能检索到的是二进制格式,表示下载任意文件类型
0X01 调用HTTP API 1. 调用HTTP方法 包使用net/http
这些函数的使用格式如下
1 2 3 Get(url string )(resp *Response,err error ) Head(url string )(resp * Response,err error ) Post(url string ,bodyType string ,body io.Reader)(resp *Response.err error )
每个函数都将URL字符串作为参数并将其用作请求的目的地。Post函数要比较复杂一些,Post()具有两个附加参数(bodyType 和io.Reader),其中 bodyType()用于接受正文的Content-Type
,HTTP标头,(通常为 application/x-www-form-urlencoded)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "log" "net/http" "net/url" "strings" ) func main () { r1,err := http.Get("http://www.baidu.com" ) if err !=nil { log.Fatalln("无法调用" ) } defer r1.Body.Close() r2,err := http.Head("http://www.baidu.com" ) defer r2.Body.Close() form := url.Values{} form.Add("foo" ,"bar" ) r3,err := http.Post( "http://www.goole.com" , "application/x-www-form-urlencode" , strings.NewReader(form.Encode()), ) defer r3.Body.Close() }
当然,Go又一个函数PostForm()
可以代替
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "log" "net/http" "net/url" ) func main () { r1,err := http.Get("http://www.baidu.com" ) if err !=nil { log.Fatalln("无法调用" ) } defer r1.Body.Close() r2,err := http.Head("http://www.baidu.com" ) defer r2.Body.Close() form := url.Values{} form.Add("foo" ,"bar" ) r3,err := http.PostForm("http://www.baidu,com" ,form) defer r3.Body.Close() }
其他的HTTP动词,如PATCH,PUT,DELETE
,不存在便捷函数,我们主要使用这些动词来与RESTFUL api
进行交互
2. 生成一个请求 我们可以使用NewRequest()
创建结构体 Request
,然后使用Client的Do()发送该结构体
结构如下
1 2 3 func NewRequest (method, url string , body io.Reader) (*Request, error ) { return NewRequestWithContext(context.Background(), method, url, body) }
当我们需要发送一个DELETE
的请求,可以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "log" "net/http" ) func main () { req,err := http.NewRequest("DELETE" ,"http://www.baidu.com" ,nil ) var client http.Client resp,err := client.Do(req) if err != nil { log.Panicln(err) } resp.Body.Close() fmt.Println(resp.Status) }
下面是一个io.Reader
的Put
请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "log" "net/http" "net/url" "strings" ) func main () { form := url.Values{} form.Add("foo" ,"bar" ) var client http.Client req,err := http.NewRequest( "PUT" , "http://www.goole.com" , strings.NewReader(form.Encode()), ) if err != nil { log.Fatalln("failed" ) } resp,err := client.Do(req) fmt.Println(resp.Status) }
3. 使用结构化进行解析 在发送请求后,我们需要ioutil.ReadAll()
获取响应正文读取数据,进行一些错误检查,并将HTTP状态码和响应正文打印到stdout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "io/ioutil" "log" "net/http" ) func main () { resp,err := http.Get("https://www.baidu.com/robots.txt" ) if err != nil { log.Fatalln("failed" ) } fmt.Println(resp.Status) body,err :=ioutil.ReadAll(resp.Body) if err != nil { log.Fatalln(err) } fmt.Println(string (body)) resp.Body.Close() }
在接收到resp
的响应后,可以通过访问可输出的参数Status
来检索状态字符串(例如200 OK),还有一个与此类似的参数StatusCode
Response
类型。该参数仅存状态字符串的整数部分
Response
类型包含一个可输出的参数Body
,其类型为io.ReadCloser
,ioReadCloser
充当io.Reader
以及io.Closer
的接口,或者需要实现Close()
函数以关闭reader并执行任何清理的接口。从io.ReadCloser
读取数据后,需要在响应正文上调用Close()
函数。使用defer
关闭响应正文是一种常见的作法,这样可以保证函数在返回之前将其关闭
返回内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 200 OK User-agent: Baiduspider Disallow: /baidu Disallow: /s? Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Googlebot Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: MSNBot Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Baiduspider-image Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: YoudaoBot Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou web spider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou inst spider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou spider2 Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou blog Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou News Spider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sogou Orion spider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: ChinasoSpider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: Sosospider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: yisouspider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: EasouSpider Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh User-agent: * Disallow: /
如果需要解析更多的结构化数据,如JSON
格式的数据进行API
交互,则可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "encoding/json" "log" "net/http" ) type Status struct { Message string Status string , } func main () { res,err := http.Post( "http://IP:PORT/API" , "application/json" , nil , ) if err != nil { log.Fatalln(err) } var status Status if err := json.NewDecoder(res.Body).Decode(&status);err != nil { log.Fatalln(err) } defer res.Body.Close() log.Printf("%s->%s\n" ,status.Status,status.Message) }
0X02 构建与Shodan交互的HTTP客户端 当一个泄露的错误消息的web应用会被列入低危险等级,但是,如果错误消息泄露了企业用户的格式,并且其VPN内使用了单因素身份认证,则这些消息可能会增加通过猜测密码攻击内部网络的可能性
以Shodan
为例子,需要一个Shodan
的api密钥
。
从Shodan 站点获取 API 密钥并将其设为环境变量,仅当API密钥为SHODAN_API_KEY
的时候,下面示例才能正常工作
SHODAN API
非常简单,可以生成良好的JSON
响应,对初学者学习API
交互很有帮助,以下是步骤
查看服务的API
文档
设计代码的逻辑结构,以减少代码的复杂性和复用性
根据需要在Go
中定义请求或者响应类型。
创建辅助函数或者类型以简化初始化,身份认证和通信,从而减少冗长或者复杂的逻辑
构建与API
消费者函数和类型交互的客户端
1. 清理API调用 在阅读SHODAN
文档的时候,你应该已经注意到:每个公开的函数都需要发送API密钥
,尽管这个值传递给你所创建的每个消费者函数,但这么操作会非常繁琐。硬编码处理基础https://api.shodan.io
也会遇到相同的问题,如下面函数所示,要定义API函数,需要将令牌和URL一起传递给每个函数。
1 2 func APIInfo (token, url string ) ;func HostSearch (token, url string )
因此,我们选择一种更为常用的方法,先创建一个shodan.go
文件
1 2 3 4 5 6 7 8 9 10 11 package Shodanconst BaseURL = "http://api.shodan.io" type Client struct { apiKey string } func New (apikey string ) *Client { return &Client{apikey: apikey} }
Shodan URL 被定义为一个常见值,这样我们在实现函数中重用它,
由于这些是结构体Client
上的方法,因此可以通过s.apiKey
去检索API
密钥,并且通过BaseURL
去检索URL
2. 查询Shodan 订阅情况 现在,开始与Shodan
进行互动,根据API
文档,用于查询信息的调用如下
shodan 文档:https://developer.shodan.io/api
1 https://api.shodan.io/api-info?key={YOUR_API_KEY}
返回信息是如下的格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "scan_credits" : 100000 , "usage_limits" : { "scan_credits" : -1 , "query_credits" : -1 , "monitored_ips" : -1 } , "plan" : "stream-100" , "https" : false , "unlocked" : true , "query_credits" : 100000 , "monitored_ips" : 19 , "unlocked_left" : 100000 , "telnet" : false }
我们首先需要在api.go
中定义一个可用于把json
响应解组为go
结构体的类型,如果缺少这一步,将无法处理或者访问响应正文。
新建api.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package Shodanimport ( "encoding/json" "fmt" "net/http" ) type APIInfo struct { QueryCredits int `json:"query_credits"` ScanCredits int `json:"scan_credits"` Telnet bool `json:"telnet"` Plan string `json:"plan"` Https bool `json:"https"` Unlocked bool `json:"unlocked"` } func (s *Client) APIInfo()(*APIInfo, error ) { res,err := http.Get(fmt.Sprintf("%s/api-info?key=%s" ,BaseURL,s.apikey)) if err != nil { return nil ,err } var ret APIInfo; if err := json.NewDecoder(res.Body).Decode(&ret);err != nil { return nil ,err } return &ret,nil }
使用结构体数据显式调用json
元素名称,以确保映射和解析数据
同时APIInfo
发出HTTP的Get
请求,,并将响应解码成APIInfo
的结构体
我们在使用这段代码前,还需要使用一个有用的API
调用(主机搜索),将其添加到host.go
文件中。
根据API文档,该调用的请求和响应如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 ----有删减-------- { "matches" : [ { "product" : "nginx" , "hash" : -1609083510 , "ip" : 1616761883 , "org" : "Comcast Business" , "isp" : "Comcast Business" , "transport" : "tcp" , "cpe" : [ "cpe:/a:igor_sysoev:nginx" ], "data" : "HTTP/1.1 400 Bad Request\r\nServer: nginx\r\nDate: Mon, 25 Jan 2021 21:33:48 GMT\r\nContent-Type: text/html\r\nContent-Length: 650\r\nConnection: close\r\n\r\n" , "asn" : "AS7922" , "port" : 443 , "hostnames" : [ "three.webapplify.net" ], "location" : { "city" : "Denver" , "region_code" : "CO" , "area_code" : null, "longitude" : -104.9078 , "country_code3" : null, "latitude" : 39.7301 , "postal_code" : null, "dma_code" : 751 , "country_code" : "US" , "country_name" : "United States" }, "timestamp" : "2021-01-25T21:33:49.154513" , "domains" : [ "webapplify.net" ], "http" : { "robots_hash" : null, "redirects" : [], "securitytxt" : null, "title" : "410 Gone" , "sitemap_hash" : null, "robots" : null, "server" : "nginx/1.4.2" , "host" : "185.11.246.51" , "html" : "\n\n410 Gone\n\nGone\nThe requested resource/\nis no longer available on this server and there is no forwarding address.\nPlease remove all references to this resource.\n\n" , "location" : "/" , "components" : {}, "securitytxt_hash" : null, "sitemap" : null, "html_hash" : 922034037 }, "os" : null, "_shodan" : { "crawler" : "c9b639b99e5410a46f656e1508a68f1e6e5d6f99" , "ptr" : true , "id" : "118b7360-01d0-4edb-8ee9-01e411c23e60" , "module" : "auto" , "options" : {} }, "ip_str" : "185.11.246.51" }, ... ], "facets" : { "country" : [ { "count" : 1717359 , "value" : "HK" }, { "count" : 940900 , "value" : "FR" } ] }, "total" : 23047224 }
我们的代码是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package Shodanimport ( "encoding/json" "fmt" "net/http" ) type HostLocation struct { City string `json:"city"` RegionCode string `json:"region_code"` AreaCode int `json:"area_code"` Longitude float32 `json:"longitude"` CountryCode3 string `json:"country_code3"` CountryName string `json:"country_name"` PostalCode string `json:"postal_code"` DMACode int `json:"dma_code"` CountryCode string `json:"country_code"` Latitude float32 `json:"latitude"` } type Host struct { OS string `json:"os"` Timestamp string `json:"timestamp"` ISP string `json:"isp"` ASN string `json:"asn"` Hostnames []string `json:"hostnames"` Location HostLocation `json:"location"` IP int64 `json:"ip"` Domains []string `json:"domains"` Org string `json:"org"` Data string `json:"data"` Port int `json:"port"` IPString string `json:"ip_str"` } type HostSearch struct { Matches []Host `json:"matches"` } func (s *Client) HostSearch(q string ) (*HostSearch, error ) { res, err := http.Get( fmt.Sprintf("%s/shodan/host/search?key=%s&query=%s" , BaseURL, s.apiKey, q), ) if err != nil { return nil , err } defer res.Body.Close() var ret HostSearch if err := json.NewDecoder(res.Body).Decode(&ret); err != nil { return nil , err } return &ret, nil }
HostSearch:用于解析matches
数组
Host:表示matches的一个元素
HostLocation:表示主机中的location
字段
接下来,我们创建main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "Test1/Shodan" "fmt" "log" "os" ) func main () { if len (os.Args) != 2 { log.Fatalln("Usage: main <searchterm>" ) } apiKey := os.Getenv("SHODAN_API_KEY" ) s := Shodan.New(apiKey) info, err := s.APIInfo() if err != nil { log.Panicln(err) } fmt.Printf( "Query Credits: %d\nScan Credits: %d\n\n" , info.QueryCredits, info.ScanCredits) hostSearch, err := s.HostSearch(os.Args[1 ]) if err != nil { log.Panicln(err) } for _, host := range hostSearch.Matches { fmt.Printf("%18s%8d\n" , host.IPString, host.Port) } }
当我们输入go run main.go Tomcat
的时候,便可以查询
OK,一个调用Shodan API
的小型 go程序就完成了
msf 想必都熟悉,在这里,我们将会构建一个与远程Metasploit
实例进行交互的客户端,他要比Shodan
更复杂
后来改成如下的启动方式了,(图片请忽略)
1 load msgrpc ServerHost=127.0.0.1 ServerPort=55553 User='msf' Pass='msf
书上让本地启动 msfconsole
和 msgrpc
,这里,我选择使用kali
进行替代
书上说为了保险,避免对一些值进行硬编码,需要将以下值设置到环境变量中去,但是这里为了方便,我就先不设置了。
export MSFHOST xxxxxxxx
export MSFPASS xxxxxx
现在如图上方,我们已经运行了MSF 和 RPC
的服务器,接下来,我们查看MSF API
的开发文档,发现,他与使用JSON
交互的Shodan
不同,msf使用了MessagePack
(一种紧凑而高效的二进制格式)进行通信。但是,由于 go 官方库中不含,所以我们需要下载它
go get gopkg.in/vmihailenco/msgpack.v2
1. 定义目标 现在定义一个RPC包,创建msf.go
,
在Metasploit
开发人员文档中的方法session.list
官方文档:https://docs.rapid7.com/metasploit/rpc-api/
https://docs.rapid7.com/metasploit/standard-api-methods-reference/
1 { "session.list" , "token" }
这是最小的目标,它期望接收实现的方法是名称和令牌
,token
值是一个占位符,由文档可知,这是一个身份认证的令牌,是成功登录RPC
服务器发出的,从Metasploit
返回的方法session.list
响应采用以下格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "1" => { 'type' => "shell" , "tunnel_local" => "192.168.35.149:44444" , "tunnel_peer" => "192.168.35.149:43886" , "via_exploit" => "exploit/multi/handler" , "via_payload" => "payload/windows/shell_reverse_tcp" , "desc" => "Command shell" , "info" => "" , "workspace" => "Project1" , "target_host" => "" , "username" => "root" , "uuid" => "hjahs9kw" , "exploit_uuid" => "gcprpj2a" , "routes" => [ ] } }
该响应作为映射返回,Meterpreter
会话标识符是关键,而会话的详细信息是值
现在需要构建 Go
数据类型和响应结构体,根据文档,
请求结构体sessionListReq
按照Metasploit RPC
服务器所接受的方式,将结构化数据,序列化为MessagePack
格式,数据以数组的而不是映射的形式传递,因此,RPC
希望接受到的是作为值的位置数组
。==默认情况下,结构体将被编码为包含从属性名称推导出来的键名映射。==要禁用此功能且将其强制将其编码为位置数组
,必须添加一个名为_msgpack
的特殊字段,该字段利用描述符asArray
,显示指示编码器/解码器将数据视为数组
响应结构体SessionListRes
包含响应字段和结构体属性的一一对应关系,该数据本质上是一个请按套映射,外层映射是会话详细信息的会话标识符,内层映射是内层会话的详细信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package rpctype sessionListReq struct { _msgpack struct {} `msgpack:",asArray"` Method string Token string } type SessionListRes struct { ID uint32 `msgpack:",omitempty"` Type string `msgpack:"type"` TunnelLocal string `msgpack:"tunnel_local"` TunnelPeer string `msgpack:"tunnel_peer"` ViaExploit string `msgpack:"via_exploit"` ViaPayload string `msgpack:"via_payload"` Description string `msgpack:"desc"` Info string `msgpack:"info"` Workspace string `msgpack:"workspace"` SessionHost string `msgpack:"session_host"` SessionPort int `msgpack:"session_port"` Username string `msgpack:"username"` UUID string `msgpack:"uuid"` ExploitUUID string `msgpack:"exploit_uuid"` }
2. 获取有效令牌 现在,我们需要获取一个有效的登录令牌来获取请求,为此,我们将为api方法auth.login()
发出一个登录请求,该请求满足以下条件
登录失败的话
还有登出令牌的功能
3. 定义请求和响应 suth.login
和auth.logout
同理,我们需要使用描述控制符将请求序列化为数组并将响应视为映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type logoutReq struct { _msgpack struct {} `msgpack:",asArray"` Method string Token string LogoutToken string } type logoutRes struct { Result string `msgpack:"result"` } type loginReq struct { _msgpack struct {} `msgpack:",asArray"` Method string Username string Password string } type loginRes struct { Result string `msgpack:"result"` Token string `msgpack:"token"` Error bool `msgpack:"error"` ErrorClass string `msgpack:"error_class"` ErrorMessage string `msgpack:"error_message"` }
go可以自动的对登录响应进行序列化,仅填充了存在的字段,这意味着我们可以使用单一结构式表示成功或者失败
4. 创建配置结构体和RPC方法 创建一个结构体类型,以供数据隐式引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Metasploit struct { host string user string pass string token string } func New (host, user, pass string ) *Metasploit { msf := &Metasploit{ host: host, user: user, pass: pass, } return msf }
5. 执行远程调用 构建一个方法,执行远程调用。为了防止大量的代码重复,先构建一个可以执行,序列化,反序列化和HTTP通信逻辑的方法 send()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (msf *Metasploit) send(req interface {}, res interface {}) error { buf := new (bytes.Buffer) msgpack.NewEncoder(buf).Encode(req) dest := fmt.Sprintf("http://%s/api" , msf.host) r, err := http.Post(dest, "binary/message-pack" , buf) if err != nil { return err } defer r.Body.Close() if err := msgpack.NewDecoder(r.Body).Decode(&res); err != nil { return err } return nil }
在send
方法中,接受interface{}类型的请求和响应参数。使用此接口类型,可以将任何请求结构体传递到方法中,然后序列化发送到服务器,无需使用显示返回响应的方法。
接下来,使用msgPack
库对请求进行URL
编码,可以按照处理其他标准结构化数据的数据逻辑:首先通过NewEncoder()创建编码器,然后调用Encode
方法,这将用MessagePack
编码表示的请求体填充buf变量。之后发出POST请求,将主题设置为序列化数据。
然后接下来定义三个方法,每个方法使用相同的常规流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func (msf *Metasploit) Login() error { ctx := &loginReq{ Method: "auth.login" , Username: msf.user, Password: msf.pass, } var res loginRes if err := msf.send(ctx, &res); err != nil { return err } msf.token = res.Token return nil } func (msf *Metasploit) Logout() error { ctx := &logoutReq{ Method: "auth.logout" , Token: msf.token, LogoutToken: msf.token, } var res logoutRes if err := msf.send(ctx, &res); err != nil { return err } msf.token = "" return nil } func (msf *Metasploit) SessionList() (map [uint32 ]SessionListRes, error ) { req := &sessionListReq{Method: "session.list" , Token: msf.token} res := make (map [uint32 ]SessionListRes) if err := msf.send(req, &res); err != nil { return nil , err } for id, session := range res { session.ID = id res[id] = session } return res, nil }
但是,RPC
函数 session.list()需要有效的身份令牌,这就意味着必须要先登录,但是才能掉用方法SessionList()
所以可以对New
函数做一个更改
1 2 3 4 5 6 7 8 9 10 11 12 func New (host, user, pass string ) (*Metasploit,error ){ msf := &Metasploit{ host: host, user: user, pass: pass, } if err := msf.Login(); err != nil { return nil ,err } return msf,nil }
6.执行 创建 clinet/main.go
文件
这里没用获取环境变量,原文是
1 2 host := os.Getenv("MSFHOST" ) pass := os.Getenv("MSFPASS" )
这里输入自己的数值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "MSF/rpc" "fmt" "log" ) func main () { host := "" pass := "" user := "msf" if host == "" || pass == "" { log.Fatalln("Missing required environment variable MSFHOST or MSFPASS" ) } msf, err := rpc.New(host, user, pass) if err != nil { log.Panicln(err) } defer msf.Logout() sessions, err := msf.SessionList() if err != nil { log.Panicln(err) } fmt.Println("Sessions:" ) for _, session := range sessions { fmt.Printf("%5d %s\n" , session.ID, session.Info) } }
如果有Meterpreter
会话则会保存下来
0x04 使用Bing Scraping解析文档元数据 在渗透测试的时候,相对有用的信息可能会非常关键,这些信息会增加我们对目标攻击的可能性。这些信息的来源之一是文档元数据
。
某些情况下,这类信息会包含地理坐标, 应用程序版本,操作系统信息和用户名。
我们可以使用搜索引擎去检索关于一个组织的特定文件。
1. 配置和环境规划 我们首先对目标进行声明,我们只关注以xlsx
,docx
,pptx
等结尾的Office Open Xml
文档,虽然也可以关注旧版的Office
数据类型,但是二进制格式使他们成倍增加,并且会在增加代码复杂性的同时降低其可阅读性。对于PDF
文件也是如此。
我们使用抓取HTML
页面,而不是使用搜索引擎API
,在没有API的情况下,使用页面抓取的方法更为强大。
在这里我们使用一个goquery
,他的作用等用于jquery
安装:go get github.com/PuerkitoBio/goquery
2. 定义元数据包 在代码中定义与XML
数据集相对应的GO
类型,然后将代码放入一个名为 openxml.go
的文件中,该文件是我们想要解析的每个XML
的其中一种类型,然后添加数据映射和对应的函数,以确定与Appilcation
对应的可识别的Office
版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package metadataimport ( "encoding/xml" "strings" ) var OfficeVersions = map [string ]string { "16" : "2016" , "15" : "2013" , "14" : "2010" , "12" : "2007" , "11" : "2003" , } type OfficeCoreProperty struct { XMLName xml.Name `xml:"coreProperties"` Creator string `xml:"creator"` LastModifiedBy string `xml:"lastModifiedBy"` } type OfficeAppProperty struct { XMLName xml.Name `xml:"Properties"` Application string `xml:"Application"` Company string `xml:"Company"` Version string `xml:"AppVersion"` } func (a *OfficeAppProperty) GetMajorVersion() string { tokens := strings.Split(a.Version, "." ) if len (tokens) < 2 { return "Unknown" } v, ok := OfficeVersions[tokens[0 ]] if !ok { return "Unknown" } return v }
3. 把数据映射到结构体 接下来要读取适当的文件内容,并将内容赋值给所定义的结构体代码。为此定义函数NewProperties()
和proccess()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func process (f *zip.File, prop interface {}) error { rc, err := f.Open() if err != nil { return err } defer rc.Close() if err := xml.NewDecoder(rc).Decode(&prop); err != nil { return err } return nil } func NewProperties (r *zip.Reader) (*OfficeCoreProperty, *OfficeAppProperty, error ) { var coreProps OfficeCoreProperty var appProps OfficeAppProperty for _, f := range r.File { switch f.Name { case "docProps/core.xml" : if err := process(f, &coreProps); err != nil { return nil , nil , err } case "docProps/app.xml" : if err := process(f, &appProps); err != nil { return nil , nil , err } default : continue } } return &coreProps, &appProps, nil }
函数NewProperties()
接受了一个* zio.Reader
的参数,它表示Zip归档
文件的io.Reader
,使用io.Reader
实例,遍历归档文件类型,中所有文件并检查文件名,如果文件名与两个属性文件名中任意一个匹配,则调用函数process()
,并且传入文件要和填充的任意两个结构体类型:OfficeCoreProperty
或者OfficeAppProperty
函数process
接受两个参数,* zip.file
和 interface
。此代码接受通用的interface()
类型,以允许将文件内容赋给任何数据类型,因为在process
中没有特定的数据类型,增加了代码的重用性。在函数内,代码读取文件的内容并将XML
数据解码为结构体
。
4. 使用Bing
搜索和接受文件 现在,我们已经有了打开,读取,解析和提取Office Open Xml
文档需要的所有代码,并且知道我们接下来要做什么
使用适当的过滤器向Bing
提交搜索请求以检索目标结果
从HTML响应中提取HREF(链接)
数据以获得文档的导向URL
为每个导向文档URL提交一个HTTP请求
解析响应正文以创建zip.Reader
将zip.Reader
传递到我们已经开发的代码中以提取元数据
site: 用于过滤特定的域结果
fileType: 用于根据资源文件类型过滤结果
instreamset:用于过滤结果以仅包含某些文件扩展名
例如:从nytimes.com
中检索docx
文件的查询示例:
site:nytimes.com && filetype: docx &&instreamset:(url title):docx
接下来,我们要做的是确定文档链接
在文档对象模型(DOM)中的位置,可以使用F12进行查看。
有了这些,我们就可以使用goquery
来进行提取与HTML
路劲匹配的所有数据元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package mainimport ( "archive/zip" "bing/metadata" "bytes" "fmt" "github.com/PuerkitoBio/goquery" "io/ioutil" "log" "net/http" "net/url" "os" ) func handler (i int , s *goquery.Selection) { url, ok := s.Find("a" ).Attr("href" ) if !ok { return } fmt.Printf("%d: %s\n" , i, url) res, err := http.Get(url) if err != nil { return } buf, err := ioutil.ReadAll(res.Body) if err != nil { return } defer res.Body.Close() r, err := zip.NewReader(bytes.NewReader(buf), int64 (len (buf))) if err != nil { return } cp, ap, err := metadata.NewProperties(r) if err != nil { return } log.Printf( "%21s %s - %s %s\n" , cp.Creator, cp.LastModifiedBy, ap.Application, ap.GetMajorVersion()) } func main () { if len (os.Args) != 3 { log.Fatalln("Missing required argument. Usage: main.go <domain> <ext>" ) } domain := os.Args[1 ] filetype := os.Args[2 ] q := fmt.Sprintf( "site:%s && filetype:%s && instreamset:(url title):%s" , domain, filetype, filetype) search := fmt.Sprintf("http://www.bing.com/search?q=%s" , url.QueryEscape(q)) res, err := http.Get(search) if err != nil { return } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Panicln(err) } defer res.Body.Close() s := "html body div#b_content ol#b_results li.b_algo h2" doc.Find(s).Each(handler) }