Golang安全开发HTTP客户端与远程交互
HTTP基础知识
HTTP是一种无状态协议:服务器不会维护每个请求的状态,而是通过多种方式跟踪其状态,这些方式可能包括会话标识符、cookie、HTTP标头等,客户端和服务器有责任正确协商和验证状态
其次,客户端和服务器之间的通信可以同步或异步进行,但是他们需要以请求、响应的方式循环运行,可以在请求中添加几个选项和标头,以影响服务器的行为并创建可用的Web应用程序,最常见的是服务器托管Web浏览器渲染的文件,以生成数据的图形化、组织化和时尚化的表示形式,Api通常使用XML、JSON或MSGRPC进行通信,某些情况下,检索到的数据可能是二进制格式,表示要下载的任意文件类型。
在Go中,包含很多便捷函数,可以快速轻松地构建HTTP请求并将其发送到服务器,然后检索和处理响应
调用HTTP API
在Go中的net/http标准包包含多个便捷函数,可以便捷的发送POST、GET、和HEAD请求,这些请求可以说是要使用最常见的HTTP动词
Get(url string)(resp *Response,err error)
POST(url string, bodyTYpe string, body io.Reader)(resp *Response,err error)
Head(url string)(resp *Respones,err error)
每个函数都将URL字符串作为参数并将其用作请求的目的地,函数POST()要比函数Get()和HEAD()复杂一些。函数POST()具有两个附加参数(bodyType 和 io.Reader),其中bodyType用于接收请求正文的Content-Type HTTP标头(通常为application/x-www-form-urlencoded)
r1, err := http.Get("<https://www.baidu.com/robots.txt>")
//读取响应正文
defer r1.Body.Close()
r2, err := http.Head("<https://www.baidu.com/robots.txt>")
defer r2.Body.Close()
form := url.Values{}
form.Add("foo", "bar")
r3, err = http.Post(
"<https://www.baidu.com/robots.txt>",
"application/x-www-form-urlencoded",
strings.NewReader(form.Encode()),
)
defer r3.Body.Close
POST函数调用遵循这一常见约定,即当对表单数据进行URL编码时,将Content-Type设置为application/x-www-form-urlencoded
Go中还有一个可以发送POST请求的便捷函数,PostForm(),如果用它的话,就无法再设置这些值和手动编码每个请求
func PostForm(url string, data url.Values) (resp *Response, err error) {
return DefaultClient.PostForm(url, data)
}
form := url.Values{}
form.Add("foo", "bar")
//r3, err = http.Post(
// "<https://www.baidu.com/robots.txt>",
// "application/x-www-form-urlencoded",
// strings.NewReader(form.Encode()),
//)
r3,err :=http.PostForm("<https://www.baidu.com/robots.txt>",form)
defer r3.Body.Close
其他HTTP动词(例如PATCH、PUT、DELETE)不存在便捷函数,主要使用这些动词来与RESTful API进行交互,RESTful API采用了有关服务器使用方式的常用规范
生成一个请求
要使用这些动词之一生成请求,可以使用函数NewRequest()创建结构体Request,然后使用函数Client的方法Do()发送该结构体,这样执行起来很简单,http.NewRequest()的函数原型如下:
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
将HTTP动词和目标URL提供给函数NewRequest()作为其前两个参数,可以选择通过传入io.Reader作为第三个也是最后一个参数来提供请求正文
req,err :=http.NewRequest("DELETE","<https://www.baidu.com/robots.txt>",nil)
var client http.Client
resp,err :=client.Do(req)
展示一个带有io.Reader正文的PUT请求(类似于PATCH请求)
form := url.Values{}
form.Add("foo", "bar")
var client http.Client
req, err := http.NewRequest(
"PUT",
"<https://www.baidu.com/robots.txt>",
strings.NewReader(form.Encode()))
resp,err :=client.Do(req)
标准的Go net/http库包含一些函数,可以使用这些函数在将请求发送到服务器之前对其进行操作
使用结构化响应解析
在执行HTTP相关任务,就必须检查HTTP响应的各个组成部分,这些任务包括读取响应正文、访问COOKIE和标头或检查HTTP的状态
r1, err := http.Get("<https://www.baidu.com/robots.txt>")
if err != nil {
log.Panicln(err)
}
fmt.Println(r1.Status)
//读取并显示响应正文
body,err :=ioutil.ReadAll(r1.Body)
if err != nil {
log.Panicln(err)
}
//读取响应正文
fmt.Println(string(body))
defer r1.Body.Close()
在上面代码中名为r1的响应后,可以通过访问可输出的参数Status来检索状态字符串(例如200 OK),还有一个与此类似的StatusCode,该参数仅存取状态字符串的整数部分
Response类型包含一个可输出的参数Body,其类型为io.ReadCloser,io.ReadCloser充当io.Reader以及io.Closer的接口,或者是需要实现CLose()函数以关闭reader并执行任何清理的接口,从io.ReadCLoser读取数据后,需要在响应正文上调用CLose()函数,使用defer关闭响应正文
如果需要解析更多的结构化数据
import (
"encoding/json"
"log"
"net/http"
)
type Status struct {
Message string
Status string
}
func main() {
res, err := http.Post("<https://www.baidu.com/ping>",
"application/json", nil)
if err != nil {
log.Println(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)
}
这段代码定义了一个名为Status的结构体,其中包含服务器响应中的预期元素,main()函数首先发送POST请求,然后解码响应正文。之后,通过访问可输出的数据类型Status和Message来查询结构体Status
要解析结构化数据类型,可以执行如下操作:首先定义一个用来表示响应数据的结构体,然后数据解码到该结构体,以上操作也适用于解析其他编码格式(如xml或二进制表示形式)
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
)
func main() {
resp, err := http.Get("<https://www.baidu.com/robots.txt>")
if err != nil {
log.Panicln(err)
}
//打印http响应
fmt.Println(resp.Status)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Panicln(err)
}
fmt.Println(string(body))
resp.Body.Close()
resp, err = http.Head("<https://www.baidu.com/robots.txt>")
if err != nil {
log.Panicln(err)
}
resp.Body.Close()
fmt.Println(resp.Status)
form := url.Values{}
form.Add("foo", "bar")
resp, err = http.Post(
"<https://www.baidu.com/robots.txt>",
"application/x-www-form-urlencoded",
strings.NewReader(form.Encode()))
if err != nil {
log.Panicln(err)
}
resp.Body.Close()
fmt.Println(resp.Status)
req, err := http.NewRequest("DELETE", "<https://www.baidu.com/robots.txt>", nil)
if err != nil {
log.Println(err)
}
var client http.Client
resp, err = client.Do(req)
resp.Body.Close()
fmt.Println(resp.Status)
req, err = http.NewRequest("PUT", "<https://www.baidu.com/robots.txt>", strings.NewReader(form.Encode()))
resp, err = client.Do(req)
if err != nil {
log.Panicln(err)
}
resp.Body.Close()
fmt.Println(resp.Status)
}
构建与Shodan交互的HTTP客户端
回顾构建API客户端的步骤
将构建一个与Shodan API交互的HTTP客户端,解析结构并显示相关信息。
- 查看服务的API文档
- 设计代码的逻辑结构,以减少代码的复杂性和重复性
- 根据需求在Go中定义请求或响应类型
- 创建辅助函数和类型以简化初始化、身份认证和通信,从而减少冗长或重复的逻辑
- 构建与API消费者函数和类型交互的客户端
设计项目结构
首先构建API客户端时,应对其进行结构设计,以使函数调用和逻辑独立,这样可以将实现作为其他项目中的库重用,这样以后就可以就不必重新开发轮子了,建立可重用性则可能会改变项目的结构
----cmd
-----shodan
----main.go
----shodan
----api.go
----host.go
----shodan.go
main.go文件定义的main包是要构建API的,主要使用它与客户端进行交互
Shodan目录的文件(api.go、host.go和Shodan.go)定义了Shodan包,其中包含与Shodan之间进行通信所需的类型和函数,这个包以后可以作为独立库,可以用在其他项目中并使用这个包
清理API调用
Shodan Api https://developer.shodan.io/api
阅读Shodan API文档时,可以注意到,每个公开函数都需要发送API秘钥,尽管可以将这个值传递给创建的消费者函数,但这么操作就会很繁琐,硬编码或处理基础URL也会遇到同样的问题,要定义API函数,需要将令牌和URL传递给每个函数,这样写出来的代码看上去不太优雅
<https://api.shodan.io/shodan/host/{ip}?key=>
func APIInfo(token,url string){}
func HostSearch(token,url string){}
因此,应选择一种更为常用的方法,该方法可以节省很多次数,同时又可以使代码更具可读性,可以这样操作:创建一个shodan.go文件
package shodan
// 定义一个常量BaseURL
const BaseURL = "<https://api.shodan.io>"
// 定义一个Client结构体用于定义apikey的
type Client struct {
apiKey string
}
/*
*Client 表示该函数的返回类型是指向 Client 类型的指针,而不是 Client 类型本身。这是因为在Go语言中,结构体是值类型,因此,如果在函数中修改结构体的字段,则不会对原始结构体产生影响,而需要返回指向修改后结构体的指针。
return &Client{apiKey: apiKey} 的作用是创建一个新的 Client 结构体,并将其中的 apiKey 字段初始化为传递给函数的 apiKey 参数。然后,使用 & 运算符返回指向新创建的 Client 结构体的指针。
因此,func New(apiKey string) *Client 的作用是创建并返回一个指向新创建的 Client 结构体的指针,该结构体包含了用于进行 Shodan API 请求的 API 密钥。
*/
// 创建一个New函数 参数包含apikey字符串,
func New(apiKey string) *Client { //会创建一个新的Client结构体
return &Client{apiKey: apiKey} //指针指向Client,返回apikey
}
ShodanURL定义为一个常量值,这样就可以实现函数中轻松访问和重用它,如果Shodan曾经更改为其API的URL,只需要在这一位置进行更改即可更正整个代码库。接下来定义一个结构体Client,该结构体用于维护请求中的API令牌。最后,定义一个辅助函数New(),以API作为输入,创建并返回一个初始化的实例Client。现在,不是将API代码构建为任意函数,而是将他们创建为结构体CLient上的方法,这使可以直接查询实例,而不必依赖过于冗余的函数参数。可以将API函数调用
func (s *Client) APIInfo(){}
func (s *Client)HostSearch(){}
由于这些是结构体Client上的方法,因此可以通过s.apiKey检索API密钥,并且通过BaseURL检索URL,要调用结构体Client上的方法,必须首先创建结构体Client的实例,可以使用shodan.go中的辅助函数New()执行此操作
查询Shodan订阅情况
现在,开始与shodan进行互动,根据shodan API文档,用于查询订阅计划信息的调用如下:
https://api.shodan.io/api-info?key=xxxxxxxxxxxxxxxxxx
返回的响应类似于一下结构体。显然,这些值会随计划详情和剩余的订阅积分的不同而有所不同
{"scan_credits": 100, "usage_limits": {"scan_credits": 100, "query_credits": 100, "monitored_ips": 16}, "plan": "dev", "https": false, "unlocked": true, "query_credits": 100, "monitored_ips": null, "unlocked_left": 100, "telnet": false}
首先,需要在api.go中定义一个可用于把json响应解组为GO结构体的类型,如果缺少这一步,将无法处理或访问响应正文
package shodan
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"`
}
Go强大的功能使该结构体和JSON参数一一对应。可以使用一些出色的工具自动解析json,从而为你填充字段。对于结构体上的每种可导出的数据类型,都可以使用结构体标签显式定义JSON元素名称, 以确保正确地映射和解析数据
接下来,该函数向Shodan发出HTTP GET请求并将响应解码为结构体 ApiInfo
/*
这段代码是 Client 类型的 ApiInfo() 方法的实现。该方法通过 HTTP GET 请求向 Shodan API 发送 api-info 请求,并将客户端提供的 API 密钥作为参数发送。
当 API 请求成功时,该方法将返回一个 ApiInfo 结构体类型的指针和一个 error 类型的值。ApiInfo 结构体类型包含了有关当前用户的 Shodan API 使用情况的信息,例如剩余查询次数、API 计划等。error 类型的值用于指示操作是否成功。
在实现方法时,代码通过调用 http.Get() 函数向 Shodan API 发送请求,并将其响应存储在名为 res 的变量中。如果在发送请求时发生错误,则方法将立即返回该错误。否则,代码使用 defer 语句确保在方法返回之前关闭响应主体。
接下来,代码通过调用 json.NewDecoder().Decode() 函数,将响应体解析为 ApiInfo 结构体类型。如果解码过程中发生错误,则方法将立即返回该错误。否则,方法将返回指向解析后的 ApiInfo 结构体的指针
*/
func (c *Client) ApiInfo() (*ApiInfo, error) {
res, err := http.Get(fmt.Sprintf("%s/api-info?key=%s", BaseURL, c.apiKey))
if err != nil {
return nil, err
}
defer res.Body.Close()
var ret ApiInfo
if err := json.NewDecoder(res.Body).Decode(&ret); err != nil {
return nil, err
}
return &ret, err
}
这段代码简短而优雅,首先向/api-info资源发出HTTP Get请求,使用BaseURL全局常量和c.apiKey构建完整的URL。然后,将响应解码为结构体ApiInfo并将其返回给调用方
在编写利用这种崭新逻辑的代码之前,要再构建一个更有用的API调用,将其添加到host.go文件中,根据API文档,该调用的请求和响应如下:
https://api.shodan.io/shodan/host/search?key=xxxxxxxxxx&query={query}&facets={facets}
package shodan
/*
这段代码定义了一个名为 HostLocation 的结构体类型,该类型包含了与 Shodan 主机位置信息相关的字段。每个字段都包含了该位置信息的不同属性,如城市、区域代码、经纬度等。字段的含义如下:
City:主机所在城市的名称。
RegionCode:主机所在地区的代码。
AreaCode:主机所在地区的区域代码。
Longitude:主机所在位置的经度。
CountryCode3:主机所在国家的三字母代码。
CountryName:主机所在国家的名称。
PostalCode:主机所在位置的邮政编码。
DMACode:主机所在地区的 DMA(Designated Market Area)代码。
CountryCode:主机所在国家的两字母代码。
Latitude:主机所在位置的纬度。
这些字段的类型不同,包括字符串类型、整型和浮点型。此外,字段标记了不同的 JSON 标记,以便在使用 JSON 解码器时能够正确地将 JSON 数据映射到结构体字段。因此,可以将该结构体用于解析 Shodan API 返回的主机位置信息的 JSON 数据。
*/
type HostLocation struct {
City string `json:"city"`
RegionCode string `json:"regionCode"`
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"`
}
/*
这段代码定义了一个名为 Host 的结构体类型,该类型包含了与 Shodan 主机信息相关的字段。每个字段都包含了不同的主机信息属性,如操作系统、时间戳、ISP(Internet Service Provider,互联网服务提供商)、ASN(Autonomous System Number,自治系统编号)等。字段的含义如下:
OS:主机操作系统名称。
Timestamp:主机信息记录的时间戳。
ISP:主机所使用的互联网服务提供商。
ASN:主机所在自治系统的编号。
Hostnames:主机的主机名列表。
Location:主机所在的位置信息,类型为 HostLocation。
IP:主机的 IP 地址,类型为 int64。
Domains:主机的域名列表。
Org:主机所属的组织。
Data:主机信息的原始数据。
Port:Shodan 扫描到的端口号。
IPstring:主机的 IP 地址,以字符串形式表示。
这些字段的类型不同,包括字符串类型、整型、整型切片、字符串切片和自定义类型(HostLocation)。此外,字段标记了不同的 JSON 标记,以便在使用 JSON 解码器时能够正确地将 JSON 数据映射到结构体字段。因此,可以将该结构体用于解析 Shodan API 返回的主机信息的 JSON 数据。
*/
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"` //Location:主机所在的位置信息,类型为 HostLocation,这些字段的类型不同,包括字符串类型、整型、整型切片、字符串切片和自定义类型(HostLocation)。此外,字段标记了不同的 JSON 标记,以便在使用 JSON 解码器时能够正确地将 JSON 数据映射到结构体字段。因此,可以将该结构体用于解析 Shodan API 返回的主机信息的 JSON 数据。
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 {
Metches []Host `json:"metches"`
}
在上述代码定义了3种类型:
- HostSearch:用于解析matches数组
- Host:表示matches的一个元素
- HostLocation:表示主机中的location元素
这些类型可能未定义所有响应字段,Go优雅地处理了这个问题,使你可以仅使用所需的JSON字段来定义结构体。因此,上述代码将可以很好地解析JSON数据,同时通过仅包含与实例最相关的字段来减少代码的长度。要初始化并填充该结构体
func (c *Client) HostSearch(q string) (*HostSearch, error) {
res, err := http.Get(fmt.Sprintf("%s/shodan/host/search?key=%s&query=%s",BaseURL,c.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, err
}
以上函数遵循的流程和逻辑与ApiInfo()方法完全相同,不同之处在于,我们将搜索查询字符串作为参数,在传递搜索条件的同时向/shodan/host/search端点发出调用,并且将响应解码为结构体HostSearch
对于每个需要交互的API服务,要重复此结构体定义和函数实现的过程
创建一个客户端
使用一种简单的方法来创建客户端,即将搜索条件作为命令行参数,然后调用方法ApiInfo()和HostSearch()
package main
import (
"GoStudy/Go-Hacking/HTTP/shodan/shodan"
"fmt"
"log"
"os"
)
func main() {
if len(os.Args) != 2 {
log.Fatalln("Usage: shodan 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)
}
}
首先从SHODAN_API_KEY环境变量中读取API秘钥,然后使用该值初始化新的结构体CLient,随后使用它调用ApiInfo()方法。之后再调用方法HostSearch,传入从命令行参数取得的搜索字符串。最后,遍历结果以显示与查询字符串匹配的那些服务的IP和端口值
与Metasploit交互
Metasploit 文档 https://docs.metasploit.com/docs/using-metasploit/advanced/RPC/
Metasploit是用于执行各种对抗技术的框架,这些对抗技术包括侦查、利用、命令和控制、持久性、横向移动、载荷创建和交付、权限提升等
通过Metasploit中的msgrpc模块启动Metasploit控制台以及RPC监听器,然后设置服务器主机(RPC服务器将在其上监听的IP和密码)
msf6 > load msgrpc pass=root123 ServerHost=192.168.148.129
[192.168.148.129:55552 ] MSGRPC Service:
[ ] MSGRPC Username: msf
[ ] MSGRPC Password: BqlW09ND
[ ] Successfully loaded plugin: msgrpc
为了使代码更具可移植性并避免对一些值进行硬编码,可以将环境变量设置为RPC实例定义的值
export MSFHOST= xxxxx:55552
export MSFPASS=s3cr3t
在Rapid7官网上查看Metasploit API开发文档,上面公开的功能还是挺全面的,可以通过本地交互远程执行任何操作,与Shodan使用的Json进行通信不同,Metasploit使用MessagePack(一种紧凑型而高效的二进制格式)进行通信,由于Go不包含标准的MessagePack包,所以需要使用功能齐全的社区版实现:
msgpack包地址 https://gopkg.in/vmihailenco/msgpack.v5
go get gopkg.in/vmihailenco/msgpack.v2
在代码中,将实现成为msgpack,不必太过考虑MessagePack的各种规范,若构造一个可用的客户端,几乎不需要了解MessagePack本身。除此之外,用于启动编码和解码的代码与其他格式(如Json、xml)相同
Metasploit目录结构
Metasploit
---client
---main.go
---rpc
---msf.go
在msf.go文件位于rpc软件包中,将使用client/main.go来实现和测试所构建的库
定义目标
实现代码以进行交互并发出RPC调用,检索当前Meterpreter绘画列表,即Metasploit开发人员文档中的方法session.list,该方法请求定义如下:
["session.list","token"]
它期望接受要实现的方法的名称和令牌,token值是一个占位符由文档可以看到,这是一个身份验证令牌,该令牌是在登陆成功RPC服务器发出的
{
"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和响应结构体SessionListRes
package rpc
// Metasploit绘画列表类型定义
type 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"`
}
可以使用请求结构体SessionListReq,按照Metasploit RPC服务器期望接受的方式,特别是按照它期望接受的方法名称和令牌值,将结构化数据序列化为MessagePack格式。需要注意的是,这些字段没有任何描述符,数据以数组而不是映射的形式传递,因此RPC接口希望接受的数据是坐位置的位置数组,而不是健/值。由于无需定义键名,因此会省略这些属性的注释。但是,默认情况下,结构体将被编码为包含从属性名称推导出 的键名的映射。要禁用此功能并强制将其编码为位置数组,需添加一个名为_msgpack的特殊字段,该字段利用描述符asArray显式指示编码器、解码器将数据视为数组
响应结构体SessionListRes包含响应字段和结构体属性之间得以一对一映射,如前面的响应所示,该数据本质上是一个嵌套映射。外层映射是会话详情信息的会话标识符,而内层映射是会话详细信息,使用健/值对表示,与请求不同的是,响应并不会像位置数组那样被结构化,每个结构体属性会使用描述符来显式明明数据并将数据映射成Meatasploit的表现形式。该代码把绘画标识符作为结构体的一个属性。但是,由于标识符的实际值是键值,因此填充的方式会有所不同,需要使用描述符omitempty,以使数据称为可选数据,从而不影响编码或解码。这样会使数据片平滑,因此不必使用嵌套映射。
获取有效令牌
现在,只有一件事需要做,那就是必须获取一个有效的令牌值以用于该请求,为此,将为API方法auth.login()发出一个登陆请求,该请求应满足以下条件
[ "auth.login", "MyUserName", "MyPassword"]
需要用到初始设置期间在Metasploit中加载msfrpc模块时使用的用户名和密码,如果身份验证成功,服务器会响应以下消息
{ "result" => "success", "token" => "a1a1a1a1a1a…" }
身份验证失败将返回以下响应
{
"error" => true,
"error_class" => "Msf::RPC::Exception",
"error_message" => "Invalid User ID or Password"
}
此外,还创建退出登录令牌的功能,该请求有方法名称、身份验证令牌和一个可选参数
[ "auth.logout", "<token>", "<LogoutToken>"]
成功的响应
{ "result" => "success" }
定义请求和响应方法
现在,已经为方法session.list()的请求和响应创建了结构体SessionListReq和SessionListRes,接下来需要对方法auth.login()和auth.logout()都执行相同的操作
// 登录和登出
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"`
}
type logoutReq struct {
_msgpack struct{} `msgpack:",asArray"`
Method string
Token string
logoutToken string
}
type logoutRes struct {
Result string `msgpack:"result"`
}
值得注意的是,Go动态地对登录响应进行序列化,仅填充了存在的字段,这意味着可以使用单一结构格式来表示成功和失败的登录
创建配置结构体RPC方法
将使用定义的数据类型创建必要的方法来向Metasploit发送RPC命令,就像Shodan中那样,可以定义一个任意数据类型来维护相关的配置和身份验证信息。这样,就不必显式重复输入主机、端口和身份验证令牌登常见元素。不过,可以使用结构体类型并在其上构建方法,以使数据隐式可用
// Metasploit客户端定义
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
}
现在,有了一个结构体,为方便期间,创建一个名为New()函数,该函数用来初始化并返回一个新的结构体
执行远程调用
现在,在结构体Metasploit上构建方法,以执行远程调用。为防止大量的代码重发,首先构建一个执行序列化、反序列化和HTTP通信逻辑的方法send()。这样就不必再构建的每个RPC函数中都包含此逻辑
// 可重复的序列化和反序列化的通用send方法
func (msf *Metasploit) send(req, res any) 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{}类型(any)的请求和响应参数。使用此接口类型,可以将任何请求结构体传递给方法中,然后序列化并将请求发送到服务器。无需使用显式返回响应的方法,而是使用参数 res interfa{}(any)通过将已解码的HTTP响应写入其在内存中的位置来填充数据
接下来,使用msgpack库对请求进行编码,可以参照处理其他标准结构化数据类型的逻辑:首先通过NewEncoder()创建编码器,然后调用方法Encode()。这将用MessagePack编码表示的请求结构体填充buf变量。编码之后,可以使用Metasploit接收器msf中的数据构建目标URL。使用该URL并发出POST请求,将内容类型显式设置为binary/message-pack,并且将主题设置为序列化数据。最后,对响应正文进行解码。如前面所述,将解码后的数据写入床底到方法中的响应接口的存储位置。无需对请求或响应结构体类型有明显的了解即可完成数据的编码和解码,这是一种灵活、可重用的方法
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
}
这里定义3个方法:Login()、Logout()、SessionList。每个方法都使用相同的常规流程:创建和初始化请求结构体、创建响应结构体以及调用辅助函数,以发送请求并接受解码后的响应。方法Login()和Logout()操作token属性。方法SessionList()使用不同于前两种方法的逻辑,在该方法中,将响应定义为map[uint32]SessionListRes并在该响应上循环以展开映射,在结构体上设置ID属性而不是维护嵌套的映射
RPC函数session.list()需要有效的身份验证令牌,这表示要先登录,才能成功调用SessionList()。使用Metasploit接收器结构体访问令牌,该令牌尚未生效,它是一个空字符串。由于在此处编写的功能代码还不完善,因此可以在定义方法SessionList时显式添加对方法Login()的调用,但是对于实现的每个其他经过身份验证方法,都必须进行检查以确定是否存在有效的身份验证令牌并显式调用方法Login()。
已经实现了一个函数New(),该函数是一个辅助函数,因此可对该函数进行修正,以查看将身份验证纳入处理过程的运行情况
func New(host, user, pass string) (*Metasploit, error) {
msf := &Metasploit{
host: host,
user: user,
pass: pass,
}
//嵌入Metasploit登录初始化
if err := msf.Login(); err != nil {
return nil, err
}
return msf, nil
}
修改后的代码将错误作为返回值的一部分,这是为了警告可能出现的身份验证失败。同样,把对方法Login()的显式调用添加到代码中,只要使用此New()函数实例化结构体Metasploit,经过身份验证的方法调用就可以访问有效的身份验证令牌
创建使用程序
创建基于新类库的实用程序
import (
"GoStudy/Go-Hacking/HTTP/Metasploit/rpc"
"fmt"
"log"
"os"
)
func main() {
host :=os.Getenv("MSFHOST")
pass :=os.Getenv("MSFPASS")
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()
session,err :=msf.SessionList()
if err != nil {
log.Panicln(err)
}
fmt.Println("Sessions:")
for _,session:=range session{
fmt.Printf("%5d %s\\n",session.ID,session.Info)
}
}
RPC代码
package rpc
import (
"bytes"
"fmt"
"gopkg.in/vmihailenco/msgpack.v2"
"net/http"
)
type 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"`
}
// 登录和登出
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"`
}
type logoutReq struct {
_msgpack struct{} `msgpack:",asArray"`
Method string
Token string
logoutToken string
}
type logoutRes struct {
Result string `msgpack:"result"`
}
// Metasploit客户端定义
type Metasploit struct {
host string
user string
pass string
token string
}
func New(host, user, pass string) (*Metasploit, error) {
msf := &Metasploit{
host: host,
user: user,
pass: pass,
}
//嵌入Metasploit登录初始化
if err := msf.Login(); err != nil {
return nil, err
}
return msf, nil
}
// 可重复的序列化和反序列化的通用send方法
func (msf *Metasploit) send(req, res any) 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
}
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
}
实现了与远程Metasploit进行交互,以检索可用的Meterpreter会话
运行截图
使用Bing Scraping解析文档数据
在Shodan部分中所强调的那样,当在正确地上下文环境中查看信息时,相对有用的信息可能会非常关键,这些信息会增加我们对目标攻击成功的可能性。诸如雇佣员姓名、电话号码、电子邮件地址和客户端软件版本之类的信息通常会被高度重视,因为它们提供了具体或可操作的信息,攻击者可以直接利用或使用这些信息来进行更有效和更有针对性的攻击。这类信息来源之一是文档元数据,一个名为FOCA的工具来检索和解析文档元数据方面做的非常出色:
GitHub 项目 https://github.com/ElevenPaths/FOCA
应用程序会在保存到磁盘的文件结构中存储任意信息。某些情况下,这些信息中可能包含地理坐标、应用程序版本、操作系统信息和用户名。可以使用搜索引擎包含高级查询过滤器,使得我们可以检索关于一个组织的特定文件
配置环境和规划
首先,将关注以xlsx、docx、pptx等结尾的Office Open Xml文档。尽管也可以关注旧版Office的数据类型,但是二进制格式使他们成倍增加,并且会增加代码复杂度的同时降低其可读性。
将使用一个出色的包Goquery,它功能很强大,作用等同于JQuery,jQuery使一个JavaScript库,其中包含只管的语法,可便利购HTML文档并且选择其中的数据
Goquery 项目地址 https://github.com/PuerkitoBio/goquery
安装goquery
go get github.com/PuerkitoBio/goquery
完成对Goquery的安装后,将不必再安装其他必备软件,将使用标准的Go包与Open XML文件交互。这些文件是zip归档文件,提取后包含XML文件,元数据存储在归档的docProps目录内的两个文件中
在core.xml文件包含作者信息以及详细的修改信息,其结构如下:
<cp:coreProperties xmlns:cp="<http://schemas.openxmlformats.org/package/2006/metadata/core-properties>" xmlns:dc="<http://purl.org/dc/elements/1.1/>" xmlns:dcterms="<http://purl.org/dc/terms/>" xmlns:dcmitype="<http://purl.org/dc/dcmitype/>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>">
<dc:title>附件14:</dc:title>
<dc:creator>微软用户</dc:creator>
<cp:lastModifiedBy>zhangqq</cp:lastModifiedBy>
<cp:revision>26</cp:revision>
<cp:lastPrinted>2020-11-05T03:48:00Z</cp:lastPrinted>
<dcterms:created xsi:type="dcterms:W3CDTF">2020-09-11T08:08:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2021-10-28T09:55:00Z</dcterms:modified>
</cp:coreProperties>
Creator和lastModifiedBy元素是最重要的,这些字段包含可在社会工程学或密码猜测活动中使用的员工或用户名
app.xml文件包含有关于创建Open XML文档的应用程序类型和版本的详细信息,结构如下:
<Properties xmlns="<http://schemas.openxmlformats.org/officeDocument/2006/extended-properties>" xmlns:vt="<http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes>">
<Template>Normal.dotm</Template>
<TotalTime>24</TotalTime>
<Pages>45</Pages>
<Words>3517</Words>
<Characters>20052</Characters>
<Application>Microsoft Office Word</Application>
<DocSecurity>0</DocSecurity>
<Lines>167</Lines>
<Paragraphs>47</Paragraphs>
<ScaleCrop>false</ScaleCrop>
<Company>微软中国</Company>
<LinksUpToDate>false</LinksUpToDate>
<CharactersWithSpaces>23522</CharactersWithSpaces>
<SharedDoc>false</SharedDoc>
<HyperlinksChanged>false</HyperlinksChanged>
<AppVersion>15.0000</AppVersion>
</Properties>
我们只对其中一些元素感兴趣,这些元素包括Application、Company、AppVersion,版本本身与Office版本名(如Office 2013、Office 2016等)没有明显的关联,但该字段与更可读、更常见的替代项之间确实存在逻辑映射,我们开发的代码将维护这个映射
定义元数据包
我们将定义一个openxml.go的文件,该文件是我们想要解析每个xml文件的其中一个类型,然后添加数据映射和响应的函数,以确定与AppVersion对应的可识别的Office版本
package metadata
import (
"encoding/xml"
"strings"
)
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"`
}
var OfficeVersions = map[string]string{
"16": "2016",
"15": "2013",
"14": "2010",
"12": "2007",
"11": "2003",
}
func (a *OfficeAppProperty) GetMajorVersion() string {
tokens := strings.Split(a.Version, ".") //截取AppVersion
if len(tokens) < 2 {
return "Unknown"
}
v, ok := OfficeVersions[tokens[0]]
if !ok {
return "Unknown"
}
return v
}
定义结构体OfficeCoreProperty和OfficeAppProperty后,定义一个映射OfficeVersions,该映射维护主要版本与可识别发行年份的关系。若要使用此映射,可在结构体OfficeAppProperty上定义方法GetMajorVersion(),该方法拆分XML数据的AppVersion值以检索主要版本号,随后使用该值和映射OfficeVersions来检索发布年份
把数据映射到结构体
现在,构建好了用于处理和检查兴趣的XML数据的逻辑和类型,接下来可以创建用于读取适当文件将内容赋给结构体的代码。为此,定义函数NewProperties()和process()
// 处理归档和嵌入式XML文档
func process(f *zip.File, prop any) 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()接受一个*zip.Reader类型的参数,它表示ZIP归档文件的io.Reader。使用zip.Reader实例,遍历归档文件中的所有文件并检查文件名。如果文件名与属性文件名中的任意一个匹配,则调用函数process,并且传入文件和要填充的任意结构体类型—OfficeCoreProperty或者OfficeAppProperty
函数process()接收两个参数:*zip.File和interface{}(any),与Metasploit工具类似,此接收通用interface{}(any)类型,以允许将文件内容赋给任何数据类型。因为在函数process()中没有特定的数据类型,所以这增加了代码重用性。在函数内,代码读取文件的内容并将XML数据解码为结构体
使用Bing搜索和接收文件
在拥有了打开、读取、解析和提取Office Open Xml文档需要的所有代码,并且知道需要对文件做什么。
那么接下来,要弄清楚如何使用Bing搜索和检索文件。
如:
- 使用适当的过滤器向Bing提交搜索请求以检索目标结果
- 从HTML响应中提取HREF链接数据以获得文档的导向URL
- 为每个导向文档URL提交一个HTTP请求
- 解析响应正文以创建zip.Reader
- 将zip.Reader传递给已经开发的代码,来进行提取数据
首先要建立一个搜索查询模块,与Google一样,Bing包含高级查询参数,我们可以使用这些参数过滤大量的搜索结果。这些过滤器大多以filer_type:value 格式提交
Bing搜索语法:
Bing搜索语法文档 https://support.microsoft.com/zh-cn/topic/%E9%AB%98%E7%BA%A7%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E5%AD%97-ea595928-5d63-4a0b-9c6b-0b769865e78a
在这里主要使用以下关键字:
site:用于过滤特定域结果
filetype:用于根据资源文件类型过滤结果
instreamset:用于过滤结果以仅包括某写文件扩展名
site:pingan.com.cn && filetype docx && instreamset : (url title) : docx
以Pingan.com为例
提交查询之后,可以在开发者工具查看HTML中的HERF链接信息
在知道了URL和参数格式,就可以看到HTML响应,但是首先要确定文档链接在文档对象模型中的位置
有了这个路径,就可以使用goquery来系统的提取HTML路径匹配的所有数据元素
package main
import (
"GoStudy/Go-Hacking/HTTP/bing/metadata"
"archive/zip"
"bytes"
"fmt"
"github.com/PuerkitoBio/goquery"
"io"
"log"
"net/http"
"net/url"
"os"
)
// 抓取bing结果并解析文档数据
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 := io.ReadAll(res.Body)
if err != nil {
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(res.Body)
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("%25s %25s - %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")
}
domian := os.Args[1]
filetype := os.Args[2]
q := fmt.Sprintf("site:%s && filetype:%s && instreamset : (url title) : %s",domian,filetype,filetype)
search :=fmt.Sprintf("<https://www.bing.com/search?!=%s>",url.QueryEscape(q))
//client := &http.Client{}
client :=&http.Client{}
req,err :=http.NewRequest("GET",search,nil)
if err != nil {
return
}
req.Header.Add("user-agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36")
resp,err :=client.Do(req)
if err != nil {
return
}
doc,err :=goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatalln(err)
}
s := "html body div#b_content ol#b_results li.b_algo div.b_title h2"
doc.Find(s).Each(handler)
}
该代码创建两个函数,其中函数handler()接收一个goquery.Selection实例,在这里它将使用锚点html元素填充,查找并提取href属性。此属性包含从Bing搜索返回的文档的直接连接。然后,使用URL发出get请求以检索文档。假如没有错误发生,然后读取响应正文,利用它来创建一个zip.Reader。
在之前创建的函数NewProperties()需要一个zip.Reader。现在已经拥有了适当的数据类型,然后将其传递给该函数,然后从文件中填充属性并将其打印到屏牧上
函数main()引导并控制整个过程,将域名和文件类型作为命令行参数传递给它。然后,该函数使用此输入数据和适当的过滤器来构建Bing查询。过滤器字符串经过编码,用于构建完整的Bing搜索URL。使用函数goquery.NewDocumentFromReader()来获取HTML响应(在原文中,goqury.NewDocument该方法以弃用,改用http包进行发送请求),可以使用goquery检索文档,最后,使用在浏览器开发人员工具中标识的HTML元素选择器字符串查找并迭代匹配HTML元素,对于每个匹配到的元素,都会对函数handler()进行调用
坑点:现在bing搜索不会直接返回响应,如果要绕过就需要在代码Header添加UA头,这样才会有对应的响应