feat: support http tunneled rtsp (#419)

* enhancement: supporting http tunneled rtsp

* refactor: simplify HTTP tunnel support per review feedback

- Extract streamCandidate() for nmap port classification
- Add isCommonHTTPPort() for masscan and nmap fallback
- Move URL building to Stream.String() and Stream.URL()
- Pass Stream directly to attack methods instead of individual args
- Add TLS config for HTTPS tunnel support
- Make auth detection non-fatal for tunneled streams
- Rename HTTPTunnel to UseHTTPTunnel

* - Testing the auth workflow for the tunneled streams is not blocking the rest of the pipeline since I changed the return values to Auth unknown and nil
- added extra ports in the test according to suggestions

* fixing some lint errors

* removing the unused buildrtspurl

* delayed the urlstream call for clarity

removed error messages

refactored the test that used the deprecated buildTRSPurl to use stream.URL and stream.String() methods

* extracting iscommonHTTP port to pkg/ports (package ports)

switching on u.scheme to create proper schemes for http and https

* refactor: replace HTTP tunnel bool with scheme-based detection; enable TLS only for HTTPS-tunneled streams

* chore: simnplify InferTunnelScheme and newRTSPClient

* fix: remove rendundant check in streamCandidate

* fix: typo in parseScheme

* tests: coverage for new schemes

* fix: use RTSP and not RTSPS for HTTPS URLs

* fix: tunneled RTSP scheme handling and auth detection fallback

* ui: render empty credentials as none in summary and TUI

* chore: ignore duplicate-string warning for none literal

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: rtsps probe headers

---------

Co-authored-by: Brendan Le Glaunec <brendan@glaulabs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lawrence Arryl Lopez
2026-03-18 01:41:20 -04:00
committed by GitHub
parent 14dcb74e89
commit 8531c006d4
15 changed files with 626 additions and 136 deletions
+31 -39
View File
@@ -291,33 +291,26 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
}
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
stream.Routes = []string{route}
code, err := a.describeStatus(stream)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
}
code, err := a.describeStatus(u)
if err != nil {
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
return access, nil
}
func (a Attacker) credAttack(stream cameradar.Stream, username, password string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, stream.Route(), username, password)
stream.Username = username
stream.Password = password
code, err := a.describeStatus(stream)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
}
code, err := a.describeStatus(u)
if err != nil {
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
return code == base.StatusOK || code == base.StatusNotFound, nil
}
@@ -330,32 +323,27 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
return stream, ctx.Err()
}
u, urlStr, err := buildRTSPURL(stream, stream.Route(), stream.Username, stream.Password)
if err != nil {
return stream, fmt.Errorf("building rtsp url: %w", err)
}
client, err := a.newRTSPClient(u)
client, err := a.newRTSPClient(stream)
if err != nil {
return stream, fmt.Errorf("starting rtsp client: %w", err)
}
defer client.Close()
desc, res, err := a.describeWithRetry(ctx, client, u, urlStr)
desc, res, err := a.describeWithRetry(ctx, client, stream)
if err != nil {
return a.handleDescribeError(stream, urlStr, err)
return a.handleDescribeError(stream, err)
}
a.logDescribeResponse(urlStr, res)
a.logDescribeResponse(stream.String(), res)
if desc == nil || len(desc.Medias) == 0 {
return stream, fmt.Errorf("no media tracks found for %q", urlStr)
return stream, fmt.Errorf("no media tracks found for %q", stream)
}
res, err = client.Setup(desc.BaseURL, desc.Medias[0], 0, 0)
if err != nil {
return a.handleSetupError(stream, urlStr, err)
return a.handleSetupError(stream, err)
}
a.logSetupResponse(urlStr, res)
a.logSetupResponse(stream.String(), res)
stream.Available = res != nil && res.StatusCode == base.StatusOK
if stream.Available {
@@ -365,11 +353,15 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
return stream, nil
}
func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, u *base.URL, urlStr string) (*description.Session, *base.Response, error) {
func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, stream cameradar.Stream) (*description.Session, *base.Response, error) {
u, err := stream.URL()
if err != nil {
return nil, nil, fmt.Errorf("building rtsp url: %w", err)
}
var (
desc *description.Session
res *base.Response
err error
)
for range 5 {
desc, res, err = client.Describe(u)
@@ -379,7 +371,7 @@ func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Clien
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", urlStr, badStatus.Code))
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", stream, badStatus.Code))
select {
case <-ctx.Done():
return nil, nil, ctx.Err()
@@ -391,13 +383,13 @@ func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Clien
return nil, nil, err
}
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", urlStr, err)
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", stream, err)
}
func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
func (a Attacker) handleDescribeError(stream cameradar.Stream, err error) (cameradar.Stream, error) {
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, badStatus.Code))
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
stream.Address.String(),
stream.Port,
@@ -407,20 +399,20 @@ func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, er
return stream, nil
}
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", stream, err))
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
return stream, fmt.Errorf("performing describe request at %q: %w", stream, err)
}
func (a Attacker) handleSetupError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
func (a Attacker) handleSetupError(stream cameradar.Stream, err error) (cameradar.Stream, error) {
var badStatus liberrors.ErrClientBadStatusCode
if errors.As(err, &badStatus) {
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, badStatus.Code))
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", stream, badStatus.Code))
stream.Available = badStatus.Code == base.StatusOK
return stream, nil
}
return stream, fmt.Errorf("performing setup request at %q: %w", urlStr, err)
return stream, fmt.Errorf("performing setup request at %q: %w", stream, err)
}
func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {