DittoTRX: A Maltego transform server for IDN homograph attacks

May 22, 2021

A while ago, Simone published his awesome ditto tool for checking a domain for active imposter attacks, and I finally found some time to play with it.

In this Blog post, I’ll share some insights from initial experiments and recon, and open source the toolchain I created to run a central ditto transform server to analyze the data in Maltego.

This way multiple analysts can run transforms to check domains for active imposters without calculating the variations and checking them on their own machine, but on a dedicated server instead. The server can take care of throttling the external lookups to avoid being rate limited, future updates could add a queuing mechanism as well.

[0x0] Index

[0x1] IDN Homograph attacks

Internationalized domain name homograph attacks exploit the fact that characters from different alphabets often look alike the ones from the Latin alphabet (= they are homographs). A malicious actor can use this to deceive a victim, for example to visit a malicious website that pretends to be the legitimate one, a classic phishing attack. A regular user of facebook.com may be tricked into to clicking a link where the Latin character “a” is replaced with the Cyrillic character “а”.

You can read more about IDN homograph attacks on wikipedia.

[0x2] Maltego Code

I’ll be demoing the use of my new Maltego golang package here, and show how to create a simple transform server for any tool in very little time.

At the core, all we need is an HTTP service that replies to the request from the Maltego transform service.

The maltego package provides a primitive to register http handlers:

func RegisterTransform(handlerFunc http.HandlerFunc, name string)

Since the ditto tool invocations can take longer depending on configuration and your hardware, I configured the http service to never time out. Querying all registered and live imposter domains for common names like amazon etc, without varying TLDs, took between 2-3 minutes for me.

To handle requests to the root page, I used the

func Home(w http.ResponseWriter, r *http.Request)

handler, which will give a simple text based overview of the registered handlers and their routes once visited.

func main() {

	flag.Parse()

	const (
		// ditto status names
		registered = "registered"
		available = "available"
	)

	// register transforms to http.DefaultServeMux

	// all similar domains
	maltego.RegisterTransform(ditto("", false), "similarDomains")

	// only registered domains
	maltego.RegisterTransform(ditto(registered, false), "registeredDomains")

	// only registered domains that resolve to an IP
	maltego.RegisterTransform(ditto(registered, true), "liveDomains")

	// only show domains that are available and not registered
	maltego.RegisterTransform(ditto(available, false), "availableDomains")

	// only live domains that resolve for all TLDs
	maltego.RegisterTransform(ditto(registered, true, "-tld"), "liveDomainsTLD")

	// register catch all handler to serve home page
	http.HandleFunc("/", maltego.Home)

	fmt.Println("serving at", *flagAddr)

	s := &http.Server{
		Addr:              *flagAddr,
		Handler:           http.DefaultServeMux,
		ReadTimeout:       0,
		ReadHeaderTimeout: 0,
		WriteTimeout:      0,
		IdleTimeout:       0,
		MaxHeaderBytes:    0,
	}

	// start server
	err := s.ListenAndServe()
	if err != nil {
		log.Fatal("failed to serve HTTP: ", err)
	}
}

Let’s look at the implementation of ditto invocations:

var ditto = func(status string, hasIP bool, args ...string) http.HandlerFunc {

	return maltego.MakeHandler(func(w http.ResponseWriter, r *http.Request, t *maltego.Transform) {

		// get host that was queried
		host := t.RequestMessage.Entities.Items[0].Value

		fmt.Println("got request from", r.RemoteAddr, "to lookup:", host)

		if !govalidator.IsDNSName(host) {
			t.AddUIMessage("invalid domain: "+host, maltego.UIMessageFatal)
			return
		}

		id, err := cryptoutils.RandomString(10)
		if err != nil {
			fmt.Println("failed to generate id:", err)
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		file := filepath.Join(storage, id+"-"+host)

		finalArgs := []string{"-domain", host, "-no-progress-bar", "-csv", file, "-throttle=1000", "-workers=4", "-whois"}
		for _, a := range args {
			finalArgs = append(finalArgs, a)
		}

		start := time.Now()

		// run ditto
		// we are running inside a docker container, the ditto binary has been copied into it at build time. 
		// TODO: drop privileges
		out, err := exec.Command("/root/ditto",  finalArgs...).CombinedOutput()
		if err != nil {
			fmt.Println(string(out))
			fmt.Println("failed to run ditto:", err)
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		data, err := ioutil.ReadFile(file)
		if err != nil {
			fmt.Println("failed to read file:", err)
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		rd := csv.NewReader(bytes.NewReader(data))
		records, err := rd.ReadAll()
		if err != nil {
			fmt.Println("failed to read CSV records:", err)
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		fmt.Println("results for", host, "=", len(records), "in", time.Since(start))

		// process results
		for i, rec := range records {
			//fmt.Println(rec)
			if i == 0 {
				// skip CSV header
				continue
			}

			// handle status if provided
			if status != "" {
				if rec[2] != status {
					continue
				}
			}

			if hasIP {
				if rec[3] == "" {
					continue
				}
			}

			addEntity(t, rec)
		}

		_ = os.Remove(file)
	})
}

As you can see, the ditto function is a wrapper to invoke the ditto commandline tool with different flag combinations, by wrapping the

func MakeHandler(handler func(w http.ResponseWriter, r *http.Request, t *Transform)) http.HandlerFunc

primitive to construct transform handlers.

Ditto generates a CSV file as output, which is read, processed and afterwards deleted again. Some simple filtering is applied based on the desired status or the existence of an IP address before collecting the entities.

Adding entities is done using a simple helper function, that takes the CSV output from ditto and assigns the fields to the correct properties:

func addEntity(t *maltego.Transform, rec []string) {
	e := t.AddEntity("dittotrx.IDNDomain", rec[0])
	e.AddProp("unicode", rec[0])
	e.AddProp("ascii", rec[1])
	e.AddProp("status", rec[2])
	e.AddProp("ips", rec[3])
	e.AddProp("names", rec[4])

	e.AddProp("registrar", rec[5])
	e.AddProp("created_at", rec[6])
	e.AddProp("updated_at", rec[7])
	e.AddProp("expires_at", rec[8])
	e.AddProp("nameservers", rec[9])
}

Now that we have some logic for the server side, we need to create a client side Maltego configuration.

Running the transformations will produce output entities, which are modeled based on the data that ditto provides. In order to further drill down on their data, we will also add a couple of local transforms. These can be found in cmd/transform:

$ tree cmd/transform/
├── lookupAddr
│   └── main.go
├── toDateCreatedAt
│   └── main.go
├── toDomains
│   └── main.go
├── toIPAddresses
│   └── main.go
├── toNameServers
│   └── main.go
├── toRegistrars
│   └── main.go
└── visitDomain
    └── main.go

7 directories, 7 files

Let’s look at the implementation of a simple local transform, as a standalone binary:

// This is an example for a local transformation that does a reverse name lookup for a given address.
// It will take an IP address and return the hostnames associated with it as maltego entities.
func main() {

	log.Println(os.Args[1:])

	// parse arguments
	lt := maltego.ParseLocalArguments(os.Args[1:])

	// ensure the provided address is valid
	ip := net.ParseIP(lt.Value)
	if ip == nil {
		maltego.Die("invalid ip", lt.Value+" is not a valid IP address")
	}

	// lookup provided ip address
	names, err := net.LookupAddr(lt.Value)
	if err != nil {
		maltego.Die(err.Error(), "failed to lookup address")
	}

	// create new transform
	t := maltego.Transform{}

	// iterate over lookup results
	for _, host := range names {
		e := t.AddEntity(maltego.DNSName, host)
		e.AddProperty("hostname", "Hostname", maltego.Strict, host)
	}

	t.AddUIMessage("complete", maltego.UIMessageInform)

	// return output to stdout and exit cleanly (exit code 0)
	fmt.Println(t.ReturnOutput())
}

Parsing the commandline args provided by maltego is done with maltego.ParseLocalArguments, I discard the first element in the args array via slicing, because it is the name of the program.

We can then access the main value the transform shall operate on from lt.Value and try to parse it as an IP. If that fails, the transform can’t proceed and execution is stopped via maltego.Die, which will exit the program and display an error message to the user in Maltego. If everything is fine, we perform the lookup, construct a new maltego.Transform, add the resulting names as maltego.DNSName entities (there are constants for the core maltego types provided in my maltego go package), and exit the program cleanly using t.ReturnOutput(). That’s it - less than 20 lines of code and you have a local transform!

Let’s compile the local transforms and move their binaries to /usr/local/bin:

go build -o /usr/local/bin/lookupAddr cmd/transform/lookupAddr/*.go
go build -o /usr/local/bin/toDateCreatedAt cmd/transform/toDateCreatedAt/*.go
go build -o /usr/local/bin/toDomains cmd/transform/toDomains/*.go
go build -o /usr/local/bin/toIPAddresses cmd/transform/toIPAddresses/*.go
go build -o /usr/local/bin/toNameServers cmd/transform/toNameServers/*.go
go build -o /usr/local/bin/toRegistrars cmd/transform/toRegistrars/*.go
go build -o /usr/local/bin/visitDomain cmd/transform/visitDomain/*.go

I’ve created a small helper tool to generate Maltego entities and transforms based on a YAML configuration, since configuring everything via the UI can be tedious. I’ll cover the details of this tool in another blogpost, for now we will just look at a simple example.

The following yaml is used to generate the Maltego configuration:

org: DittoTRX
author: Philipp Mieden
description: Transformations for the ditto tool

entities:
  - name: IDNDomain
    image:
      name: domain
      color: black
    description: A domain likely used for phishing
    parent: maltego.Domain
    fields:
      - name: unicode
        description: Unicode representation of domain name
      - name: ascii
        description: ASCII representation of domain name
      - name: status
        description: Registration status of domain name
      - name: ips
        description: IPs for the domain name
      - name: names
        description: DNS names that resolve to this domains IPs
      - name: registrar
        description: The name of the registrar
      - name: created_at
        description: The date of creation
      - name: updated_at
        description: The last update
      - name: expires_at
        description: The date of expiry
      - name: nameservers
        description: Nameservers associated with domain

workingDir: ~

transforms:
  - id: LookupAddr
    input: maltego.IPv4Address
    description: Lookup Address
    executable: /usr/local/bin/lookupAddr
  - id: ToDomainNames
    input: dittotrx.IDNDomain
    description: To Domain Names
    executable: /usr/local/bin/toDomains
  - id: ToRegistrarNames
    input: dittotrx.IDNDomain
    description: To Registrar Names
    executable: /usr/local/bin/toRegistrars
  - id: ToNameServers
    input: dittotrx.IDNDomain
    description: To Name Servers
    executable: /usr/local/bin/toNameServers
  - id: ToCreationDate
    input: dittotrx.IDNDomain
    description: To Creation Date
    executable: /usr/local/bin/toDateCreatedAt
  - id: ToIPAddresses
    input: dittotrx.IDNDomain
    description: To IP Addresses
    executable: /usr/local/bin/toIPAddresses
  - id: VisitDomain
    input: dittotrx.IDNDomain
    description: Open Domain in default Browser
    executable: /usr/local/bin/visitDomain

As you can see, the transforms all operate on our custom entity type IDNDomain and have a description and an executable set.

To generate the config, all we need to run is:

$ maltego-gen maltego.yml
material icon repository exists, pulling
bootstrapped configuration archive for Maltego
packing maltego dittotrx archive
packed maltego dittotrx archive
copied generated file to /Users/you/DittoTRX.mtz

The resulting .mtz archive can now be imported with Maltego:

Now all that is left is to head to your Transform Distribution Server and add the transforms. For simplicity, I am using a basic authentication, it is protected over TLS. Future versions could use oauth as suggested by Maltego.

You can read more about how to set up a TDS or add transforms in the excellent Maltego docs.

[0x3] Recon

So lets go and check some domains!

Create a standard maltego.Domain, add your value and show all transforms:

All the transforms that end with [Ditto] are from the ditto service. You can filter only for these by entering a search term:

I’ll choose the transform ‘Similar Live Domains’ to ask the server to calculate all possible variations for me, and then check each result if the domain is registered, where it is pointing to, and if the underlying IP address is reachable.

We can repeat this for many big companies and yield results, even though the majority seems to be leftovers from past campaigns, that are no longer active, but still registered and redirect to their registrars page:

I’ve added a transform to open all selected entities in the default system browser, so it’s easy to open them all at once to see what content they are currently serving.

Obviously this poses a security risk, one of the sites could serve an exploit that affects your browser! Consider doing this only in a sandbox.

The Chrome browser issued a warning when visiting the sites:

Safari does not care, several websites also have a valid TLS certificate:

I noticed that many sites are not always delivering the same result, so after reloading them they would serve a different page. This is likely because the server checks certain parameters that are set during redirects, to only deliver payloads to intended victims.

Some instagram imposter is redirecting to a malicious flash player update, that seems to be adware according to the virustotal scan.

A youtube imposter politely asks for my location:

Some websites have expired certificates, which indicates they have been operated before, as setting up a service with TLS requires additional effort, and who would do that if they did not intend to use it for something…

Let’s play with the view settings to look at the different amazon domains, by increasing the size of entities the more outgoing links they have:

Let’s change the view again to look at the most common registrars for amazon imposters. Now, the more links go towards an entity, the bigger it will become:

An instagram follower optimization service, that takes their users privacy very serious:

Several sites enjoy protection from cloudflare:

Fun fact: Cloudflare is the only one of the checked domains that has only a single imposter domain:

And that one (https://xn--cludflare-6x3d.com/) is owned by Simone and redirects to the ditto github repo ;-)

Some websites are on block lists already and will trigger a warning in the browser:

One of the netflix imposters has a LiteSpeed Web Server running that serves amazon named content:

There is also support to show the Creation Date of the IDN domains, lets see what it shows for microsoft:

Below are different graph arrangement modes, very useful to understand the entity hierarchy or find clusters in bigger data sets:

When double clicking an IDNDomain entity, the properties can be inspected:

This blogpost only touches the surface of what can be done using this Maltego integration, give it a try and let me know what you think!

From exploring a couple domains from big fortune 500 companies, I get a feeling that plenty of those domains have been used in the past for advertising or distribution of malware. Several services responded but did only serve a default web server page or a 404, likely because some token or identifier was missing, that would have been added when the victim clicks a prepared link.

I would argue that any connections to domains that use such homograph character variations should be blocked by corporate intrusion detection systems, as their only use is to fool a human victim.

It’s nice to have a tool to check for such attacks, automating this to monitor domain creation in real time might help to detect fake domains as they are being set up, and the ditto tool already supports that!

Exploring the results in Maltego is very convenient and allows to dig into different aspects quickly, especially because the extracted data can be further enriched using other transforms.

I’ve tested Safari, Chrome and Firefox for opening the imposter websites, and only the Chrome browser issued a warning to the user. That makes this technique certainly dangerous and useful to trick people. Registered and reachable imposter domains do seem to exist for every big company I’ve checked, which confirms the IDN homograph attacks are used in the wild.

[0x4] Resources

Maltego Graphs

Maltego Configuration

Repositories