Go渗透测试笔记(三)—HTTP客户端与工具的远程交互

0x00 Go的HTTP基础知识

  1. HTTP是一种无状态的协议,服务器不会维护每个请求的状态,而是通过多种方式跟踪其状态,这些方式可能包括:会话标识符,cookie,HTTP标头等。客户端和服务器有责任正确协商和验证状态
  2. 其次,客户端和服务器之间的通信可以一部或者同步进行,但他们需要以请求/响应的方式循环运行。可以在请求头中添加几个选项和表标头,以影响服务器的行为并创建可用的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 main

import (
"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 main

import (
"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 main

import (
"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.ReaderPut请求

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 main

import (
"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 main

import (
"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.ReadCloserioReadCloser充当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 main

import (
"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为例子,需要一个Shodanapi密钥

从Shodan 站点获取 API 密钥并将其设为环境变量,仅当API密钥为SHODAN_API_KEY的时候,下面示例才能正常工作

SHODAN API非常简单,可以生成良好的JSON响应,对初学者学习API交互很有帮助,以下是步骤

  1. 查看服务的API文档
  2. 设计代码的逻辑结构,以减少代码的复杂性和复用性
  3. 根据需要在Go 中定义请求或者响应类型。
  4. 创建辅助函数或者类型以简化初始化,身份认证和通信,从而减少冗长或者复杂的逻辑
  5. 构建与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 Shodan

const 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 Shodan

import (
"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
https://api.shodan.io/shodan/host/search?key={YOUR_API_KEY}&query={query}&facets={facets}
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 Shodan

import (
"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 main

import (
"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程序就完成了

0x03 与Metasploit交互

msf 想必都熟悉,在这里,我们将会构建一个与远程Metasploit实例进行交互的客户端,他要比Shodan更复杂

后来改成如下的启动方式了,(图片请忽略)

1
load msgrpc ServerHost=127.0.0.1 ServerPort=55553 User='msf' Pass='msf

书上让本地启动 msfconsolemsgrpc,这里,我选择使用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 rpc

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"`
}

2. 获取有效令牌

现在,我们需要获取一个有效的登录令牌来获取请求,为此,我们将为api方法auth.login()发出一个登录请求,该请求满足以下条件

登录失败的话

还有登出令牌的功能

3. 定义请求和响应

suth.loginauth.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 main

import (
"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 metadata

import (
"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.fileinterface。此代码接受通用的interface()类型,以允许将文件内容赋给任何数据类型,因为在process中没有特定的数据类型,增加了代码的重用性。在函数内,代码读取文件的内容并将XML数据解码为结构体

4. 使用Bing搜索和接受文件

现在,我们已经有了打开,读取,解析和提取Office Open Xml文档需要的所有代码,并且知道我们接下来要做什么

  1. 使用适当的过滤器向Bing提交搜索请求以检索目标结果
  2. 从HTML响应中提取HREF(链接)数据以获得文档的导向URL
  3. 为每个导向文档URL提交一个HTTP请求
  4. 解析响应正文以创建zip.Reader
  5. 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 main

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