235 lines
5.2 KiB
Go
235 lines
5.2 KiB
Go
package attack
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"net/textproto"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Ullaakut/cameradar/v6"
|
|
"github.com/bluenviron/gortsplib/v5"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/headers"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
|
)
|
|
|
|
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
|
|
client := &gortsplib.Client{
|
|
ReadTimeout: a.timeout,
|
|
WriteTimeout: a.timeout,
|
|
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
client.Scheme = u.Scheme
|
|
client.Host = u.Host
|
|
|
|
err := client.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) {
|
|
client, err := a.newRTSPClient(u)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer client.Close()
|
|
|
|
_, res, err := client.Describe(u)
|
|
if err != nil {
|
|
var badStatus liberrors.ErrClientBadStatusCode
|
|
if errors.As(err, &badStatus) {
|
|
return badStatus.Code, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
if res == nil {
|
|
return 0, errors.New("no response received")
|
|
}
|
|
|
|
return res.StatusCode, nil
|
|
}
|
|
|
|
// probeDescribeHeaders performs a manual DESCRIBE request and returns the status code and headers.
|
|
//
|
|
// NOTE: We do not use gortsplib here because it does not expose response headers when the status code is 401 Unauthorized,
|
|
// which is exactly what we need in order to detect authentication methods.
|
|
func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr string) (base.StatusCode, base.Header, error) {
|
|
dialer := &net.Dialer{Timeout: a.timeout}
|
|
var conn net.Conn
|
|
var err error
|
|
|
|
if u.Scheme == "rtsps" {
|
|
tlsDialer := &tls.Dialer{NetDialer: dialer, Config: &tls.Config{InsecureSkipVerify: true}}
|
|
conn, err = tlsDialer.DialContext(ctx, "tcp", u.Host)
|
|
} else {
|
|
conn, err = dialer.DialContext(ctx, "tcp", u.Host)
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
deadline = time.Now().Add(a.timeout)
|
|
}
|
|
|
|
err = conn.SetDeadline(deadline)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
request := fmt.Sprintf(
|
|
"DESCRIBE %s RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: cameradar\r\nAccept: application/sdp\r\nHost: %s\r\n\r\n",
|
|
urlStr,
|
|
u.Host,
|
|
)
|
|
_, err = conn.Write([]byte(request))
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
reader := textproto.NewReader(bufio.NewReader(conn))
|
|
statusLine, err := reader.ReadLine()
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
fields := strings.Fields(statusLine)
|
|
if len(fields) < 2 {
|
|
return 0, nil, fmt.Errorf("invalid RTSP status line: %q", statusLine)
|
|
}
|
|
|
|
code, err := strconv.Atoi(fields[1])
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("parsing RTSP status code %q: %w", fields[1], err)
|
|
}
|
|
|
|
mimeHeader, err := reader.ReadMIMEHeader()
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
headers := make(base.Header)
|
|
for key, values := range mimeHeader {
|
|
headers[key] = append(base.HeaderValue(nil), values...)
|
|
}
|
|
|
|
return base.StatusCode(code), headers, nil
|
|
}
|
|
|
|
func (a Attacker) handleRedirect(stream *cameradar.Stream, resHeaders base.Header) {
|
|
locations := headerValues(resHeaders, "Location")
|
|
if len(locations) == 0 {
|
|
return
|
|
}
|
|
location, err := url.Parse(locations[0])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch location.Scheme {
|
|
case "rtsps":
|
|
stream.Secure = true
|
|
case "rtsp":
|
|
stream.Secure = false
|
|
}
|
|
|
|
if location.Hostname() != "" {
|
|
if addr, err := netip.ParseAddr(location.Hostname()); err == nil {
|
|
stream.Address = addr
|
|
}
|
|
}
|
|
|
|
if location.Port() != "" {
|
|
if port, err := strconv.Atoi(location.Port()); err == nil {
|
|
stream.Port = uint16(port)
|
|
}
|
|
}
|
|
}
|
|
|
|
func authTypeFromHeaders(values base.HeaderValue) cameradar.AuthType {
|
|
if len(values) == 0 {
|
|
return cameradar.AuthUnknown
|
|
}
|
|
|
|
var hasBasic bool
|
|
var hasDigest bool
|
|
|
|
for _, value := range values {
|
|
var authHeader headers.Authenticate
|
|
err := authHeader.Unmarshal(base.HeaderValue{value})
|
|
if err != nil {
|
|
lower := strings.ToLower(value)
|
|
hasDigest = hasDigest || strings.Contains(lower, "digest")
|
|
hasBasic = hasBasic || strings.Contains(lower, "basic")
|
|
continue
|
|
}
|
|
|
|
switch authHeader.Method {
|
|
case headers.AuthMethodDigest:
|
|
hasDigest = true
|
|
case headers.AuthMethodBasic:
|
|
hasBasic = true
|
|
}
|
|
}
|
|
|
|
if hasDigest {
|
|
return cameradar.AuthDigest
|
|
}
|
|
if hasBasic {
|
|
return cameradar.AuthBasic
|
|
}
|
|
return cameradar.AuthUnknown
|
|
}
|
|
|
|
func headerValues(header base.Header, name string) base.HeaderValue {
|
|
if header == nil {
|
|
return nil
|
|
}
|
|
for key, values := range header {
|
|
if strings.EqualFold(key, name) {
|
|
return values
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildRTSPURL(stream cameradar.Stream, route, username, password string) (*base.URL, string, error) {
|
|
host := net.JoinHostPort(stream.Address.String(), strconv.Itoa(int(stream.Port)))
|
|
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
|
|
|
|
scheme := "rtsp"
|
|
if stream.Secure {
|
|
scheme = "rtsps"
|
|
}
|
|
|
|
u := &url.URL{
|
|
Scheme: scheme,
|
|
Host: host,
|
|
Path: path,
|
|
}
|
|
if username != "" || password != "" {
|
|
u.User = url.UserPassword(username, password)
|
|
}
|
|
|
|
urlStr := u.String()
|
|
parsed, err := base.ParseURL(urlStr)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return parsed, urlStr, nil
|
|
}
|