Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 30 additions & 25 deletions cmd/interactsh-server/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"crypto/rand"
"crypto/tls"
"encoding/hex"
Expand Down Expand Up @@ -293,34 +294,38 @@ func main() {
)
switch {
case cliOptions.CertificatePath != "" && cliOptions.PrivateKeyPath != "":
var domain string
if len(cliOptions.Domains) > 0 {
domain = cliOptions.Domains[0]
}
acmeManagerTLS, acmeErr := acme.BuildTlsConfigWithCertAndKeyPaths(cliOptions.CertificatePath, cliOptions.PrivateKeyPath, domain)
if acmeErr != nil {
gologger.Error().Msgf("https will be disabled: %s", acmeErr)
reloader, reloaderErr := acme.NewCertReloader(cliOptions.CertificatePath, cliOptions.PrivateKeyPath)
if reloaderErr != nil {
gologger.Error().Msgf("https will be disabled: %s", reloaderErr)
} else {
tlsConfig = acmeManagerTLS
}
case !cliOptions.SkipAcme && len(cliOptions.Domains) > 0:
var certs []tls.Certificate
for idx, domain := range cliOptions.Domains {
trimmedDomain := strings.TrimSuffix(domain, ".")
hostmaster := serverOptions.Hostmasters[idx]
var acmeErr error
domainCerts, certFiles, acmeErr = acme.HandleWildcardCertificates(fmt.Sprintf("*.%s", trimmedDomain), hostmaster, acmeStore, cliOptions.Debug, cliOptions.Resolvers)
if acmeErr != nil {
gologger.Error().Msgf("An error occurred while applying for a certificate, error: %v", acmeErr)
gologger.Error().Msgf("Could not generate certs for auto TLS, https will be disabled")
} else {
certs = append(certs, domainCerts...)
go reloader.Start(context.Background())
tlsConfig = &tls.Config{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing TLS MinVersion allows downgrade to TLS 1.2 (CWE-327) — TLS configuration for custom certificates does not specify MinVersion, allowing clients to negotiate TLS 1.2. While Go 1.22+ defaults to TLS 1.2 minimum, explicitly setting TLS 1.3 provides defense-in-depth against future changes and protocol downgrade attacks.

Suggested Fix
Add MinVersion: tls.VersionTLS13 to the tls.Config at line 302:
tlsConfig = &tls.Config{
    GetCertificate: reloader.GetCertificate,
    NextProtos:     []string{"h2", "http/1.1"},
    MinVersion:     tls.VersionTLS13,
}

GetCertificate: reloader.GetCertificate,
NextProtos: []string{"h2", "http/1.1"},
}
}
var tlsErr error
tlsConfig, tlsErr = acme.BuildTlsConfigWithCerts("", certs...)
if tlsErr != nil {
gologger.Error().Msgf("An error occurred while preparing tls configuration, error: %v", tlsErr)
case !cliOptions.SkipAcme && len(cliOptions.Domains) > 0 && len(serverOptions.Hostmasters) > 0:
cfg, cfgErr := acme.NewCertmagicConfig(serverOptions.Hostmasters[0], acmeStore, cliOptions.Debug, cliOptions.Resolvers)
if cfgErr != nil {
gologger.Error().Msgf("Could not configure ACME: %s", cfgErr)
} else {
for _, domain := range cliOptions.Domains {
trimmedDomain := strings.TrimSuffix(domain, ".")
certs, files, acmeErr := acme.HandleWildcardCertificates(cfg, fmt.Sprintf("*.%s", trimmedDomain))
if acmeErr != nil {
gologger.Error().Msgf("An error occurred while applying for a certificate for %s: %v", domain, acmeErr)
gologger.Error().Msgf("Could not generate certs for auto TLS, https will be disabled")
} else {
domainCerts = append(domainCerts, certs...)
certFiles = append(certFiles, files...)
}
}
if len(domainCerts) > 0 {
tlsConfig = &tls.Config{
GetCertificate: cfg.GetCertificate,
NextProtos: []string{"h2", "http/1.1"},
}
}
}
}

Expand Down
41 changes: 11 additions & 30 deletions pkg/server/acme/acme_certbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ type CertificateFiles struct {
PrivKeyPath string
}

// HandleWildcardCertificates handles ACME wildcard cert generation with DNS
// challenge using certmagic library from caddyserver.
func HandleWildcardCertificates(domain, email string, store *Provider, debug bool, customResolvers []string) ([]tls.Certificate, []CertificateFiles, error) {
// NewCertmagicConfig creates and configures a *certmagic.Config for ACME DNS-01 challenge.
// The returned config can be reused across multiple HandleWildcardCertificates calls.
func NewCertmagicConfig(email string, store *Provider, debug bool, customResolvers []string) (*certmagic.Config, error) {
logger, err := zap.NewProduction()
if err != nil {
return nil, nil, err
return nil, err
}
certmagic.DefaultACME.Agreed = true
certmagic.DefaultACME.Email = email
Expand All @@ -54,8 +54,6 @@ func HandleWildcardCertificates(domain, email string, store *Provider, debug boo
}(),
},
}
originalDomain := strings.TrimPrefix(domain, "*.")

certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
if debug {
certmagic.DefaultACME.Logger = logger
Expand All @@ -67,6 +65,13 @@ func HandleWildcardCertificates(domain, email string, store *Provider, debug boo
if debug {
cfg.Logger = logger
}
return cfg, nil
}

// HandleWildcardCertificates handles ACME wildcard cert generation with DNS
// challenge using certmagic library from caddyserver.
func HandleWildcardCertificates(cfg *certmagic.Config, domain string) ([]tls.Certificate, []CertificateFiles, error) {
originalDomain := strings.TrimPrefix(domain, "*.")

var creating bool
if !certAlreadyExists(cfg, &certmagic.DefaultACME, domain) {
Expand Down Expand Up @@ -158,27 +163,3 @@ func ExtractCaddyPaths(cfg *certmagic.Config, issuer certmagic.Issuer, domain st
return
}

// BuildTlsConfigWithCertAndKeyPaths Build TlsConfig with certificates
func BuildTlsConfigWithCertAndKeyPaths(certPath, privKeyPath, domain string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, privKeyPath)
if err != nil {
return nil, errors.New("Could not load certs and private key")
}
return BuildTlsConfigWithCerts(domain, cert)
}

// BuildTlsConfigWithCerts Build TlsConfig with existing certificates
func BuildTlsConfigWithCerts(domain string, certs ...tls.Certificate) (*tls.Config, error) {
if certs == nil {
return nil, errors.New("no certificates provided")
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
Certificates: certs,
}
if domain != "" {
tlsConfig.ServerName = domain
}
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
return tlsConfig, nil
}
110 changes: 110 additions & 0 deletions pkg/server/acme/cert_reloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package acme

import (
"context"
"crypto/tls"
"os"
"sync/atomic"
"time"

"github.com/projectdiscovery/gologger"
)

const defaultCertCheckInterval = 1 * time.Hour

func certCheckInterval() time.Duration {
if v := os.Getenv("CERT_CHECK_INTERVAL"); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
gologger.Warning().Msgf("Invalid CERT_CHECK_INTERVAL %q, using default %s", v, defaultCertCheckInterval)
}
return defaultCertCheckInterval
}

// CertReloader watches a certificate/key file pair and reloads when files change on disk.
type CertReloader struct {
certPath string
keyPath string
cert atomic.Pointer[tls.Certificate]
modTimeNs atomic.Int64
}

// NewCertReloader loads the initial certificate and returns a reloader.
func NewCertReloader(certPath, keyPath string) (*CertReloader, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}

modTime, _ := latestModTime(certPath, keyPath)

r := &CertReloader{
certPath: certPath,
keyPath: keyPath,
}
r.cert.Store(&cert)
r.modTimeNs.Store(modTime.UnixNano())
return r, nil
}

// GetCertificate returns the current certificate. Safe for concurrent use.
func (r *CertReloader) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
return r.cert.Load(), nil
}

// Start polls for certificate file changes and reloads when detected.
// Blocks until ctx is cancelled.
// The check interval is configurable via the CERT_CHECK_INTERVAL env variable (e.g. "30m", "2h").
func (r *CertReloader) Start(ctx context.Context) {
interval := certCheckInterval()
gologger.Info().Msgf("Certificate reload check interval: %s", interval)
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.tryReload()
}
}
}

func (r *CertReloader) tryReload() {
mt, err := latestModTime(r.certPath, r.keyPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 TOCTOU vulnerability in certificate reload mechanism (CWE-367) — The tryReload() function has a time-of-check-time-of-use (TOCTOU) race condition. It checks file modification time at line 81, then loads the certificate at line 90. An attacker with filesystem access could replace the certificate files with symlinks between these operations, causing the server to load certificates from an attacker-controlled location.

Attack Example
1. Wait for tryReload() to call os.Stat() at line 81
2. Replace cert.pem with symlink to /tmp/attacker-cert.pem between lines 81-90
3. Server loads attacker's certificate at line 90 via tls.LoadX509KeyPair()
Suggested Fix
Use file descriptors instead of paths: open files with O_NOFOLLOW flag, fstat() the fd, then read from the fd. This prevents symlink following and eliminates the TOCTOU window. Example: fd := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW, 0); defer unix.Close(fd); fstat(fd); read certificate from fd

if err != nil {
gologger.Warning().Msgf("Could not stat certificate files: %s", err)
return
}
if mt.UnixNano() <= r.modTimeNs.Load() {
return
}

cert, err := tls.LoadX509KeyPair(r.certPath, r.keyPath)
if err != nil {
gologger.Warning().Msgf("Could not reload certificate: %s", err)
return
}

r.cert.Store(&cert)
r.modTimeNs.Store(mt.UnixNano())
gologger.Info().Msgf("Reloaded TLS certificate from %s", r.certPath)
}

// latestModTime returns the most recent modification time of the two files.
func latestModTime(certPath, keyPath string) (time.Time, error) {
certInfo, err := os.Stat(certPath)
if err != nil {
return time.Time{}, err
}
keyInfo, err := os.Stat(keyPath)
if err != nil {
return time.Time{}, err
}
if keyInfo.ModTime().After(certInfo.ModTime()) {
return keyInfo.ModTime(), nil
}
return certInfo.ModTime(), nil
}
Loading
Loading