Skip to content

Commit

Permalink
Add auto TLS fingerprint (#51)
Browse files Browse the repository at this point in the history
Co-authored-by: Sleeyax <[email protected]>
Co-authored-by: vladimir.razdrogin <[email protected]>
  • Loading branch information
3 people authored Jan 21, 2024
1 parent c7b5a2c commit 05f420d
Show file tree
Hide file tree
Showing 27 changed files with 876 additions and 330 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/.idea/
.idea
/.gradle/
/build/
*.h
*.so
*.dll
*.dylib
.DS_Store
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ It boosts the power of Burp Suite while reducing the likelihood of fingerprintin

It does this without resorting to hacks, reflection or forked Burp Suite Community code. All code in this repository only leverages Burp's official Extender API.

![screenshot](./docs/screenshot.png)
![screenshot](./docs/settings.png)

## Showcase
[CloudFlare bot score](https://cloudflare.manfredi.io/en/tools/connection):
Expand All @@ -24,7 +24,7 @@ Then, the local server forwards the response back to Burp. The response header o
Configuration settings and other necessary information like the destination server address and protocol are sent to the local server per request by a magic header.
This magic header is stripped from the request before it's forwarded to the destination server, of course.

![diagram](./docs/diagram.png)
![diagram](./docs/basic_diagram.png)

> :information_source: Another option would've been to code an upstream proxy server and connect burp to it, but I personally needed an extension for customization and portability.
Expand All @@ -36,8 +36,18 @@ This magic header is stripped from the request before it's forwarded to the dest
## Configuration
This extension is 'plug and play' and should speak for itself. You can hover with your mouse over each field in the 'Awesome TLS' tab for more information about each field.

To load your custom Client Hello, you can capture it in Wireshark, copy client hello record as hex stream and paste it into the field "Hex Client Hello".
![screenshot](./docs/wireshark_capture_client_hello.png)
<details>
<summary>Advanced usage</summary>

In the 'advanced' tab, you can enable an additional proxy listener that will automatically apply the current fingerprint from the request:

![screenshot](./docs/advanced_settings.png)

When enabled, the diagram changes to this:

![diagram](./docs/advanced_diagram.png)

</details>

## Manual build Instructions
This extension was developed with JetBrains IntelliJ (and GoLand) IDE.
Expand Down
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,4 @@ copy_linux_arm
copy_linux_arm64
copy_windows_amd64
copy_windows_386
buildJar "fat"
buildJar "fat"
Binary file added docs/advanced_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/advanced_settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/basic_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/diagram.png
Binary file not shown.
Binary file removed docs/screenshot.png
Binary file not shown.
Binary file added docs/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 36 additions & 4 deletions src-go/server/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,45 @@ package main
import "C"

import (
"encoding/json"
"flag"
"fmt"
"log"

"server/internal"
"server/internal/tls"

"server"
)

func main() {
addr := flag.String("a", server.DefaultAddress, "Address to listen on ([ip:]port)")
spoofAddr := flag.String("spoof", server.DefaultSpoofProxyAddress, "Spoof proxy address to listen on ([ip:]port)")
flag.Parse()
log.Fatalln(server.StartServer(*addr))

defaultConfig, err := json.Marshal(internal.TransportConfig{
InterceptProxyAddr: server.DefaultInterceptProxyAddress,
BurpAddr: server.DefaultBurpProxyAddress,
Fingerprint: tls.DefaultFingerprint,
UseInterceptedFingerprint: false,
HttpTimeout: int(internal.DefaultHttpTimeout.Seconds()),
HttpKeepAliveInterval: int(internal.DefaultHttpKeepAlive.Seconds()),
IdleConnTimeout: int(internal.DefaultIdleConnTimeout.Seconds()),
TLSHandshakeTimeout: int(internal.DefaultTLSHandshakeTimeout.Seconds()),
})
if err != nil {
log.Fatalln(err)
}

if err := server.SaveSettings(string(defaultConfig)); err != nil {
log.Fatalln(err)
}

log.Fatalln(server.StartServer(*spoofAddr))
}

//export StartServer
func StartServer(address *C.char) *C.char {
if err := server.StartServer(C.GoString(address)); err != nil {
func StartServer(spoofAddr *C.char) *C.char {
if err := server.StartServer(C.GoString(spoofAddr)); err != nil {
return C.CString(err.Error())
}
return C.CString("")
Expand All @@ -32,6 +55,15 @@ func StopServer() *C.char {
return C.CString("")
}

//export SaveSettings
func SaveSettings(configJson *C.char) *C.char {
if err := server.SaveSettings(C.GoString(configJson)); err != nil {
return C.CString(err.Error())
}

return C.CString("")
}

//export SmokeTest
func SmokeTest() {
fmt.Println("smoke test success")
Expand Down
1 change: 1 addition & 0 deletions src-go/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/ooni/oohttp v0.6.1
github.com/open-ch/ja3 v1.0.1
github.com/refraction-networking/utls v1.3.2
)

Expand Down
2 changes: 2 additions & 0 deletions src-go/server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EIT
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/open-ch/ja3 v1.0.1 h1:kMqfkgS+cTasMlsQaJ627qlw7kA/qRZVTmF0BtFjOLQ=
github.com/open-ch/ja3 v1.0.1/go.mod h1:lTWgltvZDGQjIa/TjWTzfpCVa/eGP+szng2DWz9mAvk=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
github.com/sleeyax/oohttp v0.0.0-20230603105812-6ac0447b1a8e h1:evx5O2TAZdPLDCqPuEI5yo4Sg3LT5cImPVbno6HKM2s=
Expand Down
233 changes: 233 additions & 0 deletions src-go/server/intercept.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package server

import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"net"
"net/url"
"strings"
"sync"
"syscall"
"time"

http "github.com/ooni/oohttp"
"github.com/open-ch/ja3"
)

const (
tlsClientHelloMsgType = "16"

maxConnErrors = 5
)

type interceptProxy struct {
burpClient *http.Client
burpAddr string
mutex sync.RWMutex
clientHelloData map[string]string
listener net.Listener
ctx context.Context
cancel context.CancelFunc
}

func newInterceptProxy(interceptAddr, burpAddr string) (*interceptProxy, error) {
proxyURL, err := url.Parse(fmt.Sprintf("http://%s", burpAddr))
if err != nil {
return nil, err
}

tr := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}

l, err := net.Listen("tcp", interceptAddr)
if err != nil {
return nil, err
}

ctx, cancel := context.WithCancel(context.Background())

return &interceptProxy{
burpClient: &http.Client{
Transport: tr,
},
burpAddr: burpAddr,
mutex: sync.RWMutex{},
clientHelloData: map[string]string{},
listener: l,
ctx: ctx,
cancel: cancel,
}, nil
}

func (s *interceptProxy) getTLSFingerprint(sni string) string {
s.mutex.RLock()
defer s.mutex.RUnlock()

return s.clientHelloData[sni]
}

func (s *interceptProxy) Start() {
var errCounter int

for {
select {
case <-s.ctx.Done():
return
default:
if errCounter > maxConnErrors {
return
}

conn, err := s.listener.Accept()
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
errCounter++
log.Println(err)
time.Sleep(time.Second)
continue
} else if err != nil {
log.Println(err)
return
}

errCounter = 0

go s.handleConn(conn)
}
}
}

func (s *interceptProxy) Stop() error {
s.cancel()
return s.listener.Close()
}

func (s *interceptProxy) handleConn(in net.Conn) {
defer in.Close()

out, err := net.Dial("tcp", s.burpAddr)
if err != nil {
s.writeError(err)
return
}

defer out.Close()

inReader := io.TeeReader(in, out)
outReader := io.TeeReader(out, in)

var wg sync.WaitGroup

wg.Add(2)

go func() {
defer wg.Done()
s.readClientHello(inReader)
}()

go func() {
defer wg.Done()
s.readAll(outReader)
}()

wg.Wait()
}

func (s *interceptProxy) readClientHello(inReader io.Reader) {
var readClientHello bool
var length uint16
var clientHello []byte
var err error

for {
if readClientHello {
s.readAll(inReader)
return
}

buf := make([]byte, 1)
if _, err = inReader.Read(buf); err != nil {
s.writeError(err)
return
}

// catch ClientHello message type
if hex.EncodeToString(buf) != tlsClientHelloMsgType {
continue
}

clientHello = append(clientHello, buf...)

// read tls version
buf = make([]byte, 2)
if _, err = inReader.Read(buf); err != nil {
s.writeError(err)
return
}

clientHello = append(clientHello, buf...)

// read client hello length
buf = make([]byte, 2)
if _, err = inReader.Read(buf); err != nil {
s.writeError(err)
return
}

length = binary.BigEndian.Uint16(buf)
clientHello = append(clientHello, buf...)

// read remaining client hello by length
buf = make([]byte, length)
if _, err = inReader.Read(buf); err != nil {
s.writeError(err)
return
}

clientHello = append(clientHello, buf...)

readClientHello = true

j, err := ja3.ComputeJA3FromSegment(clientHello)
if err != nil {
s.writeError(err)
return
}

s.mutex.Lock()
s.clientHelloData[j.GetSNI()] = hex.EncodeToString(clientHello)
s.mutex.Unlock()
}
}

func (s *interceptProxy) readAll(reader io.Reader) {
_, err := io.ReadAll(reader)
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, syscall.ECONNRESET) && !errors.Is(err, syscall.EPIPE) {
s.writeError(err)
}
}

func (s *interceptProxy) writeError(err error) {
if errors.Is(err, io.EOF) {
return
}

log.Println(err)

reqErr := strings.NewReader(fmt.Sprintf("Awesome TLS intercept proxy error: %s", err.Error()))
req, err := http.NewRequest("POST", "http://awesome-tls-error", reqErr)
if err != nil {
log.Println(err)
}

_, err = s.burpClient.Do(req)
if err != nil {
log.Println(err)
}
}
2 changes: 1 addition & 1 deletion src-go/server/internal/tls/clienthello.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

type HexClientHello string

func (hexClientHello HexClientHello) ToClientHelloId() (*utls.ClientHelloSpec, error) {
func (hexClientHello HexClientHello) ToClientHelloSpec() (*utls.ClientHelloSpec, error) {
if hexClientHello == "" {
return nil, errors.New("empty client hello")
}
Expand Down
Loading

0 comments on commit 05f420d

Please sign in to comment.