Skip to content

Commit 05f420d

Browse files
VovkoOsleeyaxvladimir.razdrogin
authored
Add auto TLS fingerprint (#51)
Co-authored-by: Sleeyax <yourd3veloper@gmail.com> Co-authored-by: vladimir.razdrogin <vladimir.razdrogin@aliexress.ru>
1 parent c7b5a2c commit 05f420d

27 files changed

+876
-330
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
/.idea/
1+
.idea
22
/.gradle/
33
/build/
44
*.h
55
*.so
66
*.dll
77
*.dylib
8+
.DS_Store

README.md

+14-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ It boosts the power of Burp Suite while reducing the likelihood of fingerprintin
44

55
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.
66

7-
![screenshot](./docs/screenshot.png)
7+
![screenshot](./docs/settings.png)
88

99
## Showcase
1010
[CloudFlare bot score](https://cloudflare.manfredi.io/en/tools/connection):
@@ -24,7 +24,7 @@ Then, the local server forwards the response back to Burp. The response header o
2424
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.
2525
This magic header is stripped from the request before it's forwarded to the destination server, of course.
2626

27-
![diagram](./docs/diagram.png)
27+
![diagram](./docs/basic_diagram.png)
2828

2929
> :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.
3030
@@ -36,8 +36,18 @@ This magic header is stripped from the request before it's forwarded to the dest
3636
## Configuration
3737
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.
3838

39-
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".
40-
![screenshot](./docs/wireshark_capture_client_hello.png)
39+
<details>
40+
<summary>Advanced usage</summary>
41+
42+
In the 'advanced' tab, you can enable an additional proxy listener that will automatically apply the current fingerprint from the request:
43+
44+
![screenshot](./docs/advanced_settings.png)
45+
46+
When enabled, the diagram changes to this:
47+
48+
![diagram](./docs/advanced_diagram.png)
49+
50+
</details>
4151

4252
## Manual build Instructions
4353
This extension was developed with JetBrains IntelliJ (and GoLand) IDE.

build.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,4 @@ copy_linux_arm
107107
copy_linux_arm64
108108
copy_windows_amd64
109109
copy_windows_386
110-
buildJar "fat"
110+
buildJar "fat"

docs/advanced_diagram.png

18.1 KB
Loading

docs/advanced_settings.png

32.9 KB
Loading

docs/basic_diagram.png

13.9 KB
Loading

docs/diagram.png

-20.3 KB
Binary file not shown.

docs/screenshot.png

-76 KB
Binary file not shown.

docs/settings.png

24.4 KB
Loading

src-go/server/cmd/main.go

+36-4
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,45 @@ package main
33
import "C"
44

55
import (
6+
"encoding/json"
67
"flag"
78
"fmt"
89
"log"
910

11+
"server/internal"
12+
"server/internal/tls"
13+
1014
"server"
1115
)
1216

1317
func main() {
14-
addr := flag.String("a", server.DefaultAddress, "Address to listen on ([ip:]port)")
18+
spoofAddr := flag.String("spoof", server.DefaultSpoofProxyAddress, "Spoof proxy address to listen on ([ip:]port)")
1519
flag.Parse()
16-
log.Fatalln(server.StartServer(*addr))
20+
21+
defaultConfig, err := json.Marshal(internal.TransportConfig{
22+
InterceptProxyAddr: server.DefaultInterceptProxyAddress,
23+
BurpAddr: server.DefaultBurpProxyAddress,
24+
Fingerprint: tls.DefaultFingerprint,
25+
UseInterceptedFingerprint: false,
26+
HttpTimeout: int(internal.DefaultHttpTimeout.Seconds()),
27+
HttpKeepAliveInterval: int(internal.DefaultHttpKeepAlive.Seconds()),
28+
IdleConnTimeout: int(internal.DefaultIdleConnTimeout.Seconds()),
29+
TLSHandshakeTimeout: int(internal.DefaultTLSHandshakeTimeout.Seconds()),
30+
})
31+
if err != nil {
32+
log.Fatalln(err)
33+
}
34+
35+
if err := server.SaveSettings(string(defaultConfig)); err != nil {
36+
log.Fatalln(err)
37+
}
38+
39+
log.Fatalln(server.StartServer(*spoofAddr))
1740
}
1841

1942
//export StartServer
20-
func StartServer(address *C.char) *C.char {
21-
if err := server.StartServer(C.GoString(address)); err != nil {
43+
func StartServer(spoofAddr *C.char) *C.char {
44+
if err := server.StartServer(C.GoString(spoofAddr)); err != nil {
2245
return C.CString(err.Error())
2346
}
2447
return C.CString("")
@@ -32,6 +55,15 @@ func StopServer() *C.char {
3255
return C.CString("")
3356
}
3457

58+
//export SaveSettings
59+
func SaveSettings(configJson *C.char) *C.char {
60+
if err := server.SaveSettings(C.GoString(configJson)); err != nil {
61+
return C.CString(err.Error())
62+
}
63+
64+
return C.CString("")
65+
}
66+
3567
//export SmokeTest
3668
func SmokeTest() {
3769
fmt.Println("smoke test success")

src-go/server/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.20
44

55
require (
66
github.com/ooni/oohttp v0.6.1
7+
github.com/open-ch/ja3 v1.0.1
78
github.com/refraction-networking/utls v1.3.2
89
)
910

src-go/server/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EIT
44
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
55
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
66
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
7+
github.com/open-ch/ja3 v1.0.1 h1:kMqfkgS+cTasMlsQaJ627qlw7kA/qRZVTmF0BtFjOLQ=
8+
github.com/open-ch/ja3 v1.0.1/go.mod h1:lTWgltvZDGQjIa/TjWTzfpCVa/eGP+szng2DWz9mAvk=
79
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
810
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
911
github.com/sleeyax/oohttp v0.0.0-20230603105812-6ac0447b1a8e h1:evx5O2TAZdPLDCqPuEI5yo4Sg3LT5cImPVbno6HKM2s=

src-go/server/intercept.go

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"encoding/binary"
6+
"encoding/hex"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"log"
11+
"net"
12+
"net/url"
13+
"strings"
14+
"sync"
15+
"syscall"
16+
"time"
17+
18+
http "github.com/ooni/oohttp"
19+
"github.com/open-ch/ja3"
20+
)
21+
22+
const (
23+
tlsClientHelloMsgType = "16"
24+
25+
maxConnErrors = 5
26+
)
27+
28+
type interceptProxy struct {
29+
burpClient *http.Client
30+
burpAddr string
31+
mutex sync.RWMutex
32+
clientHelloData map[string]string
33+
listener net.Listener
34+
ctx context.Context
35+
cancel context.CancelFunc
36+
}
37+
38+
func newInterceptProxy(interceptAddr, burpAddr string) (*interceptProxy, error) {
39+
proxyURL, err := url.Parse(fmt.Sprintf("http://%s", burpAddr))
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
tr := &http.Transport{
45+
Proxy: http.ProxyURL(proxyURL),
46+
}
47+
48+
l, err := net.Listen("tcp", interceptAddr)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
ctx, cancel := context.WithCancel(context.Background())
54+
55+
return &interceptProxy{
56+
burpClient: &http.Client{
57+
Transport: tr,
58+
},
59+
burpAddr: burpAddr,
60+
mutex: sync.RWMutex{},
61+
clientHelloData: map[string]string{},
62+
listener: l,
63+
ctx: ctx,
64+
cancel: cancel,
65+
}, nil
66+
}
67+
68+
func (s *interceptProxy) getTLSFingerprint(sni string) string {
69+
s.mutex.RLock()
70+
defer s.mutex.RUnlock()
71+
72+
return s.clientHelloData[sni]
73+
}
74+
75+
func (s *interceptProxy) Start() {
76+
var errCounter int
77+
78+
for {
79+
select {
80+
case <-s.ctx.Done():
81+
return
82+
default:
83+
if errCounter > maxConnErrors {
84+
return
85+
}
86+
87+
conn, err := s.listener.Accept()
88+
var netErr net.Error
89+
if errors.As(err, &netErr) && netErr.Timeout() {
90+
errCounter++
91+
log.Println(err)
92+
time.Sleep(time.Second)
93+
continue
94+
} else if err != nil {
95+
log.Println(err)
96+
return
97+
}
98+
99+
errCounter = 0
100+
101+
go s.handleConn(conn)
102+
}
103+
}
104+
}
105+
106+
func (s *interceptProxy) Stop() error {
107+
s.cancel()
108+
return s.listener.Close()
109+
}
110+
111+
func (s *interceptProxy) handleConn(in net.Conn) {
112+
defer in.Close()
113+
114+
out, err := net.Dial("tcp", s.burpAddr)
115+
if err != nil {
116+
s.writeError(err)
117+
return
118+
}
119+
120+
defer out.Close()
121+
122+
inReader := io.TeeReader(in, out)
123+
outReader := io.TeeReader(out, in)
124+
125+
var wg sync.WaitGroup
126+
127+
wg.Add(2)
128+
129+
go func() {
130+
defer wg.Done()
131+
s.readClientHello(inReader)
132+
}()
133+
134+
go func() {
135+
defer wg.Done()
136+
s.readAll(outReader)
137+
}()
138+
139+
wg.Wait()
140+
}
141+
142+
func (s *interceptProxy) readClientHello(inReader io.Reader) {
143+
var readClientHello bool
144+
var length uint16
145+
var clientHello []byte
146+
var err error
147+
148+
for {
149+
if readClientHello {
150+
s.readAll(inReader)
151+
return
152+
}
153+
154+
buf := make([]byte, 1)
155+
if _, err = inReader.Read(buf); err != nil {
156+
s.writeError(err)
157+
return
158+
}
159+
160+
// catch ClientHello message type
161+
if hex.EncodeToString(buf) != tlsClientHelloMsgType {
162+
continue
163+
}
164+
165+
clientHello = append(clientHello, buf...)
166+
167+
// read tls version
168+
buf = make([]byte, 2)
169+
if _, err = inReader.Read(buf); err != nil {
170+
s.writeError(err)
171+
return
172+
}
173+
174+
clientHello = append(clientHello, buf...)
175+
176+
// read client hello length
177+
buf = make([]byte, 2)
178+
if _, err = inReader.Read(buf); err != nil {
179+
s.writeError(err)
180+
return
181+
}
182+
183+
length = binary.BigEndian.Uint16(buf)
184+
clientHello = append(clientHello, buf...)
185+
186+
// read remaining client hello by length
187+
buf = make([]byte, length)
188+
if _, err = inReader.Read(buf); err != nil {
189+
s.writeError(err)
190+
return
191+
}
192+
193+
clientHello = append(clientHello, buf...)
194+
195+
readClientHello = true
196+
197+
j, err := ja3.ComputeJA3FromSegment(clientHello)
198+
if err != nil {
199+
s.writeError(err)
200+
return
201+
}
202+
203+
s.mutex.Lock()
204+
s.clientHelloData[j.GetSNI()] = hex.EncodeToString(clientHello)
205+
s.mutex.Unlock()
206+
}
207+
}
208+
209+
func (s *interceptProxy) readAll(reader io.Reader) {
210+
_, err := io.ReadAll(reader)
211+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, syscall.ECONNRESET) && !errors.Is(err, syscall.EPIPE) {
212+
s.writeError(err)
213+
}
214+
}
215+
216+
func (s *interceptProxy) writeError(err error) {
217+
if errors.Is(err, io.EOF) {
218+
return
219+
}
220+
221+
log.Println(err)
222+
223+
reqErr := strings.NewReader(fmt.Sprintf("Awesome TLS intercept proxy error: %s", err.Error()))
224+
req, err := http.NewRequest("POST", "http://awesome-tls-error", reqErr)
225+
if err != nil {
226+
log.Println(err)
227+
}
228+
229+
_, err = s.burpClient.Do(req)
230+
if err != nil {
231+
log.Println(err)
232+
}
233+
}

src-go/server/internal/tls/clienthello.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
type HexClientHello string
1212

13-
func (hexClientHello HexClientHello) ToClientHelloId() (*utls.ClientHelloSpec, error) {
13+
func (hexClientHello HexClientHello) ToClientHelloSpec() (*utls.ClientHelloSpec, error) {
1414
if hexClientHello == "" {
1515
return nil, errors.New("empty client hello")
1616
}

0 commit comments

Comments
 (0)